From 5bde427e9c68f7f832e0b2bc7083dc8a5ae68dad Mon Sep 17 00:00:00 2001
From: Lukas Holecek <hluk@email.cz>
Date: Mon, 12 Apr 2021 19:39:49 +0200
Subject: [PATCH] Allow to use non-native notifications

Avoid using native notifications on unsupported Windows 7.
---
 shared/themes/notification.css                |  15 +
 src/CMakeLists.txt                            |   1 +
 src/app/clipboardserver.cpp                   |  29 ++
 src/common/appconfig.h                        |  25 ++
 src/gui/actionhandler.cpp                     |   1 -
 src/gui/configurationmanager.cpp              |   5 +
 src/gui/mainwindow.cpp                        |   1 -
 src/gui/notification.h                        |  70 +---
 src/gui/notificationbasic.cpp                 | 387 ++++++++++++++++++
 src/gui/notificationbasic.h                   |  25 ++
 src/gui/notificationdaemon.cpp                | 163 +++++++-
 src/gui/notificationdaemon.h                  |  38 ++
 .../notificationnative.cpp}                   | 119 ++++--
 .../notificationnative/notificationnative.h   |  28 ++
 src/gui/theme.cpp                             |   9 +
 src/gui/theme.h                               |   2 +
 src/notifications.cmake                       |  20 +-
 src/scriptable/scriptable.cpp                 |   6 +-
 src/scriptable/scriptableproxy.cpp            |   7 +-
 src/tests/tests.cpp                           |   2 +
 src/ui/configtabappearance.ui                 |  16 +
 src/ui/configtabnotifications.ui              | 198 ++++++++-
 utils/appveyor/after_build.sh                 |  20 +-
 utils/appveyor/before_build.sh                |   2 +-
 24 files changed, 1078 insertions(+), 111 deletions(-)
 create mode 100644 shared/themes/notification.css
 create mode 100644 src/gui/notificationbasic.cpp
 create mode 100644 src/gui/notificationbasic.h
 rename src/gui/{notification.cpp => notificationnative/notificationnative.cpp} (69%)
 create mode 100644 src/gui/notificationnative/notificationnative.h

