// SPDX-License-Identifier: GPL-3.0-or-later

#include "clipboardserver.h"

#include "common/action.h"
#include "common/appconfig.h"
#include "common/clientsocket.h"
#include "common/client_server.h"
#include "common/commandstatus.h"
#include "common/config.h"
#include "common/log.h"
#include "common/mimetypes.h"
#include "common/shortcuts.h"
#include "common/sleeptimer.h"
#include "common/timer.h"
#include "common/textdata.h"
#include "common/version.h"
#include "gui/actionhandler.h"
#include "gui/clipboardbrowser.h"
#include "gui/commanddialog.h"
#include "gui/iconfactory.h"
#include "gui/mainwindow.h"
#include "gui/notificationbutton.h"
#include "gui/notificationdaemon.h"
#include "item/itemfactory.h"
#include "item/itemstore.h"
#include "item/serialize.h"
#include "scriptable/scriptableproxy.h"
#include "scriptable/scriptoverrides.h"

#include <QAction>
#include <QApplication>
#include <QApplicationStateChangeEvent>
#include <QFile>
#include <QKeyEvent>
#include <QMenu>
#include <QMessageBox>
#include <QMimeData>
#include <QPixmapCache>
#include <QPushButton>
#include <QSessionManager>
#include <QStyleFactory>
#include <QTimer>
#include <QTextEdit>
#include <QWindow>

#ifdef COPYQ_GLOBAL_SHORTCUTS
#include "../qxt/qxtglobalshortcut.h"
#else
class QxtGlobalShortcut final {};
#endif

#include <memory>

namespace {

uint monitorCommandStateHash(const QVector<Command> &commands)
{
    uint seed = 0;
#if QT_VERSION >= QT_VERSION_CHECK(6,10,0)
    QtPrivate::QHashCombine hash(seed);
#else
    QtPrivate::QHashCombine hash;
#endif

    for (const auto &command : commands) {
        if (command.type() == CommandType::Script)
            seed = hash(seed, command.cmd);
        else if (command.type() == CommandType::Automatic && !command.input.isEmpty())
            seed = hash(seed, command.input);
    }

    return seed;
}

void setTabWidth(QTextEdit *editor, int spaces)
{
    const QLatin1Char space(' ');
    const QFontMetricsF metrics(editor->fontMetrics());
    const qreal width = metrics.horizontalAdvance(QString(spaces, space));
    editor->setTabStopDistance(width);
}

void cleanUpLogFilesTimer()
{
    bool ok;
    int maxLogSize = qEnvironmentVariableIntValue("COPYQ_MAX_LOG_SIZE", &ok);
    if (!ok)
        maxLogSize = 10 * 1024 * 1024;

    int maxFiles = qEnvironmentVariableIntValue("COPYQ_MAX_LOG_FILES", &ok);
    if (!ok)
        maxFiles = 100;

    constexpr int startIntervalMs = 30 * 1000;
    constexpr int cleanUpIntervalMs = 1 * 60 * 60 * 1000;

    auto timer = new QTimer(qApp);
    timer->setSingleShot(false);
    timer->setInterval(cleanUpIntervalMs);
    timer->start();

    auto callback = [=](){ dropLogsToFileCountAndSize(maxFiles, maxLogSize); };
    QTimer::singleShot(startIntervalMs, qApp, callback);
    QObject::connect(timer, &QTimer::timeout, qApp, callback);
}

void setPreventScreenCapture(QWindow *window, bool prevent)
{
    platformNativeInterface()->setPreventScreenCapture(window->winId(), prevent);
}

} // namespace

