/* * Copyright (c) 2010-2020 Belledonne Communications SARL. * * This file is part of linphone-desktop * (see https://www.linphone.org). * * This program 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. * * This program 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 this program. If not, see . */ #include #include #include #include #include #include #include #include "app/App.hpp" #include "components/call/CallModel.hpp" #include "components/core/CoreManager.hpp" #include "utils/Utils.hpp" #include "Notifier.hpp" // ============================================================================= using namespace std; namespace { constexpr char NotificationsPath[] = "qrc:/ui/modules/Linphone/Notifications/"; // --------------------------------------------------------------------------- // Notifications QML properties/methods. // --------------------------------------------------------------------------- constexpr char NotificationShowMethodName[] = "open"; constexpr char NotificationPropertyData[] = "notificationData"; constexpr char NotificationPropertyX[] = "popupX"; constexpr char NotificationPropertyY[] = "popupY"; constexpr char NotificationPropertyWindow[] = "__internalWindow"; constexpr char NotificationPropertyTimer[] = "__timer"; // --------------------------------------------------------------------------- // Arbitrary hardcoded values. // --------------------------------------------------------------------------- constexpr int NotificationSpacing = 10; constexpr int MaxNotificationsNumber = 5; constexpr int MaxTimeout = 30000; } // ============================================================================= template void setProperty (QObject &object, const char *property, const T &value) { if (!object.setProperty(property, QVariant(value))) { qWarning() << QStringLiteral("Unable to set property: `%1`.").arg(property); abort(); } } // ============================================================================= // Available notifications. // ============================================================================= const QHash Notifier::Notifications = { { Notifier::ReceivedMessage, { "NotificationReceivedMessage.qml", 10 } }, { Notifier::ReceivedFileMessage, { "NotificationReceivedFileMessage.qml", 10 } }, { Notifier::ReceivedCall, { "NotificationReceivedCall.qml", 30 } }, { Notifier::NewVersionAvailable, { "NotificationNewVersionAvailable.qml", 30 } }, { Notifier::SnapshotWasTaken, { "NotificationSnapshotWasTaken.qml", 10 } }, { Notifier::RecordingCompleted, { "NotificationRecordingCompleted.qml", 10 } } }; // ----------------------------------------------------------------------------- Notifier::Notifier (QObject *parent) : QObject(parent) { const int nComponents = Notifications.size(); mComponents = new QQmlComponent *[nComponents]; QQmlEngine *engine = App::getInstance()->getEngine(); for (const auto &key : Notifications.keys()) { QQmlComponent *component = new QQmlComponent(engine, QUrl(NotificationsPath + Notifier::Notifications[key].filename)); if (Q_UNLIKELY(component->isError())) { qWarning() << QStringLiteral("Errors found in `Notification` component %1:").arg(key) << component->errors(); abort(); } mComponents[key] = component; } mMutex = new QMutex(); } Notifier::~Notifier () { delete mMutex; const int nComponents = Notifications.size(); for (int i = 0; i < nComponents; ++i) delete mComponents[i]; delete[] mComponents; } // ----------------------------------------------------------------------------- QObject *Notifier::createNotification (Notifier::NotificationType type, QVariantMap data) { QQuickItem *wrapperItem = nullptr; mMutex->lock(); Q_ASSERT(mInstancesNumber <= MaxNotificationsNumber); if (mInstancesNumber == MaxNotificationsNumber) { // Check existing instances. qWarning() << QStringLiteral("Unable to create another notification."); mMutex->unlock(); return nullptr; } QList allScreens = QGuiApplication::screens(); if(allScreens.size() > 0){ // Ensure to have a screen to avoid errors QQuickItem * previousWrapper = nullptr; ++mInstancesNumber; for(int i = 0 ; i < allScreens.size() ; ++i){ QQuickView *view = new QQuickView(App::getInstance()->getEngine(), nullptr); // Use QQuickView to create a visual root object that is independant from current application Window QScreen *screen = allScreens[i]; QObject::connect(view, &QQuickView::statusChanged, [allScreens](QQuickView::Status status){ // Debug handler : show screens descriptions on Error if( status == QQuickView::Error){ QScreen * primaryScreen = QGuiApplication::primaryScreen(); qInfo() << "Primary screen : " << primaryScreen->geometry() << primaryScreen->availableGeometry() << primaryScreen->virtualGeometry() << primaryScreen->availableVirtualGeometry(); for(int i = 0 ; i < allScreens.size() ; ++i){ QScreen *screen = allScreens[i]; qInfo() << QString("Screen [")+QString::number(i)+"] (hdpi, Geometry, Available, Virtual, AvailableGeometry) :" << screen->devicePixelRatio() << screen->geometry() << screen->availableGeometry() << screen->virtualGeometry() << screen->availableVirtualGeometry(); } } }); view->setScreen(screen); // Bind the visual root object to the screen view->setProperty("flags", QVariant(Qt::BypassWindowManagerHint | Qt::WindowStaysOnBottomHint | Qt::CustomizeWindowHint | Qt::X11BypassWindowManagerHint)); // Set the visual ghost window view->setSource(QString(NotificationsPath)+Notifier::Notifications[type].filename); QQuickWindow *subWindow = view->findChild("__internalWindow"); QObject::connect(subWindow, &QObject::destroyed, view, &QObject::deleteLater); // When destroying window, detroy visual root object too int * screenHeightOffset = &mScreenHeightOffset[screen->name()]; // Access optimization QRect availableGeometry = screen->availableGeometry(); int heightOffset = availableGeometry.y() + (availableGeometry.height() - subWindow->height());//*screen->devicePixelRatio(); when using manual scaler subWindow->setX(availableGeometry.x()+ (availableGeometry.width()-subWindow->property("width").toInt()));//*screen->devicePixelRatio()); when using manual scaler subWindow->setY(heightOffset-(*screenHeightOffset % heightOffset)); *screenHeightOffset = (subWindow->height() + *screenHeightOffset) + NotificationSpacing; if (*screenHeightOffset - heightOffset + availableGeometry.y() >= 0) *screenHeightOffset = 0; // if(primaryScreen != screen){ //Useful when doing manual scaling jobs. Need to implement scaler in GUI objects // //subwindow->setProperty("xScale", (double)screen->availableVirtualGeometry().width()/availableGeometry.width() ); // //subwindow->setProperty("yScale", (double)screen->availableVirtualGeometry().height()/availableGeometry.height()); // } wrapperItem = view->findChild("__internalWrapper"); ::setProperty(*wrapperItem, NotificationPropertyData,data); view->setGeometry(subWindow->geometry()); // Ensure to have sufficient space to both let painter do job without error, and stay behind popup if(previousWrapper!=nullptr){ // Link objects in order to propagate events without having to store them QObject::connect(wrapperItem, SIGNAL(isOpened()), previousWrapper,SLOT(open())); QObject::connect(wrapperItem, SIGNAL(isClosed()), previousWrapper,SLOT(close())); QObject::connect(wrapperItem, &QObject::destroyed, previousWrapper, &QObject::deleteLater); } previousWrapper = wrapperItem; // The last one is used as a point of start when deleting and openning view->show(); } qInfo() << QStringLiteral("Create notifications:") << wrapperItem; } mMutex->unlock(); return wrapperItem; } // ----------------------------------------------------------------------------- void Notifier::showNotification (QObject *notification, int timeout) { // Display notification. QMetaObject::invokeMethod(notification, NotificationShowMethodName, Qt::DirectConnection); QTimer *timer = new QTimer(notification); timer->setInterval(timeout > MaxTimeout ? MaxTimeout : timeout); timer->setSingleShot(true); notification->setProperty(NotificationPropertyTimer, QVariant::fromValue(timer)); // Destroy it after timeout. QObject::connect(timer, &QTimer::timeout, this, [this, notification]() { deleteNotification(QVariant::fromValue(notification)); }); // Called explicitly (by a click on notification for example) QObject::connect(notification, SIGNAL(deleteNotification(QVariant)), this, SLOT(deleteNotification(QVariant))); timer->start(); } // ----------------------------------------------------------------------------- void Notifier::deleteNotification (QVariant notification) { mMutex->lock(); QObject *instance = notification.value(); // Notification marked destroyed. if (instance->property("__valid").isValid()) { mMutex->unlock(); return; } qInfo() << QStringLiteral("Delete notification:") << instance; instance->setProperty("__valid", true); instance->property(NotificationPropertyTimer).value()->stop(); mInstancesNumber--; Q_ASSERT(mInstancesNumber >= 0); if (mInstancesNumber == 0) mScreenHeightOffset.clear(); mMutex->unlock(); instance->deleteLater(); } // ============================================================================= #define CREATE_NOTIFICATION(TYPE, DATA) \ QObject * notification = createNotification(TYPE, DATA); \ if (!notification) \ return; \ const int timeout = Notifications[TYPE].timeout * 1000; \ showNotification(notification, timeout); // ----------------------------------------------------------------------------- // Notification functions. // ----------------------------------------------------------------------------- void Notifier::notifyReceivedMessage (const shared_ptr &message) { QVariantMap map; map["message"] = message->getFileTransferInformation() ? tr("newFileMessage") : Utils::coreStringToAppString(message->getText()); shared_ptr chatRoom(message->getChatRoom()); map["peerAddress"] = Utils::coreStringToAppString(chatRoom->getPeerAddress()->asString()); map["localAddress"] = Utils::coreStringToAppString(chatRoom->getLocalAddress()->asString()); map["window"].setValue(App::getInstance()->getMainWindow()); CREATE_NOTIFICATION(Notifier::ReceivedMessage, map) } void Notifier::notifyReceivedFileMessage (const shared_ptr &message) { QVariantMap map; map["fileUri"] = Utils::coreStringToAppString(message->getFileTransferInformation()->getFilePath()); map["fileSize"] = quint64(message->getFileTransferInformation()->getSize() +message->getFileTransferInformation()->getFileSize()); CREATE_NOTIFICATION(Notifier::ReceivedFileMessage, map) } void Notifier::notifyReceivedCall (const shared_ptr &call) { CallModel *callModel = &call->getData("call-model"); QVariantMap map; map["call"].setValue(callModel); CREATE_NOTIFICATION(Notifier::ReceivedCall, map) QObject::connect(callModel, &CallModel::statusChanged, notification, [this, notification](CallModel::CallStatus status) { if (status == CallModel::CallStatusEnded || status == CallModel::CallStatusConnected) deleteNotification(QVariant::fromValue(notification)); }); } void Notifier::notifyNewVersionAvailable (const QString &version, const QString &url) { QVariantMap map; map["message"] = tr("newVersionAvailable").arg(version); map["url"] = url; CREATE_NOTIFICATION(Notifier::NewVersionAvailable, map) } void Notifier::notifySnapshotWasTaken (const QString &filePath) { QVariantMap map; map["filePath"] = filePath; CREATE_NOTIFICATION(Notifier::SnapshotWasTaken, map) } void Notifier::notifyRecordingCompleted (const QString &filePath) { QVariantMap map; map["filePath"] = filePath; CREATE_NOTIFICATION(Notifier::RecordingCompleted, map) } #undef SHOW_NOTIFICATION #undef CREATE_NOTIFICATION