diff --git a/shared/themes/notification.css b/shared/themes/notification.css
new file mode 100644
index 000000000..b8eff38fa
--- /dev/null
+++ b/shared/themes/notification.css
@@ -0,0 +1,15 @@
+#Notification, #Notification QWidget
+{
+    /* Resets notification opacity. It will be set in NotificationDaemon::setNotificationOpacity(). */
+    ;background: ${notification_bg + #000}
+}
+#Notification QWidget{
+    ;color: ${notification_fg}
+    ;${notification_font}
+}
+#Notification #NotificationTitle{
+    ;${scale=1.2}${notification_font}${scale=1}
+}
+#Notification #NotificationTip{
+    ;font-style: italic
+}
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index e841901f0..a708d74d8 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -4,6 +4,7 @@ file(GLOB copyq_SOURCES
     app/*.cpp
     common/*.cpp
     gui/*.cpp
+    gui/notification.h
     item/*.cpp
     scriptable/*.cpp
     scriptable/scriptableproxy.h
diff --git a/src/app/clipboardserver.cpp b/src/app/clipboardserver.cpp
index c94bade1d..5f9df01e5 100644
--- a/src/app/clipboardserver.cpp
+++ b/src/app/clipboardserver.cpp
@@ -700,6 +700,35 @@ void ClipboardServer::loadSettings()
         startMonitoring();
     }
 
+    m_sharedData->notifications->setNativeNotificationsEnabled(
+        appConfig.option<Config::native_notifications>() );
+    m_sharedData->notifications->setNotificationOpacity(
+        m_sharedData->theme.color("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");
diff --git a/src/common/appconfig.h b/src/common/appconfig.h
index 61f49c8c8..e81ad3f8f 100644
--- a/src/common/appconfig.h
+++ b/src/common/appconfig.h
@@ -68,16 +68,36 @@ struct item_popup_interval : Config<int> {
     static QString name() { return "item_popup_interval"; }
 };
 
+struct notification_position : Config<int> {
+    static QString name() { return "notification_position"; }
+    static Value defaultValue() { return 3; }
+};
+
 struct clipboard_notification_lines : Config<int> {
     static QString name() { return "clipboard_notification_lines"; }
     static Value value(Value v) { return qBound(0, v, 10000); }
 };
 
+struct notification_horizontal_offset : Config<int> {
+    static QString name() { return "notification_horizontal_offset"; }
+    static Value defaultValue() { return 10; }
+};
+
+struct notification_vertical_offset : Config<int> {
+    static QString name() { return "notification_vertical_offset"; }
+    static Value defaultValue() { return 10; }
+};
+
 struct notification_maximum_width : Config<int> {
     static QString name() { return "notification_maximum_width"; }
     static Value defaultValue() { return 300; }
 };
 
+struct notification_maximum_height : Config<int> {
+    static QString name() { return "notification_maximum_height"; }
+    static Value defaultValue() { return 100; }
+};
+
 struct edit_ctrl_return : Config<bool> {
     static QString name() { return "edit_ctrl_return"; }
     static Value defaultValue() { return true; }
@@ -412,6 +432,11 @@ struct style : Config<QString> {
     static QString name() { return "style"; }
 };
 
+struct native_notifications : Config<bool> {
+    static QString name() { return "native_notifications"; }
+    static Value defaultValue() { return true; }
+};
+
 } // namespace Config
 
 class AppConfig final
diff --git a/src/gui/actionhandler.cpp b/src/gui/actionhandler.cpp
index aea309cfc..b6898f5d3 100644
--- a/src/gui/actionhandler.cpp
+++ b/src/gui/actionhandler.cpp
@@ -188,5 +188,4 @@ void ActionHandler::showActionErrors(Action *action, const QString &message, ush
     notification->setTitle(title);
     notification->setMessage(msg, Qt::PlainText);
     notification->setIcon(icon);
-    notification->show();
 }
diff --git a/src/gui/configurationmanager.cpp b/src/gui/configurationmanager.cpp
index 9fb574e0e..4f148e0e8 100644
--- a/src/gui/configurationmanager.cpp
+++ b/src/gui/configurationmanager.cpp
@@ -259,8 +259,13 @@ void ConfigurationManager::initOptions()
     bind<Config::expire_tab>(m_tabHistory->spinBoxExpireTab);
     bind<Config::editor>(m_tabHistory->lineEditEditor);
     bind<Config::item_popup_interval>(m_tabNotifications->spinBoxNotificationPopupInterval);
+    bind<Config::notification_position>(m_tabNotifications->comboBoxNotificationPosition);
     bind<Config::clipboard_notification_lines>(m_tabNotifications->spinBoxClipboardNotificationLines);
+    bind<Config::notification_horizontal_offset>(m_tabNotifications->spinBoxNotificationHorizontalOffset);
+    bind<Config::notification_vertical_offset>(m_tabNotifications->spinBoxNotificationVerticalOffset);
     bind<Config::notification_maximum_width>(m_tabNotifications->spinBoxNotificationMaximumWidth);
+    bind<Config::notification_maximum_height>(m_tabNotifications->spinBoxNotificationMaximumHeight);
+    bind<Config::native_notifications>(m_tabNotifications->checkBoxUseNativeNotifications);
     bind<Config::edit_ctrl_return>(m_tabHistory->checkBoxEditCtrlReturn);
     bind<Config::show_simple_items>(m_tabHistory->checkBoxShowSimpleItems);
     bind<Config::number_search>(m_tabHistory->checkBoxNumberSearch);
diff --git a/src/gui/mainwindow.cpp b/src/gui/mainwindow.cpp
index 486a7c2a2..bf3d66bd8 100644
--- a/src/gui/mainwindow.cpp
+++ b/src/gui/mainwindow.cpp
@@ -2305,7 +2305,6 @@ void MainWindow::showError(const QString &msg)
     notification->setTitle( tr("CopyQ Error", "Notification error message title") );
     notification->setMessage(msg);
     notification->setIcon(IconTimesCircle);
-    notification->show();
 }
 
 Notification *MainWindow::createNotification(const QString &id)
diff --git a/src/gui/notification.h b/src/gui/notification.h
index f9acac82f..4c2f65226 100644
--- a/src/gui/notification.h
+++ b/src/gui/notification.h
@@ -17,75 +17,37 @@
     along with CopyQ.  If not, see <http://www.gnu.org/licenses/>.
 */
 
-#ifndef NOTIFICATION_H
-#define NOTIFICATION_H
+#pragma once
 
-#include "gui/notificationbutton.h"
-
-#include <QColor>
-#include <QTimer>
 #include <QObject>
-#include <QPixmap>
-#include <QPointer>
 
-#include <memory>
+#include "gui/notificationbutton.h"
 
 class KNotification;
 class QWidget;
 
-class Notification final : public QObject
+class Notification : public QObject
 {
     Q_OBJECT
 
 public:
-    static void initConfiguration();
-
-    explicit Notification(const QColor &iconColor, QObject *parent = nullptr);
-
-    ~Notification();
-
-    void setTitle(const QString &title);
-    void setMessage(const QString &msg, Qt::TextFormat format = Qt::PlainText);
-    void setPixmap(const QPixmap &pixmap);
-    void setIcon(const QString &icon);
-    void setIcon(ushort icon);
-    void setIconColor(const QColor &color);
-    void setInterval(int msec);
-    void setButtons(const NotificationButtons &buttons);
-
-    void show();
-
-    void close();
+    explicit Notification(QObject *parent) : QObject(parent) {}
+    virtual void setTitle(const QString &title) = 0;
+    virtual void setMessage(const QString &msg, Qt::TextFormat format = Qt::PlainText) = 0;
+    virtual void setPixmap(const QPixmap &pixmap) = 0;
+    virtual void setIcon(const QString &icon) = 0;
+    virtual void setIcon(ushort icon) = 0;
+    virtual void setInterval(int msec) = 0;
+    virtual void setOpacity(qreal opacity) = 0;
+    virtual void setButtons(const NotificationButtons &buttons) = 0;
+    virtual void adjust() = 0;
+    virtual QWidget *widget() = 0;
+    virtual void show() = 0;
+    virtual void close() = 0;
 
 signals:
     /** Emitted if notification needs to be closed. */
     void closeNotification(Notification *self);
 
     void buttonClicked(const NotificationButton &button);
-
-private:
-    void onButtonClicked(unsigned int id);
-    void onDestroyed();
-    void onClosed();
-    void onIgnored();
-    void onActivated();
-    void update();
-
-    void notificationLog(const char *message);
-
-    KNotification *dropNotification();
-
-    QPointer<KNotification> m_notification;
-    NotificationButtons m_buttons;
-
-    QColor m_iconColor;
-    QTimer m_timer;
-    int m_intervalMsec = -1;
-    QString m_title;
-    QString m_message;
-    QString m_icon;
-    ushort m_iconId;
-    QPixmap m_pixmap;
 };
-
-#endif // NOTIFICATION_H
diff --git a/src/gui/notificationbasic.cpp b/src/gui/notificationbasic.cpp
new file mode 100644
index 000000000..0079cc076
--- /dev/null
+++ b/src/gui/notificationbasic.cpp
@@ -0,0 +1,387 @@
+/*
+    Copyright (c) 2020, Lukas Holecek <hluk@email.cz>
+
+    This file is part of CopyQ.
+
+    CopyQ is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    CopyQ is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with CopyQ.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#include "gui/notificationbasic.h"
+#include "gui/notification.h"
+
+#include "common/common.h"
+#include "common/display.h"
+#include "common/textdata.h"
+#include "common/timer.h"
+#include "gui/iconfactory.h"
+#include "gui/icons.h"
+#include "gui/pixelratio.h"
+
+#include <QApplication>
+#include <QDialog>
+#include <QDialogButtonBox>
+#include <QGridLayout>
+#include <QHBoxLayout>
+#include <QIcon>
+#include <QLabel>
+#include <QMap>
+#include <QMouseEvent>
+#include <QPainter>
+#include <QPushButton>
+#include <QTextEdit>
+
+#include <memory>
+
+namespace {
+
+class NotificationButtonWidget final : public QPushButton
+{
+    Q_OBJECT
+
+public:
+    NotificationButtonWidget(const NotificationButton &button, QWidget *parent)
+        : QPushButton(button.name, parent)
+        , m_button(button)
+    {
+        connect( this, &NotificationButtonWidget::clicked,
+                 this, &NotificationButtonWidget::onClicked );
+    }
+
+signals:
+    void clickedButton(const NotificationButton &button);
+
+private:
+    void onClicked()
+    {
+        emit clickedButton(m_button);
+    }
+
+    NotificationButton m_button;
+};
+
+class NotificationBasic;
+
+class NotificationBasicWidget final : public QWidget
+{
+    Q_OBJECT
+
+public:
+    NotificationBasicWidget(NotificationBasic *parent);
+
+    void setTitle(const QString &title);
+    void setMessage(const QString &msg, Qt::TextFormat format = Qt::AutoText);
+    void setPixmap(const QPixmap &pixmap);
+    void setIcon(const QString &icon);
+    void setIcon(ushort icon);
+    void setInterval(int msec);
+    void setOpacity(qreal opacity);
+    void setButtons(const NotificationButtons &buttons);
+
+    void updateIcon();
+
+    void adjust();
+
+    void mousePressEvent(QMouseEvent *event) override;
+    void enterEvent(QEvent *event) override;
+    void leaveEvent(QEvent *event) override;
+    void paintEvent(QPaintEvent *event) override;
+    void showEvent(QShowEvent *event) override;
+    void hideEvent(QHideEvent *event) override;
+
+private:
+    void onTimeout();
+    void onButtonClicked(const NotificationButton &button);
+
+    NotificationBasic *m_parent;
+    QGridLayout *m_layout = nullptr;
+    QHBoxLayout *m_buttonLayout = nullptr;
+    QLabel *m_titleLabel = nullptr;
+    QLabel *m_iconLabel = nullptr;
+    QLabel *m_msgLabel = nullptr;
+    QTimer m_timer;
+    bool m_autoclose = false;
+    qreal m_opacity = 1.0;
+    QString m_icon;
+};
+
+class NotificationBasic final : public Notification
+{
+    Q_OBJECT
+
+    friend class NotificationBasicWidget;
+
+public:
+    NotificationBasic(QObject *parent)
+        : Notification(parent)
+        , m_widget(this)
+    {
+        m_widget.setObjectName("Notification");
+    }
+
+    void setTitle(const QString &title) override {
+        m_widget.setTitle(title);
+    }
+    void setMessage(const QString &msg, Qt::TextFormat format = Qt::AutoText) override {
+        m_widget.setMessage(msg, format);
+    }
+    void setPixmap(const QPixmap &pixmap) override {
+        m_widget.setPixmap(pixmap);
+    }
+    void setIcon(const QString &icon) override {
+        m_widget.setIcon(icon);
+    }
+    void setIcon(ushort icon) override {
+        m_widget.setIcon(icon);
+    }
+    void setInterval(int msec) override {
+        m_widget.setInterval(msec);
+    }
+    void setOpacity(qreal opacity) override {
+        m_widget.setOpacity(opacity);
+    }
+    void setButtons(const NotificationButtons &buttons) override {
+        m_widget.setButtons(buttons);
+    }
+
+    void adjust() override {
+        m_widget.updateIcon();
+        m_widget.adjust();
+    }
+
+    QWidget *widget() override {
+        m_widget.updateIcon();
+        m_widget.adjust();
+        return &m_widget;
+    }
+
+    void show() override {
+        m_widget.show();
+    }
+
+    void close() override {
+        m_widget.close();
+    }
+
+private:
+    NotificationBasicWidget m_widget;
+};
+
+} // namespace
+
+NotificationBasicWidget::NotificationBasicWidget(NotificationBasic *parent)
+    : m_parent(parent)
+{
+    m_layout = new QGridLayout(this);
+    m_layout->setMargin(8);
+
+    m_iconLabel = new QLabel(this);
+    m_iconLabel->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum);
+
+    m_msgLabel = new QLabel(this);
+    m_msgLabel->setAlignment(Qt::AlignTop | Qt::AlignAbsolute);
+
+    setTitle(QString());
+
+    setWindowFlags(Qt::ToolTip);
+    setWindowOpacity(m_opacity);
+    setAttribute(Qt::WA_ShowWithoutActivating);
+
+    initSingleShotTimer( &m_timer, 0, this, &NotificationBasicWidget::onTimeout );
+}
+
+void NotificationBasicWidget::setTitle(const QString &title)
+{
+    if ( !title.isEmpty() ) {
+        if (!m_titleLabel)
+            m_titleLabel = new QLabel(this);
+
+        m_titleLabel->setObjectName("NotificationTitle");
+        m_titleLabel->setTextFormat(Qt::PlainText);
+        m_titleLabel->setText(title);
+
+        m_layout->addWidget(m_iconLabel, 0, 0);
+        m_layout->addWidget(m_titleLabel, 0, 1, Qt::AlignCenter);
+        m_layout->addWidget(m_msgLabel, 1, 0, 1, 2);
+    } else {
+        if (m_titleLabel) {
+            m_titleLabel->deleteLater();
+            m_titleLabel = nullptr;
+        }
+
+        m_layout->addWidget(m_iconLabel, 0, 0, Qt::AlignTop);
+        m_layout->addWidget(m_msgLabel, 0, 1);
+    }
+}
+
+void NotificationBasicWidget::setMessage(const QString &msg, Qt::TextFormat format)
+{
+    m_msgLabel->setTextFormat(format);
+    m_msgLabel->setText(msg);
+    m_msgLabel->setVisible( !msg.isEmpty() );
+}
+
+void NotificationBasicWidget::setPixmap(const QPixmap &pixmap)
+{
+    m_msgLabel->setPixmap(pixmap);
+}
+
+void NotificationBasicWidget::setIcon(const QString &icon)
+{
+    m_icon = icon;
+}
+
+void NotificationBasicWidget::setIcon(ushort icon)
+{
+    m_icon = QString(QChar(icon));
+}
+
+void NotificationBasicWidget::setInterval(int msec)
+{
+    if (msec >= 0) {
+        m_autoclose = true;
+        m_timer.setInterval(msec);
+        if (isVisible())
+            m_timer.start();
+    } else {
+        m_autoclose = false;
+    }
+}
+
+void NotificationBasicWidget::setOpacity(qreal opacity)
+{
+    m_opacity = opacity;
+    setWindowOpacity(m_opacity);
+}
+
+void NotificationBasicWidget::setButtons(const NotificationButtons &buttons)
+{
+    for (const auto &buttonWidget : findChildren<NotificationButtonWidget*>())
+        buttonWidget->deleteLater();
+
+    if ( !buttons.isEmpty() ) {
+        if (!m_buttonLayout)
+            m_buttonLayout = new QHBoxLayout();
+
+        m_buttonLayout->addStretch();
+        m_layout->addLayout(m_buttonLayout, 2, 0, 1, 2);
+
+        for (const auto &button : buttons) {
+            const auto buttonWidget = new NotificationButtonWidget(button, this);
+            connect( buttonWidget, &NotificationButtonWidget::clickedButton,
+                     this, &NotificationBasicWidget::onButtonClicked );
+            m_buttonLayout->addWidget(buttonWidget);
+        }
+    } else if (m_buttonLayout) {
+        m_buttonLayout->deleteLater();
+        m_buttonLayout = nullptr;
+    }
+}
+
+void NotificationBasicWidget::updateIcon()
+{
+    const QColor color = getDefaultIconColor(*this);
+    const auto height = static_cast<int>( m_msgLabel->fontMetrics().lineSpacing() * 1.2 );
+    const auto iconId = toIconId(m_icon);
+
+    const auto ratio = pixelRatio(this);
+
+    auto pixmap = iconId == 0
+            ? QPixmap(m_icon)
+            : createPixmap(iconId, color, static_cast<int>(height * ratio));
+
+    pixmap.setDevicePixelRatio(ratio);
+
+    m_iconLabel->setPixmap(pixmap);
+    m_iconLabel->resize(pixmap.size());
+}
+
+void NotificationBasicWidget::adjust()
+{
+    m_msgLabel->setMaximumSize(maximumSize());
+    if ( !m_msgLabel->isVisible() && m_msgLabel->sizeHint().width() > maximumWidth() ) {
+        m_msgLabel->setWordWrap(true);
+        m_msgLabel->adjustSize();
+    }
+    adjustSize();
+}
+
+void NotificationBasicWidget::mousePressEvent(QMouseEvent *)
+{
+    m_timer.stop();
+
+    emit m_parent->closeNotification(m_parent);
+}
+
+void NotificationBasicWidget::enterEvent(QEvent *event)
+{
+    setWindowOpacity(1.0);
+    m_timer.stop();
+    QWidget::enterEvent(event);
+}
+
+void NotificationBasicWidget::leaveEvent(QEvent *event)
+{
+    setWindowOpacity(m_opacity);
+    m_timer.start();
+    QWidget::leaveEvent(event);
+}
+
+void NotificationBasicWidget::paintEvent(QPaintEvent *event)
+{
+    QWidget::paintEvent(event);
+
+    QPainter p(this);
+
+    // black outer border
+    p.setPen(Qt::black);
+    p.drawRect(rect().adjusted(0, 0, -1, -1));
+
+    // light inner border
+    p.setPen( palette().color(QPalette::Window).lighter(300) );
+    p.drawRect(rect().adjusted(1, 1, -2, -2));
+}
+
+void NotificationBasicWidget::showEvent(QShowEvent *event)
+{
+    m_timer.start();
+
+    // QTBUG-33078: Window opacity must be set after show event.
+    setWindowOpacity(m_opacity);
+    QWidget::showEvent(event);
+}
+
+void NotificationBasicWidget::hideEvent(QHideEvent *event)
+{
+    QWidget::hideEvent(event);
+    emit m_parent->closeNotification(m_parent);
+}
+
+void NotificationBasicWidget::onTimeout()
+{
+    if (m_autoclose)
+        emit m_parent->closeNotification(m_parent);
+}
+
+void NotificationBasicWidget::onButtonClicked(const NotificationButton &button)
+{
+    emit m_parent->buttonClicked(button);
+    emit m_parent->closeNotification(m_parent);
+}
+
+Notification *createNotificationBasic(QObject *parent)
+{
+    return new NotificationBasic(parent);
+}
+
+#include "notificationbasic.moc"
diff --git a/src/gui/notificationbasic.h b/src/gui/notificationbasic.h
new file mode 100644
index 000000000..ef63f3c18
--- /dev/null
+++ b/src/gui/notificationbasic.h
@@ -0,0 +1,25 @@
+/*
+    Copyright (c) 2020, Lukas Holecek <hluk@email.cz>
+
+    This file is part of CopyQ.
+
+    CopyQ is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    CopyQ is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with CopyQ.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#pragma once
+
+class Notification;
+class QObject;
+
+Notification *createNotificationBasic(QObject *parent);
diff --git a/src/gui/notificationdaemon.cpp b/src/gui/notificationdaemon.cpp
index 52de0a2fd..e06d463df 100644
--- a/src/gui/notificationdaemon.cpp
+++ b/src/gui/notificationdaemon.cpp
@@ -20,20 +20,98 @@
 #include "gui/notificationdaemon.h"
 
 #include "common/common.h"
+#include "common/display.h"
+#include "common/timer.h"
 #include "gui/notification.h"
+#include "gui/notificationbasic.h"
 #include "gui/screen.h"
 
+#ifdef WITH_NATIVE_NOTIFICATIONS
+#   include "gui/notificationnative/notificationnative.h"
+#   include <QSysInfo>
+#endif
+
 #include <QApplication>
 #include <QPixmap>
+#include <QPoint>
+#include <QVariant>
+#include <QWidget>
+
+namespace {
+
+const int notificationMarginPoints = 10;
+
+int notificationMargin()
+{
+    return pointsToPixels(notificationMarginPoints);
+}
+
+bool hasNativeNotifications()
+{
+#ifdef Q_OS_WIN
+    static const bool supportsNotifications =
+        !QSysInfo::productVersion().startsWith(QLatin1String("7"));
+    return supportsNotifications;
+#else
+    return true;
+#endif
+}
+
+} // namespace
 
 NotificationDaemon::NotificationDaemon(QObject *parent)
     : QObject(parent)
+    , m_position(BottomRight)
+    , m_notifications()
+    , m_opacity(1.0)
+    , m_horizontalOffsetPoints(0)
+    , m_verticalOffsetPoints(0)
+    , m_maximumWidthPoints(300)
+    , m_maximumHeightPoints(100)
 {
-    Notification::initConfiguration();
+#ifdef WITH_NATIVE_NOTIFICATIONS
+    if (hasNativeNotifications())
+        initNotificationNativeConfiguration();
+#endif
+
+    initSingleShotTimer( &m_timerUpdate, 100, this, &NotificationDaemon::doUpdateNotificationWidgets );
 }
 
 NotificationDaemon::~NotificationDaemon() = default;
 
+void NotificationDaemon::setPosition(NotificationDaemon::Position position)
+{
+    m_position = position;
+}
+
+void NotificationDaemon::setOffset(int horizontalPoints, int verticalPoints)
+{
+    m_horizontalOffsetPoints = horizontalPoints;
+    m_verticalOffsetPoints = verticalPoints;
+}
+
+void NotificationDaemon::setMaximumSize(int maximumWidthPoints, int maximumHeightPoints)
+{
+    m_maximumWidthPoints = maximumWidthPoints;
+    m_maximumHeightPoints = maximumHeightPoints;
+}
+
+void NotificationDaemon::updateNotificationWidgets()
+{
+    if ( !m_timerUpdate.isActive() )
+        m_timerUpdate.start();
+}
+
+void NotificationDaemon::setNotificationOpacity(qreal opacity)
+{
+    m_opacity = opacity;
+}
+
+void NotificationDaemon::setNotificationStyleSheet(const QString &styleSheet)
+{
+    m_styleSheet = styleSheet;
+}
+
 void NotificationDaemon::setIconColor(const QColor &color)
 {
     m_iconColor = color;
@@ -55,9 +133,68 @@ void NotificationDaemon::onNotificationClose(Notification *notification)
         }
     }
 
+    if (notification->widget() != nullptr)
+        updateNotificationWidgets();
+
     notification->deleteLater();
 }
 
+void NotificationDaemon::doUpdateNotificationWidgets()
+{
+    const QPoint cursor = QCursor::pos();
+
+    // Postpone update if mouse cursor is over a notification.
+    for (auto &notificationData : m_notifications) {
+        auto notification = notificationData.notification;
+        QWidget *w = notification->widget();
+        if ( w != nullptr && w->isVisible() && w->geometry().contains(cursor) ) {
+            m_timerUpdate.start();
+            return;
+        }
+    }
+
+    const QRect screen = screenGeometry(0);
+
+    int y = (m_position & Top) ? offsetY() : screen.bottom() - offsetY();
+
+    for (auto &notificationData : m_notifications) {
+        auto notification = notificationData.notification;
+        QWidget *w = notification->widget();
+        if (w == nullptr)
+            continue;
+
+        notification->setOpacity(m_opacity);
+        w->setStyleSheet(m_styleSheet);
+        w->setMaximumSize( pointsToPixels(m_maximumWidthPoints), pointsToPixels(m_maximumHeightPoints) );
+        notification->adjust();
+
+        // Avoid positioning a notification under mouse cursor.
+        QRect rect = w->geometry();
+        do {
+            int x;
+            if (m_position & Left)
+                x = offsetX();
+            else if (m_position & Right)
+                x = screen.right() - rect.width() - offsetX();
+            else
+                x = screen.right() / 2 - rect.width() / 2;
+
+            if (m_position & Bottom)
+                y -= rect.height();
+
+            if (m_position & Top)
+                y += rect.height() + notificationMargin();
+            else
+                y -= notificationMargin();
+
+            rect.moveTo(x, y);
+        } while( rect.contains(cursor) );
+
+        w->move(rect.topLeft());
+        notification->show();
+    }
+}
+
 Notification *NotificationDaemon::findNotification(const QString &id)
 {
     for (auto &notificationData : m_notifications) {
@@ -75,7 +212,16 @@ Notification *NotificationDaemon::createNotification(const QString &id)
         notification = findNotification(id);
 
     if (notification == nullptr) {
-        notification = new Notification(m_iconColor, this);
+#ifdef WITH_NATIVE_NOTIFICATIONS
+        if (m_nativeNotificationsEnabled && hasNativeNotifications()) {
+            notification = createNotificationNative(m_iconColor, this);
+            QTimer::singleShot(0, notification, &Notification::show);
+        } else {
+            notification = createNotificationBasic(this);
+        }
+#else
+        notification = createNotificationBasic(this);
+#endif
 
         connect(this, &QObject::destroyed, notification, &QObject::deleteLater);
         connect( notification, &Notification::closeNotification,
@@ -86,5 +232,18 @@ Notification *NotificationDaemon::createNotification(const QString &id)
         m_notifications.append(NotificationData{id, notification});
     }
 
+    if (notification->widget() != nullptr)
+        updateNotificationWidgets();
+
     return notification;
 }
+
+int NotificationDaemon::offsetX() const
+{
+    return pointsToPixels(m_horizontalOffsetPoints);
+}
+
+int NotificationDaemon::offsetY() const
+{
+    return pointsToPixels(m_verticalOffsetPoints);
+}
diff --git a/src/gui/notificationdaemon.h b/src/gui/notificationdaemon.h
index d8215c5f5..0ca7b49cc 100644
--- a/src/gui/notificationdaemon.h
+++ b/src/gui/notificationdaemon.h
@@ -37,6 +37,17 @@ class NotificationDaemon final : public QObject
 {
     Q_OBJECT
 public:
+    enum Position {
+        Top = 0x2,
+        Bottom = 0x4,
+        Right = 0x8,
+        Left = 0x10,
+        TopRight = Top | Right,
+        BottomRight = Bottom | Right,
+        BottomLeft = Bottom | Left,
+        TopLeft = Top | Left
+    };
+
     explicit NotificationDaemon(QObject *parent = nullptr);
 
     ~NotificationDaemon();
@@ -44,8 +55,22 @@ public:
     Notification *createNotification(const QString &id = QString());
     Notification *findNotification(const QString &id);
 
+    void setPosition(Position position);
+
+    void setOffset(int horizontalPoints, int verticalPoints);
+
+    void setMaximumSize(int maximumWidthPoints, int maximumHeightPoints);
+
+    void updateNotificationWidgets();
+
+    void setNotificationOpacity(qreal opacity);
+
+    void setNotificationStyleSheet(const QString &styleSheet);
+
     void setIconColor(const QColor &color);
 
+    void setNativeNotificationsEnabled(bool enable) { m_nativeNotificationsEnabled = enable; }
+
     void removeNotification(const QString &id);
 
 signals:
@@ -53,6 +78,7 @@ signals:
 
 private:
     void onNotificationClose(Notification *notification);
+    void doUpdateNotificationWidgets();
 
     struct NotificationData {
         QString id;
@@ -61,8 +87,20 @@ private:
 
     Notification *findNotification(Notification *notification);
 
+    int offsetX() const;
+    int offsetY() const;
+
+    Position m_position;
     QList<NotificationData> m_notifications;
     QColor m_iconColor;
+    qreal m_opacity;
+    int m_horizontalOffsetPoints;
+    int m_verticalOffsetPoints;
+    int m_maximumWidthPoints;
+    int m_maximumHeightPoints;
+    QString m_styleSheet;
+    QTimer m_timerUpdate;
+    bool m_nativeNotificationsEnabled = true;
 };
 
 #endif // NOTIFICATIONDAEMON_H
diff --git a/src/gui/notification.cpp b/src/gui/notificationnative/notificationnative.cpp
similarity index 69%
rename from src/gui/notification.cpp
rename to src/gui/notificationnative/notificationnative.cpp
index f9602fce2..5a48b46d5 100644
--- a/src/gui/notification.cpp
+++ b/src/gui/notificationnative/notificationnative.cpp
@@ -17,12 +17,13 @@
     along with CopyQ.  If not, see <http://www.gnu.org/licenses/>.
 */
 
-#include "gui/notification.h"
+#include "notificationnative.h"
 
 #include "common/log.h"
 #include "common/timer.h"
 #include "gui/iconfactory.h"
 #include "gui/icons.h"
+#include "gui/notification.h"
 
 #include <KNotification>
 #include <QApplication>
@@ -36,6 +37,7 @@
 #include <QMap>
 #include <QMouseEvent>
 #include <QPainter>
+#include <QPointer>
 #include <QPushButton>
 #include <QStandardPaths>
 #include <QTextEdit>
@@ -64,9 +66,57 @@ QPixmap defaultIcon()
     return pixmap;
 }
 