ClipboardServer::ClipboardServer(QApplication *app, const QString &sessionName)
    : QObject()
    , App(app, sessionName)
    , m_wnd(nullptr)
    , m_shortcutActions()
    , m_ignoreKeysTimer()
    , m_sharedData(std::make_shared<ClipboardBrowserShared>())
{
    // Server-spawned subprocesses connect back to an already-running server.
    // Skip the connection retry delay so they fail fast during shutdown.
    qputenv("COPYQ_WAIT_FOR_SERVER_MS", "0");

    m_server = new Server(clipboardServerName(sessionName), this);

    if ( m_server->isListening() ) {
        log( QStringLiteral("Starting server: CopyQ %1").arg(versionString) );
        App::installTranslator();
        qApp->setLayoutDirection(QLocale().textDirection());
    } else {
        App::installTranslator();
        log( tr("CopyQ server is already running."), LogWarning );
        exit(0);
        return;
    }

    if ( qgetenv("COPYQ_QT_FILE_DIALOGS") == "1" ) {
        QCoreApplication::setAttribute(Qt::AA_DontUseNativeDialogs);
    }

    cleanUpLogFilesTimer();

    if ( sessionName.isEmpty() ) {
        QGuiApplication::setApplicationDisplayName(QStringLiteral("CopyQ"));
    } else {
        log( QStringLiteral("Session: %1").arg(sessionName) );
        QGuiApplication::setApplicationDisplayName(
            QStringLiteral("CopyQ-%1").arg(sessionName));
    }

    QApplication::setQuitOnLastWindowClosed(false);

    ensureSettingsDirectoryExists();

    m_sharedData->itemFactory = new ItemFactory(m_sharedData, this);
    m_sharedData->notifications = new NotificationDaemon(this);
    m_sharedData->actions = new ActionHandler(m_sharedData->notifications, this);
    m_wnd = new MainWindow(m_sharedData);
    qApp->setProperty("CopyQ_server", QVariant::fromValue(static_cast<QObject*>(this)));

    connect( m_sharedData->notifications, &NotificationDaemon::notificationButtonClicked,
             this, &ClipboardServer::onNotificationButtonClicked );

    m_sharedData->itemFactory->loadPlugins();
    if ( !m_sharedData->itemFactory->hasLoaders() )
        log("No plugins loaded", LogNote);

    connect( m_server, &Server::newConnection,
             this, &ClipboardServer::onClientNewConnection );

    connect( qApp, &QCoreApplication::aboutToQuit,
             this, &ClipboardServer::onAboutToQuit );

    connect( qApp, &QGuiApplication::commitDataRequest, this, &ClipboardServer::onCommitData );
    connect( qApp, &QGuiApplication::saveStateRequest, this, &ClipboardServer::onSaveState );

    connect( m_wnd, &MainWindow::requestExit,
             this, &ClipboardServer::maybeQuit );
    connect( m_wnd, &MainWindow::disableClipboardStoringRequest,
             this, &ClipboardServer::onDisableClipboardStoringRequest );
    connect( m_wnd, &MainWindow::sendActionData,
             this, &ClipboardServer::sendActionData );
    connect( m_wnd, &MainWindow::stopAction,
             this, &ClipboardServer::stopAction );

    // notify window if configuration changes
    connect( m_wnd, &MainWindow::configurationChanged,
             this, &ClipboardServer::loadSettings );

    connect( m_wnd, &MainWindow::commandsSaved,
             this, &ClipboardServer::onCommandsSaved );

    m_server->start();

    {
        AppConfig appConfig;
        loadSettings(&appConfig);
    }

    m_wnd->setCurrentTab(0);
    m_wnd->enterBrowseMode();

    qApp->installEventFilter(this);

    // Ignore global shortcut key presses in any widget.
    m_ignoreKeysTimer.setInterval(100);
    m_ignoreKeysTimer.setSingleShot(true);

    initSingleShotTimer(&m_timerClearUnsentActionData, 2000, this, [&]() {
        m_actionDataToSend.clear();
    });

    initSingleShotTimer(&m_timerCleanItemFiles, 120000, this, &ClipboardServer::cleanDataFiles);

    initSingleShotTimer(&m_updateThemeTimer, 1000, this, [this](){
        AppConfig appConfig;
        loadSettings(&appConfig);
    });

    setClipboardMonitorRunning(false);
    startMonitoring();

    callback(QStringLiteral("onStart"));
}