+class NotificationNative final : public Notification
+{
+    Q_OBJECT
+
+public:
+    explicit NotificationNative(const QColor &iconColor, QObject *parent = nullptr);
+
+    ~NotificationNative();
+
+    void setTitle(const QString &title) override;
+    void setMessage(const QString &msg, Qt::TextFormat format = Qt::PlainText) override;
+    void setPixmap(const QPixmap &pixmap) override;
+    void setIcon(const QString &icon) override;
+    void setIcon(ushort icon) override;
+    void setInterval(int msec) override;
+    void setOpacity(qreal) override {}
+    void setButtons(const NotificationButtons &buttons) override;
+    void adjust() override {}
+    QWidget *widget() override { return nullptr; }
+    void show() override;
+    void close() override;
+
+private:
+    void onButtonClicked(unsigned int id);
+    void onDestroyed();
+    void onClosed();
+    void onIgnored();
+    void onActivated();
+    void update();
+
+    void notificationLog(const char *message);
+
+    KNotification *dropNotification();
+
+    QPointer<KNotification> m_notification;
+    NotificationButtons m_buttons;
+
+    QColor m_iconColor;
+    QTimer m_timer;
+    int m_intervalMsec = -1;
+    QString m_title;
+    QString m_message;
+    QString m_icon;
+    ushort m_iconId;
+    QPixmap m_pixmap;
+    bool m_closed = false;
+};
+
 } // namespace
 