ClipboardServer::~ClipboardServer()
{
    qApp->setProperty("CopyQ_server", QVariant());

    removeGlobalShortcuts();

    delete m_wnd;
    m_wnd = nullptr;

    delete m_sharedData->actions;
    m_sharedData->actions = nullptr;

    delete m_sharedData->notifications;
    m_sharedData->notifications = nullptr;

    delete m_sharedData->itemFactory;
    m_sharedData->itemFactory = nullptr;
}

QStringList ClipboardServer::copyqStats() const
{
    const int total = m_clients.size();
    const bool hasClipboardProvider = m_provideClipboardClientId != 0
        && m_clients.contains(m_provideClipboardClientId);
    const bool hasSelectionProvider = m_provideSelectionClientId != 0
        && m_clients.contains(m_provideSelectionClientId);

    return {QStringLiteral("CLIENTS: connected=%1, clipboard_provider=%2, selection_provider=%3")
        .arg(total)
        .arg(hasClipboardProvider ? "yes" : "no")
        .arg(hasSelectionProvider ? "yes" : "no")};
}

void ClipboardServer::stopMonitoring()
{
    if (!m_monitor)
        return;

    COPYQ_LOG("Terminating monitor");
    setClipboardMonitorRunning(false);

    const auto client = findClient(m_monitor->id());
    if (client)
        client->sendMessage(QByteArray(), CommandStop);
}

void ClipboardServer::startMonitoring()
{
    if (m_monitor || m_exitting || m_ignoreNewConnections || !m_wnd->isMonitoringEnabled())
        return;

    COPYQ_LOG("Starting monitor");

    m_monitor = new Action();
    m_monitor->setCommand(QStringLiteral("copyq --clipboard-access monitorClipboard"));
    connect( m_monitor.data(), &QObject::destroyed,
             this, &ClipboardServer::onMonitorFinished );
    m_sharedData->actions->internalAction(m_monitor);
}

void ClipboardServer::removeGlobalShortcuts()
{
    for (auto it = m_shortcutActions.constBegin(); it != m_shortcutActions.constEnd(); ++it)
        delete it.key();
    m_shortcutActions.clear();
}

void ClipboardServer::onCommandsSaved(const QVector<Command> &commands)
{
#ifdef COPYQ_GLOBAL_SHORTCUTS
    removeGlobalShortcuts();

    QList<QKeySequence> usedShortcuts;

    for (const auto &command : commands) {
        if (command.type() & CommandType::GlobalShortcut) {
            for (const auto &shortcutText : command.globalShortcuts) {
                QKeySequence shortcut(shortcutText, QKeySequence::PortableText);
                if ( !shortcut.isEmpty() && !usedShortcuts.contains(shortcut) ) {
                    usedShortcuts.append(shortcut);
                    createGlobalShortcut(shortcut, command);
                }
            }
        }
    }
#endif

    const auto hash = monitorCommandStateHash(commands);
    if ( m_monitor && hash != m_monitorCommandsStateHash ) {
        m_monitorCommandsStateHash = hash;
        stopMonitoring();
        startMonitoring();
    }
}

void ClipboardServer::onAboutToQuit()
{
    COPYQ_LOG("Closing server.");

    // Avoid calling onExit multiple times.
    // (QCoreApplication::aboutToQuit() signal can be emitted multiple times
    // when system is shutting down.)
    if (m_exitting)
        return;
    m_exitting = true;

    if (m_wnd->isScriptOverridden(ScriptOverrides::OnExit))
        callback(QStringLiteral("onExit"));
    waitForCallbackToFinish();

    m_ignoreNewConnections = true;

    terminateClients(10000);
    m_server->close(); // No new connections can be open.
    terminateClients(5000);

    m_wnd->saveTabs();
    cleanDataFiles();
}

void ClipboardServer::onCommitData(QSessionManager &sessionManager)
{
    COPYQ_LOG("Got commit data request from session manager.");

    const bool cancel = sessionManager.allowsInteraction() && !askToQuit();
    sessionManager.release();

    if (cancel) {
        sessionManager.cancel();
        startMonitoring();
    } else {
        m_wnd->saveTabs();

#ifdef Q_OS_WIN
        // WORKAROUND: This is required to exit application from
        //             installer, otherwise main window is only
        //             minimized after this when tray is disabled.
        m_wnd->hide();
        exit();
#endif
    }
}