-void Notification::initConfiguration()
+void initNotificationNativeConfiguration()
 {
     const QString dataDir = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation);
     QDir dir(dataDir);
@@ -98,14 +148,14 @@ void Notification::initConfiguration()
     configFile.close();
 }
 
-Notification::Notification(const QColor &iconColor, QObject *parent)
-    : QObject(parent)
+NotificationNative::NotificationNative(const QColor &iconColor, QObject *parent)
+    : Notification(parent)
     , m_iconColor(iconColor)
 {
-    initSingleShotTimer( &m_timer, 0, this, &Notification::close );
+    initSingleShotTimer( &m_timer, 0, this, &NotificationNative::close );
 }
 
-Notification::~Notification()
+NotificationNative::~NotificationNative()
 {
     auto notification = dropNotification();
     if (notification) {
@@ -114,12 +164,12 @@ Notification::~Notification()
     }
 }
 
-void Notification::setTitle(const QString &title)
+void NotificationNative::setTitle(const QString &title)
 {
     m_title = title;
 }
 
-void Notification::setMessage(const QString &msg, Qt::TextFormat format)
+void NotificationNative::setMessage(const QString &msg, Qt::TextFormat format)
 {
     if (format == Qt::PlainText)
         m_message = msg.toHtmlEscaped();
@@ -127,14 +177,14 @@ void Notification::setMessage(const QString &msg, Qt::TextFormat format)
         m_message = msg;
 }
 
-void Notification::setPixmap(const QPixmap &pixmap)
+void NotificationNative::setPixmap(const QPixmap &pixmap)
 {
     m_icon.clear();
     m_iconId = 0;
     m_pixmap = pixmap;
 }
 
-void Notification::setIcon(const QString &icon)
+void NotificationNative::setIcon(const QString &icon)
 {
     m_iconId = toIconId(icon);
     if (m_iconId == 0)
@@ -144,25 +194,28 @@ void Notification::setIcon(const QString &icon)
     m_pixmap = QPixmap();
 }
 
-void Notification::setIcon(ushort icon)
+void NotificationNative::setIcon(ushort icon)
 {
     m_icon.clear();
     m_iconId = icon;
     m_pixmap = QPixmap();
 }
 
-void Notification::setInterval(int msec)
+void NotificationNative::setInterval(int msec)
 {
     m_intervalMsec = msec;
 }
 
-void Notification::setButtons(const NotificationButtons &buttons)
+void NotificationNative::setButtons(const NotificationButtons &buttons)
 {
     m_buttons = buttons;
 }
 
-void Notification::show()
+void NotificationNative::show()
 {
+    if (m_closed)
+        return;
+
     notificationLog("Update");
 
     if (m_notification) {
@@ -178,20 +231,20 @@ void Notification::show()
     m_notification->setComponentName(componentName);
 
     connect( m_notification.data(), static_cast<void (KNotification::*)(unsigned int)>(&KNotification::activated),
-             this, &Notification::onButtonClicked );
+             this, &NotificationNative::onButtonClicked );
     connect( m_notification.data(), &KNotification::closed,
-             this, &Notification::onClosed );
+             this, &NotificationNative::onClosed );
     connect( m_notification.data(), &KNotification::ignored,
-             this, &Notification::onIgnored );
+             this, &NotificationNative::onIgnored );
 #if KNOTIFICATIONS_VERSION < QT_VERSION_CHECK(5,67,0)
     connect( m_notification.data(), static_cast<void (KNotification::*)()>(&KNotification::activated),
-             this, &Notification::onActivated );
+             this, &NotificationNative::onActivated );
 #else
     connect( m_notification.data(), &KNotification::defaultActivated,
-             this, &Notification::onActivated );
+             this, &NotificationNative::onActivated );
 #endif
     connect( m_notification.data(), &QObject::destroyed,
-             this, &Notification::onDestroyed );
+             this, &NotificationNative::onDestroyed );
 
     update();
     if (m_notification)