void ClipboardServer::onSaveState(QSessionManager &sessionManager)
{
    COPYQ_LOG("Got save state request from session manager.");

    QSettings settings(QSettings::IniFormat, QSettings::UserScope, "copyq", "copyq_no_session");
    const auto sessionNameKey = "session_" + sessionManager.sessionId();
    const auto sessionName = qApp->property("CopyQ_session_name").toString();
    settings.setValue(sessionNameKey, sessionName);

    const QString lastSessionIdPrefix = "last_session_id_for_";
    const auto lastSessionIdKey = lastSessionIdPrefix + sessionName;
    settings.setValue(lastSessionIdKey, sessionNameKey);

    // Remove no longer valid sessions from configuration.
    QSet<QString> validSessions;
    for (const QString &key : settings.childKeys()) {
        if ( key.startsWith(lastSessionIdPrefix) )
            validSessions.insert( settings.value(key).toString() );
    }

    for (const QString &key : settings.childKeys()) {
        if ( !key.startsWith(lastSessionIdPrefix) && !validSessions.contains(key) )
            settings.remove(key);
    }
}

void ClipboardServer::onDisableClipboardStoringRequest(bool disabled)
{
    stopMonitoring();
    if (!disabled)
        startMonitoring();
}

void ClipboardServer::maybeQuit()
{
    if ( askToQuit() )
        exit();
}

bool ClipboardServer::askToQuit()
{
    if ( !m_wnd->maybeCloseCommandDialog() )
        return false;

    if ( hasRunningCommands() ) {
        QMessageBox messageBox( QMessageBox::Warning, tr("Cancel Active Commands"),
                                tr("Cancel active commands and exit?"), QMessageBox::NoButton,
                                m_wnd );

        messageBox.addButton(tr("Cancel Exiting"), QMessageBox::RejectRole);
        QAbstractButton *exitButton = messageBox.addButton(tr("Exit Anyway"), QMessageBox::AcceptRole);

        // Close the message box automatically after all running commands finish.
        QTimer timerCheckRunningCommands;
        timerCheckRunningCommands.setInterval(1000);
        connect( &timerCheckRunningCommands, &QTimer::timeout,
                 exitButton, [&]() {
                    if ( !hasRunningCommands() )
                        exitButton->click();
                 });
        timerCheckRunningCommands.start();

        messageBox.exec();
        return exitButton == messageBox.clickedButton();
    }

    return true;
}

bool ClipboardServer::hasRunningCommands() const
{
    if (!m_sharedData->actions)
        return false;

    if ( m_sharedData->actions->runningActionCount() > 0 )
        return true;

    for (auto it = m_clients.constBegin(); it != m_clients.constEnd(); ++it) {
        const auto actionId = it.value().proxy->actionId();
        if ( !m_sharedData->actions->isInternalActionId(actionId) )
            return true;
    }

    return false;
}

void ClipboardServer::terminateClients(int waitMs)
{
    const auto clients = m_clients;
    for (auto it = clients.constBegin(); it != clients.constEnd(); ++it) {
        const auto &clientData = it.value();
        if (clientData.isValid())
            clientData.client->sendMessage(QByteArray(), CommandStop);
    }

    waitForClientsToFinish(waitMs);
    emit closeClients();
    waitForClientsToFinish(waitMs / 2);
}

void ClipboardServer::waitForClientsToFinish(int waitMs)
{
    SleepTimer t(waitMs);
    while ( !m_clients.isEmpty() && t.sleep() ) {}
}

void ClipboardServer::waitForCallbackToFinish()
{
    if (m_callback) {
        COPYQ_LOG("Waiting for callback to finish");
        m_callback->waitForFinished();
        COPYQ_LOG("Callback finished");
    }
}