@@ -200,7 +253,7 @@ void Notification::show()
     notificationLog("Created");
 }
 
-void Notification::close()
+void NotificationNative::close()
 {
     notificationLog("Close");
 
@@ -213,7 +266,7 @@ void Notification::close()
     emit closeNotification(this);
 }
 
-void Notification::onButtonClicked(unsigned int id)
+void NotificationNative::onButtonClicked(unsigned int id)
 {
     notificationLog("onButtonClicked");
 
@@ -223,31 +276,31 @@ void Notification::onButtonClicked(unsigned int id)
     }
 }
 
-void Notification::onDestroyed()
+void NotificationNative::onDestroyed()
 {
     notificationLog("Destroyed");
     dropNotification();
 }
 
-void Notification::onClosed()
+void NotificationNative::onClosed()
 {
     notificationLog("onClosed");
     dropNotification();
 }
 
-void Notification::onIgnored()
+void NotificationNative::onIgnored()
 {
     notificationLog("onIgnored");
     dropNotification();
 }
 
-void Notification::onActivated()
+void NotificationNative::onActivated()
 {
     notificationLog("onActivated");
     dropNotification();
 }
 
-void Notification::update()
+void NotificationNative::update()
 {
     if (!m_notification)
         return;
@@ -298,7 +351,7 @@ void Notification::update()
     }
 }
 
-void Notification::notificationLog(const char *message)
+void NotificationNative::notificationLog(const char *message)
 {
     COPYQ_LOG_VERBOSE(
         QString("Notification [%1:%2]: %3")
@@ -307,8 +360,9 @@ void Notification::notificationLog(const char *message)
         .arg(message) );
 }
 
-KNotification *Notification::dropNotification()
+KNotification *NotificationNative::dropNotification()
 {
+    m_closed = true;
     if (!m_notification)
         return nullptr;
 
@@ -317,3 +371,10 @@ KNotification *Notification::dropNotification()
     notification->disconnect(this);
     return notification;
 }
+
+Notification *createNotificationNative(const QColor &iconColor, QObject *parent)
+{
+    return new NotificationNative(iconColor, parent);
+}
+
+#include "notificationnative.moc"
diff --git a/src/gui/notificationnative/notificationnative.h b/src/gui/notificationnative/notificationnative.h
new file mode 100644
index 000000000..eda2f9222
--- /dev/null
+++ b/src/gui/notificationnative/notificationnative.h
@@ -0,0 +1,28 @@
+/*
+    Copyright (c) 2020, Lukas Holecek <hluk@email.cz>
+
+    This file is part of CopyQ.
+
+    CopyQ is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    CopyQ is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with CopyQ.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#pragma once
+
+class Notification;
+class QColor;
+class QObject;
+
+void initNotificationNativeConfiguration();
+
+Notification *createNotificationNative(const QColor &iconColor, QObject *parent);
diff --git a/src/gui/theme.cpp b/src/gui/theme.cpp
index 740b695c9..d22853538 100644
--- a/src/gui/theme.cpp
+++ b/src/gui/theme.cpp
@@ -364,6 +364,12 @@ QString Theme::getToolTipStyleSheet() const
     return getStyleSheet(cssTemplate);
 }
 
+QString Theme::getNotificationStyleSheet() const
+{
+    const QString cssTemplate = value("css_template_notification").toString();
+    return getStyleSheet(cssTemplate);
+}
+
 Qt::ScrollBarPolicy Theme::scrollbarPolicy() const
 {
     return value("show_scrollbars").toBool()
@@ -405,6 +411,7 @@ void Theme::resetTheme()
     m_theme["find_fg"]   = Option("#000", "VALUE", ui ? ui->pushButtonColorFoundFg : nullptr);
     m_theme["notes_bg"]  = Option(defaultColorVarToolTipBase, "VALUE", ui ? ui->pushButtonColorNotesBg : nullptr);
     m_theme["notes_fg"]  = Option(defaultColorVarToolTipText, "VALUE", ui ? ui->pushButtonColorNotesFg : nullptr);
+    m_theme["notification_bg"]  = Option("#333", "VALUE", ui ? ui->pushButtonColorNotificationBg : nullptr);
     m_theme["notification_fg"]  = Option("#ddd", "VALUE", ui ? ui->pushButtonColorNotificationFg : nullptr);
 
     m_theme["font"]        = Option("", "VALUE", ui ? ui->pushButtonFont : nullptr);
@@ -412,6 +419,7 @@ void Theme::resetTheme()
     m_theme["find_font"]   = Option("", "VALUE", ui ? ui->pushButtonFoundFont : nullptr);
     m_theme["num_font"]    = Option("", "VALUE", ui ? ui->pushButtonNumberFont : nullptr);
     m_theme["notes_font"]  = Option("", "VALUE", ui ? ui->pushButtonNotesFont : nullptr);
+    m_theme["notification_font"]  = Option("", "VALUE", ui ? ui->pushButtonNotificationFont : nullptr);
     m_theme["show_number"] = Option(true, "checked", ui ? ui->checkBoxShowNumber : nullptr);
     m_theme["show_scrollbars"] = Option(true, "checked", ui ? ui->checkBoxScrollbars : nullptr);
 
@@ -527,6 +535,7 @@ void Theme::resetTheme()
 
     m_theme["css_template_items"] = Option("items");
     m_theme["css_template_main_window"] = Option("main_window");
+    m_theme["css_template_notification"] = Option("notification");
     m_theme["css_template_tooltip"] = Option("tooltip");
 }
 
diff --git a/src/gui/theme.h b/src/gui/theme.h
index 2fa5116a0..85293554f 100644
--- a/src/gui/theme.h
+++ b/src/gui/theme.h
@@ -79,6 +79,8 @@ public:
     /** Return stylesheet for tooltips. */
     QString getToolTipStyleSheet() const;
 
+    QString getNotificationStyleSheet() const;
+
     Qt::ScrollBarPolicy scrollbarPolicy() const;
 
     bool useSystemIcons() const;
diff --git a/src/notifications.cmake b/src/notifications.cmake
index 47909254b..f3422f15c 100644
--- a/src/notifications.cmake
+++ b/src/notifications.cmake
@@ -1,8 +1,18 @@
-set(KF5_MIN_VERSION "5.18.0")
+OPTION(WITH_NATIVE_NOTIFICATIONS "Build with native notification support" ON)
 
-find_package(ECM ${KF5_MIN_VERSION} REQUIRED NO_MODULE)
-list(APPEND CMAKE_MODULE_PATH ${ECM_MODULE_PATH})
+if (WITH_NATIVE_NOTIFICATIONS)
+    set(KF5_MIN_VERSION "5.18.0")
 
-find_package(KF5 ${KF5_MIN_VERSION} REQUIRED COMPONENTS Notifications)
+    find_package(ECM ${KF5_MIN_VERSION} REQUIRED NO_MODULE)
+    list(APPEND CMAKE_MODULE_PATH ${ECM_MODULE_PATH})
 