void ClipboardServer::callback(const QString &scriptFunction)
{
    if (!m_sharedData->actions)
        return;

    waitForCallbackToFinish();
    COPYQ_LOG( QStringLiteral("Starting callback: %1").arg(scriptFunction) );
    m_callback = new Action();
    m_callback->setCommand(QStringList() << "copyq" << scriptFunction);
    m_sharedData->actions->internalAction(m_callback);
}

ClientSocketPtr ClipboardServer::findClient(int actionId)
{
    for (auto it = m_clients.constBegin(); it != m_clients.constEnd(); ++it) {
        const auto &clientData = it.value();
        if ( clientData.isValid() && clientData.proxy->actionId() == actionId )
            return clientData.client;
    }

    return nullptr;
}

void ClipboardServer::sendActionData(int actionId, const QByteArray &bytes)
{
    const auto client = findClient(actionId);
    if (client) {
        client->sendMessage(bytes, CommandData);
    } else {
        m_actionDataToSend[actionId] = bytes;
        m_timerClearUnsentActionData.start();
    }
}

void ClipboardServer::stopAction(int actionId)
{
    const auto client = findClient(actionId);
    if (client) {
        client->sendMessage(QByteArray(), CommandStop);
    } else {
        m_pendingStopActionIds.insert(actionId);
        QTimer::singleShot(10000, this, [this, actionId]() {
            m_pendingStopActionIds.remove(actionId);
        });
    }
}

void ClipboardServer::cleanDataFiles()
{
    COPYQ_LOG("Cleaning unused item files");
    ::cleanDataFiles( m_wnd->tabs(), &m_sharedData->encryptionKey );
}

void ClipboardServer::setPreventScreenCapture(bool prevent)
{
    if (m_preventScreenCapture == prevent)
        return;

    m_preventScreenCapture = prevent;
    for (QWindow *window : qApp->allWindows()) {
        ::setPreventScreenCapture(window, m_preventScreenCapture);
    }
}

void ClipboardServer::onClientNewConnection(const ClientSocketPtr &client)
{
    auto proxy = new ScriptableProxy(m_wnd);
    proxy->setClientSocketId(client->id());
    connect( client.get(), &ClientSocket::destroyed,
             proxy, &ScriptableProxy::safeDeleteLater );
    connect( proxy, &ScriptableProxy::sendMessage,
             client.get(), &ClientSocket::sendMessage );

    m_clients.insert( client->id(), ClientData(client, proxy) );
    connect( this, &ClipboardServer::closeClients,
             client.get(), &ClientSocket::close );
    connect( client.get(), &ClientSocket::messageReceived,
             this, &ClipboardServer::onClientMessageReceived );
    connect( client.get(), &ClientSocket::disconnected,
             this, &ClipboardServer::onClientDisconnected );
    connect( client.get(), &ClientSocket::disconnected,
             proxy, &ScriptableProxy::disconnectClient );
    connect( client.get(), &ClientSocket::connectionFailed,
             this, &ClipboardServer::onClientConnectionFailed );
    client->start();
    std::weak_ptr<ClientSocket> weakClient = client;
    connect( proxy, &ScriptableProxy::actionIdChanged, this, [this, weakClient](int actionId) {
        if (m_pendingStopActionIds.remove(actionId)) {
            if (auto c = weakClient.lock())
                c->sendMessage(QByteArray(), CommandStop);
        }
    });

    connect( proxy, &ScriptableProxy::clipboardProviderRegistered,
             this, &ClipboardServer::onClipboardProviderRegistered );

    if (m_ignoreNewConnections) {
        COPYQ_LOG("Ignoring new client while exiting");
        client->sendMessage(QByteArray(), CommandStop);
    }
}