-list(APPEND copyq_LIBRARIES KF5::Notifications)
+    find_package(KF5 ${KF5_MIN_VERSION} REQUIRED COMPONENTS Notifications)
+
+    list(APPEND copyq_LIBRARIES KF5::Notifications)
+
+    list(APPEND copyq_SOURCES
+        gui/notificationnative/notificationnative.cpp
+        gui/notificationnative/notificationdaemonnative.cpp)
+
+    list(APPEND copyq_DEFINITIONS WITH_NATIVE_NOTIFICATIONS)
+endif()
diff --git a/src/scriptable/scriptable.cpp b/src/scriptable/scriptable.cpp
index 780315084..a7efaa97a 100644
--- a/src/scriptable/scriptable.cpp
+++ b/src/scriptable/scriptable.cpp
@@ -65,7 +65,9 @@
 #include <QThread>
 #include <QTimer>
 
-#include <knotifications_version.h>
+#ifdef WITH_NATIVE_NOTIFICATIONS
+#   include <knotifications_version.h>
+#endif
 
 Q_DECLARE_METATYPE(QByteArray*)
 Q_DECLARE_METATYPE(QFile*)
@@ -824,7 +826,9 @@ QJSValue Scriptable::version()
     m_skipArguments = 0;
     return tr("CopyQ Clipboard Manager") + " " COPYQ_VERSION "\n"
             + "Qt: " QT_VERSION_STR "\n"
+#ifdef WITH_NATIVE_NOTIFICATIONS
             + "KNotifications: " KNOTIFICATIONS_VERSION_STRING "\n"
+#endif
             + "Compiler: "
 #if defined(Q_CC_GNU)
             "GCC"
diff --git a/src/scriptable/scriptableproxy.cpp b/src/scriptable/scriptableproxy.cpp
index 7f77b00f4..52eb0ead5 100644
--- a/src/scriptable/scriptableproxy.cpp
+++ b/src/scriptable/scriptableproxy.cpp
@@ -716,7 +716,6 @@ public:
         notification->setMessage(popupMessage);
         notification->setIcon(IconKeyboard);
         notification->setInterval(2000);
-        notification->show();
 
         if ( keys.startsWith(":") ) {
             const auto text = keys.mid(1);
@@ -1265,7 +1264,6 @@ void ScriptableProxy::showMessage(const QString &title,
     notification->setIcon(icon);
     notification->setInterval(msec);
     notification->setButtons(buttons);
-    notification->show();
 }
 
 QVariantMap ScriptableProxy::nextItem(const QString &tabName, int where)
@@ -2124,7 +2122,9 @@ void ScriptableProxy::showDataNotification(const QVariantMap &data)
 
     const QStringList formats = data.keys();
     const int imageIndex = formats.indexOf(QRegularExpression("^image/.*"));
-    const QFont &font = qApp->font();
+    const QFont &font = notification->widget()
+        ? notification->widget()->font()
+        : qApp->font();
     const bool isHidden = data.contains(mimeHidden);
 
     QString title;
@@ -2161,7 +2161,6 @@ void ScriptableProxy::showDataNotification(const QVariantMap &data)
     }
 
     notification->setTitle(title);
-    notification->show();
 }
 
 bool ScriptableProxy::enableMenuItem(int actionId, int currentRun, int menuItemMatchCommandIndex, const QVariantMap &menuItem)