void ClipboardServer::onClientMessageReceived(
        const QByteArray &message, int messageCode, ClientSocketId clientId)
{
    switch (messageCode) {
    case CommandFunctionCallPriority:
    case CommandFunctionCall: {
        const auto &clientData = m_clients.value(clientId);
        if (!clientData.isValid())
            return;

        // Postpone processing clients after configuration is loaded
        if (m_loadingSettings && messageCode == CommandFunctionCall) {
            m_pendingMessages.append({message, messageCode, clientId});
            return;
        }

        clientData.proxy->callFunction(message);
        break;
    }
    case CommandReceiveData: {
        const auto &clientData = m_clients.value(clientId);
        if (!clientData.isValid())
            return;

        const int actionId = clientData.proxy->actionId();
        const QByteArray bytes = m_actionDataToSend.take(actionId);
        clientData.client->sendMessage(bytes, CommandData);
        break;
    }
    default:
        log(QStringLiteral("Unhandled command status: %1").arg(messageCode));
        break;
    }
}

void ClipboardServer::onClientDisconnected(ClientSocketId clientId)
{
    if (m_provideClipboardClientId == clientId)
        m_provideClipboardClientId = 0;
    if (m_provideSelectionClientId == clientId)
        m_provideSelectionClientId = 0;
    m_clients.remove(clientId);
}

void ClipboardServer::onClipboardProviderRegistered(ClientSocketId clientId, ClipboardMode mode)
{
    ClientSocketId &tracked = mode == ClipboardMode::Clipboard
        ? m_provideClipboardClientId
        : m_provideSelectionClientId;

    if (tracked != 0 && tracked != clientId) {
        auto it = m_clients.find(tracked);
        if (it != m_clients.end() && it->isValid())
            it->client->sendMessage(QByteArray(), CommandStop);
    }

    tracked = clientId;
}

void ClipboardServer::onClientConnectionFailed(ClientSocketId clientId)
{
    log("Client connection failed", LogWarning);
    m_clients.remove(clientId);
}

void ClipboardServer::onMonitorFinished()
{
    COPYQ_LOG("Monitor finished");
    if (!m_monitor)
        setClipboardMonitorRunning(false);
    stopMonitoring();
    startMonitoring();
}

void ClipboardServer::onNotificationButtonClicked(const NotificationButton &button)
{
    if (!m_sharedData->actions)
        return;

    const QString mimeNotificationData = COPYQ_MIME_PREFIX "notification-data";

    QVariantMap data;
    data.insert(mimeNotificationData, button.data);

    auto act = new Action();
    act->setCommand( button.script, QStringList(getTextData(data)) );
    act->setInputWithFormat(data, mimeNotificationData);
    act->setData(data);
    m_sharedData->actions->action(act);
}

void ClipboardServer::createGlobalShortcut(const QKeySequence &shortcut, const Command &command)
{
#ifndef COPYQ_GLOBAL_SHORTCUTS
    Q_UNUSED(shortcut)
    Q_UNUSED(command)
#else
    auto s = new QxtGlobalShortcut(shortcut, command.localizedName(), this);
    if (!s->isValid()) {
        log(QStringLiteral("Failed to set global shortcut \"%1\" for command \"%2\".")
            .arg(shortcut.toString(),
                 command.name),
            LogWarning);
        delete s;
        return;
    }

    connect( s, &QxtGlobalShortcut::activated,
             this, &ClipboardServer::shortcutActivated );

    m_shortcutActions[s] = command;
#endif
}