diff --git a/src/tests/tests.cpp b/src/tests/tests.cpp
index 13b253284..7cace81d5 100644
--- a/src/tests/tests.cpp
+++ b/src/tests/tests.cpp
@@ -189,6 +189,8 @@ bool testStderr(const QByteArray &stderrData, TestInterface::ReadStderrFlag flag
         plain("ERROR: QtCritical: QWindowsPipeWriter::write failed. (The pipe is being closed.)"),
         plain("ERROR: QtCritical: QWindowsPipeWriter: asynchronous write failed. (The pipe has been ended.)"),
 
+        plain("[kf.notifications] QtWarning: Received a response for an unknown notification."),
+
         regex("QtWarning: QTemporaryDir: Unable to remove .* most likely due to the presence of read-only files."),
 
         // Windows Qt 5.15.2
diff --git a/src/ui/configtabappearance.ui b/src/ui/configtabappearance.ui
index 7ece0900e..129bb6e0f 100644
--- a/src/ui/configtabappearance.ui
+++ b/src/ui/configtabappearance.ui
@@ -252,6 +252,13 @@
            </property>
           </widget>
          </item>
+         <item row="8" column="1">
+          <widget class="QPushButton" name="pushButtonColorNotificationBg">
+           <property name="text">
+            <string/>
+           </property>
+          </widget>
+         </item>
          <item row="8" column="2">
           <widget class="QPushButton" name="pushButtonColorNotificationFg">
            <property name="text">
@@ -259,6 +266,13 @@
            </property>
           </widget>
          </item>
+         <item row="8" column="3">
+          <widget class="QPushButton" name="pushButtonNotificationFont">
+           <property name="text">
+            <string/>
+           </property>
+          </widget>
+         </item>
         </layout>
        </item>
        <item>
@@ -402,7 +416,9 @@
   <tabstop>pushButtonNotesFont</tabstop>
   <tabstop>pushButtonColorNumberFg</tabstop>
   <tabstop>pushButtonNumberFont</tabstop>
+  <tabstop>pushButtonColorNotificationBg</tabstop>
   <tabstop>pushButtonColorNotificationFg</tabstop>
+  <tabstop>pushButtonNotificationFont</tabstop>
   <tabstop>checkBoxShowNumber</tabstop>
   <tabstop>checkBoxScrollbars</tabstop>
   <tabstop>checkBoxSystemIcons</tabstop>
diff --git a/src/ui/configtabnotifications.ui b/src/ui/configtabnotifications.ui
index 129df1268..7c9b6f223 100644
--- a/src/ui/configtabnotifications.ui
+++ b/src/ui/configtabnotifications.ui
@@ -59,7 +59,71 @@
         <property name="fieldGrowthPolicy">
          <enum>QFormLayout::AllNonFixedFieldsGrow</enum>
         </property>
-        <item row="0" column="0">
+        <item row="1" column="0">
+         <widget class="QLabel" name="label_8">
+          <property name="text">
+           <string>&amp;Notification position:</string>
+          </property>
+          <property name="buddy">
+           <cstring>comboBoxNotificationPosition</cstring>
+          </property>
+         </widget>
+        </item>
+        <item row="1" column="1">
+         <layout class="QHBoxLayout" name="horizontalLayout_10">
+          <item>
+           <widget class="QComboBox" name="comboBoxNotificationPosition">
+            <property name="toolTip">
+             <string>Position on screen for notifications</string>
+            </property>
+            <item>
+             <property name="text">
+              <string>Top</string>
+             </property>
+            </item>
+            <item>
+             <property name="text">
+              <string>Bottom</string>
+             </property>
+            </item>
+            <item>
+             <property name="text">
+              <string>Top Right</string>
+             </property>
+            </item>
+            <item>
+             <property name="text">
+              <string>Bottom Right</string>
+             </property>
+            </item>
+            <item>
+             <property name="text">
+              <string>Bottom Left</string>
+             </property>
+            </item>
+            <item>
+             <property name="text">
+              <string>Top Left</string>
+             </property>
+            </item>
+           </widget>
+          </item>
+          <item>
+           <spacer name="horizontalSpacer_10">
+            <property name="orientation">
+             <enum>Qt::Horizontal</enum>
+            </property>
+            <property name="sizeHint" stdset="0">
+             <size>
+              <width>40</width>
+              <height>20</height>
+             </size>
+            </property>
+           </spacer>
+          </item>
+         </layout>
+        </item>
+        <item row="2" column="0">
          <widget class="QLabel" name="label_25">
           <property name="text">
            <string>Int&amp;erval in seconds to display notifications:</string>
@@ -69,7 +133,7 @@
           </property>
          </widget>
         </item>
-        <item row="0" column="1">
+        <item row="2" column="1">
          <layout class="QHBoxLayout" name="horizontalLayout_9">
           <item>
            <widget class="QSpinBox" name="spinBoxNotificationPopupInterval">
@@ -103,7 +167,7 @@ Set to -1 to keep visible until clicked.</string>
           </item>
          </layout>
         </item>
-        <item row="1" column="0">
+        <item row="3" column="0">
          <widget class="QLabel" name="label_12">
           <property name="text">
            <string>Num&amp;ber of lines for clipboard notification:</string>
@@ -113,7 +177,7 @@ Set to -1 to keep visible until clicked.</string>
           </property>
          </widget>
         </item>
-        <item row="1" column="1">
+        <item row="3" column="1">
          <layout class="QHBoxLayout" name="horizontalLayout_11">
           <item>
            <widget class="QSpinBox" name="spinBoxClipboardNotificationLines">
@@ -142,6 +206,13 @@ Set to 0 to disable.</string>
           </item>
          </layout>
         </item>
+        <item row="0" column="0">
+         <widget class="QCheckBox" name="checkBoxUseNativeNotifications">
+          <property name="text">
+           <string>&amp;Use native notifications</string>
+          </property>
+         </widget>
+        </item>
        </layout>
       </item>
       <item>
@@ -151,6 +222,86 @@ Set to 0 to disable.</string>
         </property>
         <layout class="QFormLayout" name="formLayout_4">
          <item row="0" column="0">
+          <widget class="QLabel" name="label_7">
+           <property name="text">
+            <string>Hori&amp;zontal offset:</string>
+           </property>
+           <property name="buddy">
+            <cstring>spinBoxNotificationHorizontalOffset</cstring>
+           </property>
+          </widget>
+         </item>
+         <item row="0" column="1">
+          <layout class="QHBoxLayout" name="horizontalLayout_19">
+           <item>
+            <widget class="QSpinBox" name="spinBoxNotificationHorizontalOffset">
+             <property name="toolTip">
+              <string>Notification distance from left or right screen edge in screen points</string>
+             </property>
+             <property name="minimum">
+              <number>-99999</number>
+             </property>
+             <property name="maximum">
+              <number>99999</number>
+             </property>
+            </widget>
+           </item>
+           <item>
+            <spacer name="horizontalSpacer_19">
+             <property name="orientation">
+              <enum>Qt::Horizontal</enum>
+             </property>
+             <property name="sizeHint" stdset="0">
+              <size>
+               <width>40</width>
+               <height>20</height>
+              </size>
+             </property>
+            </spacer>
+           </item>
+          </layout>
+         </item>
+         <item row="1" column="0">
+          <widget class="QLabel" name="label_10">
+           <property name="text">
+            <string>&amp;Vertical offset:</string>
+           </property>
+           <property name="buddy">
+            <cstring>spinBoxNotificationVerticalOffset</cstring>
+           </property>
+          </widget>
+         </item>
+         <item row="1" column="1">
+          <layout class="QHBoxLayout" name="horizontalLayout_20">
+           <item>
+            <widget class="QSpinBox" name="spinBoxNotificationVerticalOffset">
+             <property name="toolTip">
+              <string>Notification distance from top or bottom screen edge in screen points</string>
+             </property>
+             <property name="minimum">
+              <number>-99999</number>
+             </property>
+             <property name="maximum">
+              <number>99999</number>
+             </property>
+            </widget>
+           </item>
+           <item>
+            <spacer name="horizontalSpacer_20">
+             <property name="orientation">
+              <enum>Qt::Horizontal</enum>
+             </property>
+             <property name="sizeHint" stdset="0">
+              <size>
+               <width>40</width>
+               <height>20</height>
+              </size>
+             </property>
+            </spacer>
+           </item>
+          </layout>
+         </item>
+         <item row="2" column="0">
           <widget class="QLabel" name="label_13">
            <property name="text">
             <string>Maximum &amp;width:</string>
@@ -160,7 +311,7 @@ Set to 0 to disable.</string>
            </property>
           </widget>
          </item>
-         <item row="0" column="1">
+         <item row="2" column="1">
           <layout class="QHBoxLayout" name="horizontalLayout_21">
            <item>
             <widget class="QSpinBox" name="spinBoxNotificationMaximumWidth">
@@ -187,6 +338,43 @@ Set to 0 to disable.</string>
            </item>
           </layout>
          </item>
+         <item row="3" column="0">
+          <widget class="QLabel" name="label_14">
+           <property name="text">
+            <string>Ma&amp;ximum height:</string>
+           </property>
+           <property name="buddy">
+            <cstring>spinBoxNotificationMaximumHeight</cstring>
+           </property>
+          </widget>
+         </item>
+         <item row="3" column="1">
+          <layout class="QHBoxLayout" name="horizontalLayout_22">
+           <item>
+            <widget class="QSpinBox" name="spinBoxNotificationMaximumHeight">
+             <property name="toolTip">
+              <string>Maximum height for notification in screen points</string>
+             </property>
+             <property name="maximum">
+              <number>9999</number>
+             </property>
+            </widget>
+           </item>
+           <item>
+            <spacer name="horizontalSpacer_22">
+             <property name="orientation">
+              <enum>Qt::Horizontal</enum>
+             </property>
+             <property name="sizeHint" stdset="0">
+              <size>
+               <width>40</width>
+               <height>20</height>
+              </size>
+             </property>
+            </spacer>
+           </item>
+          </layout>
+         </item>
         </layout>
        </widget>
       </item>
diff --git a/utils/appveyor/after_build.sh b/utils/appveyor/after_build.sh
index 6112e19e7..0667a263e 100644
--- a/utils/appveyor/after_build.sh
+++ b/utils/appveyor/after_build.sh
@@ -77,15 +77,19 @@ export COPYQ_TESTS_RERUN_FAILED=1
 "$Executable" write text/html "<p><b>Rich text</b> <i>item</i></p>"
 "$Executable" write image/png - < "$Source/src/images/icon_128x128.png"
 
-# FIXME: This does not show notifications.
+# FIXME: This does not show native notifications.
 #        Maybe a user interaction, like mouse move, is required.
-"$Executable" popup "Popup title" "Popup message..."
-"$Executable" notification \
-    .title "Notification title" \
-    .message "Notification message..." \
-    .button OK cmd data \
-    .button Close cmd data
-
+for native in "true" "false"; do
+    "$Executable" config native_notifications "$native"
+    "$Executable" popup "Popup title" "Popup message..."
+    "$Executable" notification \
+        .title "Notification title" \
+        .message "Notification message..." \
+        .button OK cmd data \
+        .button Close cmd data
+done
+
+"$Executable" sleep 1000
 "$Executable" screenshot > screenshot.png
 
 "$Executable" exit
diff --git a/utils/appveyor/before_build.sh b/utils/appveyor/before_build.sh
index 57873130f..d67b8a1c1 100644
--- a/utils/appveyor/before_build.sh
+++ b/utils/appveyor/before_build.sh
@@ -10,7 +10,7 @@ export PATH=$PATH:$INSTALL_PREFIX/bin
 
 "$build" snoretoast "v$SNORETOAST_VERSION" "$SNORETOAST_BASE_URL"
 "$build" extra-cmake-modules
-"$build" kconfig "" "" "-DKCONFIG_USE_GUI=OFF -DKCONFIG_USE_DBUS=OFF"
+"$build" kconfig "" "" "-DKCONFIG_USE_DBUS=OFF" "-DKCONFIG_USE_GUI=OFF"
 "$build" kwindowsystem
 "$build" kcoreaddons
 "$build" knotifications
-- 
GitLab