bool ClipboardServer::eventFilter(QObject *object, QEvent *ev)
{
    const QEvent::Type type = ev->type();

    if ( type == QEvent::KeyPress
         || type == QEvent::Shortcut
         || type == QEvent::ShortcutOverride )
    {
        if ( m_ignoreKeysTimer.isActive() ) {
            ev->accept();
            return true;
        }

        m_wnd->updateShortcuts();

        // Close menu on Escape key and give focus back to search edit or browser.
        if (type == QEvent::KeyPress) {
            QKeyEvent *keyevent = static_cast<QKeyEvent *>(ev);
            QMenu *menu = qobject_cast<QMenu*>(object);
            if (menu && keyevent->key() == Qt::Key_Escape) {
                menu->close();
                if (m_wnd->browseMode())
                    m_wnd->enterBrowseMode();
                else
                    m_wnd->enterSearchMode();
            }
        // Omit overriding arrow keys in text editors.
        } else if ( type == QEvent::ShortcutOverride &&
                    (object->metaObject()->className() == QLatin1String("QLineEdit")
                     || object->property("textInteractionFlags")
                        .value<Qt::TextInteractionFlags>()
                        .testFlag(Qt::TextSelectableByKeyboard)) )
        {
            QKeyEvent *keyevent = static_cast<QKeyEvent *>(ev);
            if ( keyevent->key() == Qt::Key_Left
                 || keyevent->key() == Qt::Key_Right
                 || keyevent->key() == Qt::Key_Up
                 || keyevent->key() == Qt::Key_Down)
            {
                ev->accept();
                return true;
            }
        }
    } else if (type == QEvent::Paint) {
        setActivePaintDevice(object);
    } else if (type == QEvent::FontChange) {
        QTextEdit *editor = qobject_cast<QTextEdit*>(object);
        if (editor)
            setTabWidth(editor, m_textTabSize);
    } else if ( type == QEvent::ApplicationStateChange && m_saveOnDeactivate ) {
        const auto stateChangeEvent = static_cast<QApplicationStateChangeEvent*>(ev);
        const auto state = stateChangeEvent->applicationState();
        if (state != Qt::ApplicationActive) {
            COPYQ_LOG( QStringLiteral("Saving items on application state change (%1)").arg(state) );
            m_wnd->saveTabs();
            m_timerCleanItemFiles.start();
        }
    } else if (type == QEvent::ThemeChange) {
        if ( !m_updateThemeTimer.isActive() )
            COPYQ_LOG("Got theme change event");
        QPixmapCache::clear();
        m_updateThemeTimer.start();
    } else if (m_preventScreenCapture && type == QEvent::Show) {
        auto widget = qobject_cast<QWidget*>(object);
        if (widget && widget->windowHandle())
            ::setPreventScreenCapture(widget->windowHandle(), m_preventScreenCapture);
    }

    return false;
}

void ClipboardServer::loadSettings(AppConfig *appConfig)
{
    if (!m_sharedData->itemFactory || m_loadingSettings)
        return;

    m_loadingSettings = true;

    const int gen = qApp->property("CopyQ_config_generation").toInt();
    qApp->setProperty("CopyQ_config_generation", gen + 1);
    COPYQ_LOG("Loading configuration");

    QSettings &settings = appConfig->settings();

    m_sharedData->itemFactory->loadItemFactorySettings(&settings);

    const QString styleName = appConfig->option<Config::style>();
    if ( !styleName.isEmpty() ) {
        log( QStringLiteral("Style: %1").arg(styleName) );
        QStyle *style = QStyleFactory::create(styleName);
        if (style) {
            QApplication::setStyle(style);
        } else {
            const QString styles = QStyleFactory::keys().join(QLatin1String(", "));
            log( QStringLiteral("Failed to set style, valid are: %1").arg(styles), LogWarning );
        }
    }

    settings.beginGroup(QStringLiteral("Theme"));
    m_sharedData->theme.loadTheme(settings);
    settings.endGroup();

    m_sharedData->editor = appConfig->option<Config::editor>();
    m_sharedData->maxItems = appConfig->option<Config::maxitems>();
    m_sharedData->textWrap = appConfig->option<Config::text_wrap>();
    m_sharedData->navigationStyle = appConfig->option<Config::navigation_style>();
    m_sharedData->saveOnReturnKey = !appConfig->option<Config::edit_ctrl_return>();
    m_sharedData->moveItemOnReturnKey = appConfig->option<Config::move>();
    m_sharedData->showSimpleItems = appConfig->option<Config::show_simple_items>();
    m_sharedData->numberSearch = appConfig->option<Config::number_search>();
    m_sharedData->minutesToExpire = appConfig->option<Config::expire_tab>();
    m_sharedData->encryptedExpireSeconds = appConfig->option<Config::expire_encrypted_tab_seconds>();
    m_sharedData->tabsEncrypted = appConfig->option<Config::encrypt_tabs>();
    m_sharedData->saveDelayMsOnItemAdded = appConfig->option<Config::save_delay_ms_on_item_added>();
    m_sharedData->saveDelayMsOnItemModified = appConfig->option<Config::save_delay_ms_on_item_modified>();
    m_sharedData->saveDelayMsOnItemRemoved = appConfig->option<Config::save_delay_ms_on_item_removed>();
    m_sharedData->saveDelayMsOnItemMoved = appConfig->option<Config::save_delay_ms_on_item_moved>();
    m_sharedData->saveDelayMsOnItemEdited = appConfig->option<Config::save_delay_ms_on_item_edited>();
    m_sharedData->rowIndexFromOne = appConfig->option<Config::row_index_from_one>();

    m_sharedData->actions->setMaxRowCount(appConfig->option<Config::max_process_manager_rows>());

    m_wnd->loadSettings(settings, appConfig);

    m_textTabSize = appConfig->option<Config::text_tab_width>();
    m_saveOnDeactivate = appConfig->option<Config::save_on_app_deactivated>();

    if ( platformNativeInterface()->canPreventScreenCapture() )
        setPreventScreenCapture(appConfig->option<Config::prevent_screen_capture>());

    if (m_monitor) {
        stopMonitoring();
        startMonitoring();
    }

    m_sharedData->notifications->setNativeNotificationsEnabled(
        appConfig->option<Config::native_notifications>() );
    m_sharedData->notifications->setNotificationOpacity(
        m_sharedData->theme.color(QStringLiteral("notification_bg")).alphaF() );
    m_sharedData->notifications->setNotificationStyleSheet(
        m_sharedData->theme.getNotificationStyleSheet() );

    int id = appConfig->option<Config::notification_position>();
    NotificationDaemon::Position position;
    switch (id) {
    case 0: position = NotificationDaemon::Top; break;
    case 1: position = NotificationDaemon::Bottom; break;
    case 2: position = NotificationDaemon::TopRight; break;
    case 3: position = NotificationDaemon::BottomRight; break;
    case 4: position = NotificationDaemon::BottomLeft; break;
    default: position = NotificationDaemon::TopLeft; break;
    }
    m_sharedData->notifications->setPosition(position);

    const int x = appConfig->option<Config::notification_horizontal_offset>();
    const int y = appConfig->option<Config::notification_vertical_offset>();
    m_sharedData->notifications->setOffset(x, y);

    const int w = appConfig->option<Config::notification_maximum_width>();
    const int h = appConfig->option<Config::notification_maximum_height>();
    m_sharedData->notifications->setMaximumSize(w, h);

    m_sharedData->notifications->updateNotificationWidgets();

    m_updateThemeTimer.stop();

    COPYQ_LOG("Configuration loaded");
    m_loadingSettings = false;

    const QList<ClientMessage> pendingMessages = std::move(m_pendingMessages);
    Q_ASSERT(m_pendingMessages.isEmpty());
    for (const auto &msg : pendingMessages)
        onClientMessageReceived(msg.message, msg.messageCode, msg.clientId);
}

void ClipboardServer::shortcutActivated(QxtGlobalShortcut *shortcut)
{
#ifndef COPYQ_GLOBAL_SHORTCUTS
    Q_UNUSED(shortcut)
#else
    m_ignoreKeysTimer.start();

    const QMap<QxtGlobalShortcut*, Command>::const_iterator it =
            m_shortcutActions.constFind(shortcut);
    if ( it != m_shortcutActions.constEnd() ) {
        const QString shortcutText = portableShortcutText(shortcut->shortcut());
        const Command &command = it.value();

        // If global shortcut for a menu command is triggered when the main window
        // is active, the command will be executed as if it has been trigger from
        // menu - i.e. with item selection and item data available.
        if ( command.inMenu && m_wnd->isActiveWindow() && m_wnd->triggerMenuCommand(command, shortcutText) ) {
            COPYQ_LOG("Global shortcut command triggered as a menu command");
        } else {
            QVariantMap data;
            data.insert(mimeShortcut, shortcutText.toUtf8());
            m_wnd->action(data, command, QModelIndex());
        }
    }
#endif
}
