Advance Wayland and KDE package bring-up

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-04-14 10:51:06 +01:00
parent 51f3c21121
commit cf12defd28
15214 changed files with 20594243 additions and 269 deletions
@@ -0,0 +1,167 @@
add_library(KF6Notifications)
add_library(KF6::Notifications ALIAS KF6Notifications)
qt_extract_metatypes(KF6Notifications)
set_target_properties(KF6Notifications PROPERTIES
VERSION ${KNOTIFICATIONS_VERSION}
SOVERSION ${KNOTIFICATIONS_SOVERSION}
EXPORT_NAME Notifications
)
ecm_create_qm_loader(KF6Notifications knotifications6_qt)
target_sources(KF6Notifications PRIVATE
knotification.cpp
knotificationreplyaction.cpp
knotificationmanager.cpp
knotificationpermission.cpp
knotifyconfig.cpp
knotificationplugin.cpp
)
if (HAVE_DBUS)
target_sources(KF6Notifications PRIVATE
imageconverter.cpp #needed to marshal images for sending over dbus by NotifyByPopup
notifybypopup.cpp
notifybyportal.cpp
)
endif()
if (ANDROID)
add_subdirectory(android)
target_sources(KF6Notifications PRIVATE
notifybyandroid.cpp
knotifications.qrc
)
endif()
if (WITH_SNORETOAST)
target_sources(KF6Notifications PRIVATE notifybysnore.cpp)
endif ()
if (APPLE)
target_sources(KF6Notifications PRIVATE notifybymacosnotificationcenter.mm)
endif()
ecm_qt_declare_logging_category(KF6Notifications
HEADER debug_p.h
IDENTIFIER LOG_KNOTIFICATIONS
CATEGORY_NAME kf.notifications
OLD_CATEGORY_NAMES org.kde.knotifications
DESCRIPTION "KNotifications"
EXPORT KNOTIFICATIONS
)
if (TARGET Canberra::Canberra)
target_sources(KF6Notifications PRIVATE
notifybyaudio.cpp
)
endif()
if (HAVE_DBUS)
set(notifications_xml org.freedesktop.Notifications.xml)
qt_add_dbus_interface(knotifications_dbus_SRCS ${notifications_xml} notifications_interface)
target_sources(KF6Notifications PRIVATE ${knotifications_dbus_SRCS})
endif()
configure_file(config-knotifications.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-knotifications.h )
ecm_generate_export_header(KF6Notifications
EXPORT_FILE_NAME knotifications_export.h
BASE_NAME KNotifications
GROUP_BASE_NAME KF
VERSION ${KF_VERSION}
USE_VERSION_HEADER
DEPRECATED_BASE_VERSION 0
DEPRECATION_VERSIONS
EXCLUDE_DEPRECATED_BEFORE_AND_AT ${EXCLUDE_DEPRECATED_BEFORE_AND_AT}
)
target_include_directories(KF6Notifications INTERFACE "$<INSTALL_INTERFACE:${KDE_INSTALL_INCLUDEDIR_KF}/KNotifications>")
target_link_libraries(KF6Notifications PUBLIC Qt6::Gui)
if (HAVE_DBUS)
target_link_libraries(KF6Notifications PUBLIC Qt6::DBus)
endif()
target_link_libraries(KF6Notifications PRIVATE
KF6::ConfigCore
)
if (ANDROID)
target_link_libraries(KF6Notifications PRIVATE Qt::CorePrivate)
endif()
if (TARGET SnoreToast::SnoreToastActions)
target_link_libraries(KF6Notifications PRIVATE Qt6::Network SnoreToast::SnoreToastActions)
endif ()
if (TARGET Canberra::Canberra)
target_link_libraries(KF6Notifications PRIVATE Canberra::Canberra)
endif()
if(APPLE)
target_link_libraries(KF6Notifications PRIVATE "-framework Foundation" "-framework AppKit")
endif()
ecm_generate_headers(KNotifications_HEADERS
HEADER_NAMES
KNotification
KNotificationPermission
KNotificationReplyAction
KNotifyConfig
REQUIRED_HEADERS KNotifications_HEADERS
)
install(TARGETS KF6Notifications EXPORT KF6NotificationsTargets ${KF_INSTALL_TARGETS_DEFAULT_ARGS})
install(FILES
${CMAKE_CURRENT_BINARY_DIR}/knotifications_export.h
${KNotifications_HEADERS}
DESTINATION ${KDE_INSTALL_INCLUDEDIR_KF}/KNotifications COMPONENT Devel
)
ecm_qt_install_logging_categories(
EXPORT KNOTIFICATIONS
FILE knotifications.categories
DESTINATION ${KDE_INSTALL_LOGGINGCATEGORIESDIR}
)
if(BUILD_QCH)
ecm_add_qch(
KF6Notifications_QCH
NAME KNotifications
BASE_NAME KF6Notifications
VERSION ${KF_VERSION}
ORG_DOMAIN org.kde
SOURCES # using only public headers, to cover only public API
${KNotifications_HEADERS}
"${CMAKE_SOURCE_DIR}/Mainpage.dox"
IMAGE_DIRS "${CMAKE_SOURCE_DIR}/docs/pics"
LINK_QCHS
Qt6Gui_QCH
INCLUDE_DIRS
${CMAKE_CURRENT_BINARY_DIR}
BLANK_MACROS
KNOTIFICATIONS_EXPORT
KNOTIFICATIONS_DEPRECATED
KNOTIFICATIONS_DEPRECATED_EXPORT
"KNOTIFICATIONS_DEPRECATED_VERSION(x, y, t)"
"KNOTIFICATIONS_DEPRECATED_VERSION_BELATED(x, y, xt, yt, t)"
TAGFILE_INSTALL_DESTINATION ${KDE_INSTALL_QTQCHDIR}
QCH_INSTALL_DESTINATION ${KDE_INSTALL_QTQCHDIR}
COMPONENT Devel
)
endif()
if (ANDROID)
install(FILES KF6Notifications-android-dependencies.xml
DESTINATION ${KDE_INSTALL_LIBDIR}
RENAME KF6Notifications_${CMAKE_ANDROID_ARCH_ABI}-android-dependencies.xml
)
endif()
if (TARGET Qt6::Qml)
# add_subdirectory(qml)
endif()
@@ -0,0 +1,9 @@
<rules>
<dependencies>
<lib name="KF6Notifications">
<depends>
<jar bundling="1" file="jar/KF6Notifications.aar"/>
</depends>
</lib>
</dependencies>
</rules>
@@ -0,0 +1,6 @@
#!/bin/sh
# Extract strings from all source files.
# EXTRACT_TR_STRINGS extracts strings with lupdate and convert them to .pot with
# lconvert.
$EXTRACT_TR_STRINGS `find . -name \*.cpp -o -name \*.h -o -name \*.ui -o -name \*.qml` -o $podir/knotifications6_qt.pot
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="org.kde.knotifications">
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
</manifest>
@@ -0,0 +1,2 @@
gradle_add_aar(knotifications_aar BUILDFILE ${CMAKE_CURRENT_SOURCE_DIR}/build.gradle NAME KF6Notifications)
gradle_install_aar(knotifications_aar DESTINATION jar)
@@ -0,0 +1,39 @@
buildscript {
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:@Gradle_ANDROID_GRADLE_PLUGIN_VERSION@'
}
}
repositories {
google()
jcenter()
}
apply plugin: 'com.android.library'
android {
compileSdkVersion @ANDROID_SDK_COMPILE_API@
buildToolsVersion '@ANDROID_SDK_BUILD_TOOLS_REVISION@'
sourceSets {
main {
manifest.srcFile '@CMAKE_CURRENT_SOURCE_DIR@/AndroidManifest.xml'
java.srcDirs = ['@CMAKE_CURRENT_SOURCE_DIR@/org']
}
}
lintOptions {
abortOnError false
}
defaultConfig {
minSdkVersion @ANDROID_API_LEVEL@
targetSdkVersion @ANDROID_SDK_COMPILE_API@
namespace "org.kde.knotifications"
}
}
@@ -0,0 +1,54 @@
/*
SPDX-FileCopyrightText: 2018 Volker Krause <vkrause@kde.org>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
package org.kde.knotifications;
import android.graphics.drawable.Icon;
import android.os.Build;
import java.lang.Object;
import java.util.ArrayList;
import java.util.HashMap;
/** Java side of KNotification.
* Used to convey the relevant notification data to Java.
*/
public class KNotification
{
public int id;
public String text;
public String richText;
public String title;
public Object icon;
public HashMap<String, String> actions = new HashMap<>();
public String channelId;
public String channelName;
public String channelDescription;
public String group;
public int urgency;
public String visibility;
public String inlineReplyLabel;
public String inlineReplyPlaceholder;
// see knotification.h
public static final int LowUrgency = 10;
public static final int NormalUrgency = 50;
public static final int HighUrgency = 70;
public static final int CriticalUrgency = 90;
public void setIconFromData(byte[] data, int length)
{
if (Build.VERSION.SDK_INT >= 23) {
icon = Icon.createWithData(data, 0, length);
}
}
public void addAction(String id, String label)
{
actions.put(id, label);
}
}
@@ -0,0 +1,343 @@
/*
SPDX-FileCopyrightText: 2018 Volker Krause <vkrause@kde.org>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
package org.kde.knotifications;
import android.app.Activity;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.RemoteInput;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.graphics.drawable.Icon;
import android.os.Build;
import android.os.Bundle;
import android.text.Html;
import android.util.Log;
import java.util.HashMap;
import java.util.HashSet;
/** Java side of the Android notification backend. */
public class NotifyByAndroid extends BroadcastReceiver
{
private static final String TAG = "org.kde.knotifications";
private static final String NOTIFICATION_ACTION = ".org.kde.knotifications.NOTIFICATION_ACTION";
private static final String NOTIFICATION_DELETED = ".org.kde.knotifications.NOTIFICATION_DELETED";
private static final String NOTIFICATION_OPENED = ".org.kde.knotifications.NOTIFICATION_OPENED";
private static final String NOTIFICATION_REPLIED = ".org.kde.knotifications.NOTIFICATION_REPLIED";
// the id of the notification triggering an intent
private static final String NOTIFICATION_ID_EXTRA = "org.kde.knotifications.NOTIFICATION_ID";
// the id of the action that was triggered for a notification
private static final String NOTIFICATION_ACTION_ID_EXTRA = "org.kde.knotifications.NOTIFICATION_ACTION_ID";
// the group a notification belongs too
private static final String NOTIFICATION_GROUP_EXTRA = "org.kde.knotifications.NOTIFICATION_GROUP";
// RemoteInput value key
private static final String REMOTE_INPUT_KEY = "REPLY";
// notification id offset for group summary notifications
// we need this to stay out of the regular notification's id space (which comes from the C++ side)
// and so we can distinguish if we received actions on regular notifications or group summaries
private static final int NOTIFICATION_GROUP_ID_FLAG = (1 << 24);
private android.content.Context m_ctx;
private NotificationManager m_notificationManager;
private int m_uniquePendingIntentId = 0;
private HashSet<String> m_channels = new HashSet();
private class GroupData {
public HashSet<Integer> childIds = new HashSet();
public int groupId;
};
private HashMap<String, GroupData> m_groupSummaries = new HashMap();
public NotifyByAndroid(android.content.Context context)
{
Log.i(TAG, context.getPackageName());
m_ctx = context;
m_notificationManager = (NotificationManager)m_ctx.getSystemService(Context.NOTIFICATION_SERVICE);
IntentFilter filter = new IntentFilter();
filter.addAction(m_ctx.getPackageName() + NOTIFICATION_ACTION);
filter.addAction(m_ctx.getPackageName() + NOTIFICATION_DELETED);
filter.addAction(m_ctx.getPackageName() + NOTIFICATION_OPENED);
filter.addAction(m_ctx.getPackageName() + NOTIFICATION_REPLIED);
if (Build.VERSION.SDK_INT >= 33) {
m_ctx.registerReceiver(this, filter, Context.RECEIVER_NOT_EXPORTED);
} else {
m_ctx.registerReceiver(this, filter);
}
}
public void notify(KNotification notification)
{
Log.i(TAG, notification.text);
// notification channel
if (!m_channels.contains(notification.channelId)) {
m_channels.add(notification.channelId);
if (Build.VERSION.SDK_INT >= 26) {
NotificationChannel channel = new NotificationChannel(notification.channelId, notification.channelName, NotificationManager.IMPORTANCE_DEFAULT);
channel.setDescription(notification.channelDescription);
switch (notification.urgency) {
case KNotification.CriticalUrgency:
channel.setImportance(NotificationManager.IMPORTANCE_HIGH);
break;
case KNotification.NormalUrgency:
channel.setImportance(NotificationManager.IMPORTANCE_LOW);
break;
case KNotification.LowUrgency:
channel.setImportance(NotificationManager.IMPORTANCE_MIN);
break;
case KNotification.HighUrgency:
default:
channel.setImportance(NotificationManager.IMPORTANCE_DEFAULT);
break;
}
m_notificationManager.createNotificationChannel(channel);
}
}
Notification.Builder builder;
if (Build.VERSION.SDK_INT >= 26) {
builder = new Notification.Builder(m_ctx, notification.channelId);
} else {
builder = new Notification.Builder(m_ctx);
}
if (Build.VERSION.SDK_INT >= 23) {
builder.setSmallIcon((Icon)notification.icon);
} else {
builder.setSmallIcon(m_ctx.getApplicationInfo().icon);
}
builder.setContentTitle(notification.title);
builder.setContentText(notification.text);
// regular notifications show only a single line of content, if we have more
// we need the "BigTextStyle" expandable notifications to make everything readable
// in the single line case this behaves like the regular one, so no special-casing needed
if (Build.VERSION.SDK_INT >= 24) {
builder.setStyle(new Notification.BigTextStyle().bigText(Html.fromHtml(notification.richText, Html.FROM_HTML_MODE_COMPACT)));
} else {
builder.setStyle(new Notification.BigTextStyle().bigText(Html.fromHtml(notification.richText)));
}
// lock screen visibility
switch (notification.visibility) {
case "public":
builder.setVisibility(Notification.VISIBILITY_PUBLIC);
break;
case "private":
builder.setVisibility(Notification.VISIBILITY_PRIVATE);
break;
case "secret":
builder.setVisibility(Notification.VISIBILITY_SECRET);
break;
}
// grouping
if (notification.group != null) {
createGroupNotification(notification);
builder.setGroup(notification.group);
}
// legacy priority handling for versions without NotificationChannel support
if (Build.VERSION.SDK_INT < 26) {
switch (notification.urgency) {
case KNotification.CriticalUrgency:
builder.setPriority(Notification.PRIORITY_HIGH);
break;
case KNotification.NormalUrgency:
builder.setPriority(Notification.PRIORITY_LOW);
break;
case KNotification.LowUrgency:
builder.setPriority(Notification.PRIORITY_MIN);
break;
case KNotification.HighUrgency:
default:
builder.setPriority(Notification.PRIORITY_DEFAULT);
break;
}
}
// pending intent flags backward compatibility
int PENDING_INTENT_FLAG_IMMUTABLE = 0;
int PENDING_INTENT_FLAG_MUTABLE = 0;
if (Build.VERSION.SDK_INT >= 23) {
PENDING_INTENT_FLAG_IMMUTABLE = PendingIntent.FLAG_IMMUTABLE;
}
if (Build.VERSION.SDK_INT >= 31) {
PENDING_INTENT_FLAG_MUTABLE = PendingIntent.FLAG_MUTABLE;
}
// taping the notification shows the app
Intent intent = new Intent(m_ctx.getPackageName() + NOTIFICATION_OPENED);
intent.putExtra(NOTIFICATION_ID_EXTRA, notification.id);
PendingIntent contentIntent = PendingIntent.getBroadcast(m_ctx, m_uniquePendingIntentId++, intent, PendingIntent.FLAG_UPDATE_CURRENT | PENDING_INTENT_FLAG_IMMUTABLE);
builder.setContentIntent(contentIntent);
// actions
for (HashMap.Entry<String, String> entry : notification.actions.entrySet()) {
String id = entry.getKey();
String label = entry.getValue();
Intent actionIntent = new Intent(m_ctx.getPackageName() + NOTIFICATION_ACTION);
actionIntent.putExtra(NOTIFICATION_ID_EXTRA, notification.id);
actionIntent.putExtra(NOTIFICATION_ACTION_ID_EXTRA, id);
PendingIntent pendingIntent = PendingIntent.getBroadcast(m_ctx, m_uniquePendingIntentId++, actionIntent, PendingIntent.FLAG_UPDATE_CURRENT | PENDING_INTENT_FLAG_IMMUTABLE);
Notification.Action action = new Notification.Action.Builder(0, label, pendingIntent).build();
builder.addAction(action);
}
// inline reply actions
if (notification.inlineReplyLabel != null) {
Intent replyIntent = new Intent(m_ctx.getPackageName() + NOTIFICATION_REPLIED);
replyIntent.putExtra(NOTIFICATION_ID_EXTRA, notification.id);
PendingIntent pendingReplyIntent = PendingIntent.getBroadcast(m_ctx, m_uniquePendingIntentId++, replyIntent, PendingIntent.FLAG_UPDATE_CURRENT | PENDING_INTENT_FLAG_MUTABLE);
RemoteInput input = new RemoteInput.Builder(REMOTE_INPUT_KEY)
.setAllowFreeFormInput(true)
.setLabel(notification.inlineReplyPlaceholder)
.build();
Notification.Action replyAction = new Notification.Action.Builder(0, notification.inlineReplyLabel, pendingReplyIntent)
.addRemoteInput(input)
.build();
builder.addAction(replyAction);
}
// notification about user closing the notification
Intent deleteIntent = new Intent(m_ctx.getPackageName() + NOTIFICATION_DELETED);
deleteIntent.putExtra(NOTIFICATION_ID_EXTRA, notification.id);
if (notification.group != null) {
deleteIntent.putExtra(NOTIFICATION_GROUP_EXTRA, notification.group);
}
Log.i(TAG, deleteIntent.getExtras() + " " + notification.id);
builder.setDeleteIntent(PendingIntent.getBroadcast(m_ctx, m_uniquePendingIntentId++, deleteIntent, PendingIntent.FLAG_UPDATE_CURRENT | PENDING_INTENT_FLAG_IMMUTABLE));
m_notificationManager.notify(notification.id, builder.build());
}
public void close(int id, String group)
{
m_notificationManager.cancel(id);
if (group != null && m_groupSummaries.containsKey(group)) {
GroupData g = m_groupSummaries.get(group);
g.childIds.remove(id);
if (g.childIds.isEmpty()) {
m_groupSummaries.remove(group);
m_notificationManager.cancel(g.groupId);
} else {
m_groupSummaries.put(group, g);
}
}
}
@Override
public void onReceive(Context context, Intent intent)
{
String action = intent.getAction();
int id = intent.getIntExtra(NOTIFICATION_ID_EXTRA, -1);
Log.i(TAG, action + ": " + id + " " + intent.getExtras());
if (action.equals(m_ctx.getPackageName() + NOTIFICATION_ACTION)) {
// user activated one of the custom actions
String actionId = intent.getStringExtra(NOTIFICATION_ACTION_ID_EXTRA);
notificationActionInvoked(id, actionId);
} else if (action.equals(m_ctx.getPackageName() + NOTIFICATION_DELETED)) {
// user (or system) dismissed the notification - this can happen both for groups and regular notifications
String group = null;
if (intent.hasExtra(NOTIFICATION_GROUP_EXTRA)) {
group = intent.getStringExtra(NOTIFICATION_GROUP_EXTRA);
}
if ((id & NOTIFICATION_GROUP_ID_FLAG) != 0) {
// entire group has been deleted
m_groupSummaries.remove(group);
} else {
// a single regular notification, so reduce the refcount of the group if there is one
notificationFinished(id);
if (group != null && m_groupSummaries.containsKey(group)) {
// we do not need to handle the case of childIds being empty here, the system will send us a deletion intent for the group too
// this only matters for the logic in close().
GroupData g = m_groupSummaries.get(group);
g.childIds.remove(id);
m_groupSummaries.put(group, g);
}
}
} else if (action.equals(m_ctx.getPackageName() + NOTIFICATION_OPENED)) {
// user tapped the notification
Intent newintent = new Intent(m_ctx, m_ctx.getClass());
newintent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
m_ctx.startActivity(newintent);
notificationActionInvoked(id, "default");
} else if (action.equals(m_ctx.getPackageName() + NOTIFICATION_REPLIED)) {
Bundle remoteInput = RemoteInput.getResultsFromIntent(intent);
if (remoteInput != null) {
String s = (String) remoteInput.getCharSequence(REMOTE_INPUT_KEY);
notificationInlineReply(id, s);
}
}
}
public native void notificationFinished(int notificationId);
public native void notificationActionInvoked(int notificationId, String action);
public native void notificationInlineReply(int notificationId, String text);
private void createGroupNotification(KNotification notification)
{
if (m_groupSummaries.containsKey(notification.group)) {
GroupData group = m_groupSummaries.get(notification.group);
group.childIds.add(notification.id);
m_groupSummaries.put(notification.group, group);
return;
}
GroupData group = new GroupData();
group.childIds.add(notification.id);
group.groupId = m_uniquePendingIntentId++ + NOTIFICATION_GROUP_ID_FLAG;
m_groupSummaries.put(notification.channelId, group);
Notification.Builder builder;
if (Build.VERSION.SDK_INT >= 26) {
builder = new Notification.Builder(m_ctx, notification.channelId);
} else {
builder = new Notification.Builder(m_ctx);
}
if (Build.VERSION.SDK_INT >= 23) {
builder.setSmallIcon((Icon)notification.icon);
} else {
builder.setSmallIcon(m_ctx.getApplicationInfo().icon);
}
builder.setContentTitle(notification.channelName);
builder.setContentText(notification.channelDescription);
builder.setGroup(notification.group);
builder.setGroupSummary(true);
// monitor for deletion (which happens when the last child notification is closed)
int PENDING_INTENT_FLAG_IMMUTABLE = 0;
if (Build.VERSION.SDK_INT >= 23) {
PENDING_INTENT_FLAG_IMMUTABLE = PendingIntent.FLAG_IMMUTABLE;
}
Intent deleteIntent = new Intent(m_ctx.getPackageName() + NOTIFICATION_DELETED);
deleteIntent.putExtra(NOTIFICATION_GROUP_EXTRA, notification.group);
deleteIntent.putExtra(NOTIFICATION_ID_EXTRA, group.groupId);
builder.setDeleteIntent(PendingIntent.getBroadcast(m_ctx, m_uniquePendingIntentId++, deleteIntent, PendingIntent.FLAG_UPDATE_CURRENT | PENDING_INTENT_FLAG_IMMUTABLE));
// try to stay out of the normal id space for regular notifications
m_notificationManager.notify(group.groupId, builder.build());
}
}
@@ -0,0 +1,479 @@
[Event/fatalerror]
Name=Fatal Error
Name[ar]=خطأ فادح
Name[ast]=Error fatal
Name[az]=Ciddi Xəta
Name[be]=Фатальная памылка
Name[bg]=Фатална грешка
Name[bs]=Kobna greška
Name[ca]=Error fatal
Name[ca@valencia]=Error fatal
Name[cs]=Kritická chyba
Name[da]=Fatal fejl
Name[de]=Schwerer Fehler
Name[el]=Μοιραίο σφάλμα
Name[en_GB]=Fatal Error
Name[eo]=Fatala Eraro
Name[es]=Error fatal
Name[et]=Parandamatu tõrge
Name[eu]=Zoritxarreko errorea
Name[fi]=Vakava virhe
Name[fr]=Erreur fatale
Name[gd]=Mearachd mharbhtach
Name[gl]=Erro fatal
Name[he]=שגיאה חמורה
Name[hi]=गंभीर त्रुटि
Name[hu]=Végzetes hiba
Name[ia]=Error Fatal
Name[id]=Error Fatal
Name[is]=Banvæn villa
Name[it]=Errore grave
Name[ka]=ფატალური შეცდომა
Name[ko]=치명적 오류
Name[lt]=Lemtingoji klaida
Name[lv]=Fatāla kļūda
Name[ml]=ഗുുരുതരപിശക്
Name[nb]=Kritisk feil
Name[nds]=Swoor Fehler
Name[nl]=Fatale fout
Name[nn]=Kritisk feil
Name[pa]=ਘਾਤਕ ਗਲਤੀ
Name[pl]=Poważny błąd
Name[pt]=Erro Fatal
Name[pt_BR]=Erro fatal
Name[ro]=Eroare fatală
Name[ru]=Критическая ошибка
Name[sa]=घातक त्रुटिः
Name[sk]=Fatálna chyba
Name[sl]=Usodna napaka
Name[sr]=Кобна грешка
Name[sr@ijekavian]=Кобна грешка
Name[sr@ijekavianlatin]=Kobna greška
Name[sr@latin]=Kobna greška
Name[sv]=Allvarligt fel
Name[ta]=முறிவுண்டாக்கும் சிக்கல்
Name[tg]=Хатои ҷиддӣ
Name[tr]=Onulmaz Hata
Name[uk]=Критична помилка
Name[vi]=Lỗi nghiêm trọng
Name[x-test]=xxFatal Errorxx
Name[zh_CN]=严重错误
Name[zh_TW]=嚴重錯誤
Comment=There was a serious error causing the program to exit
Comment[ar]=حدث خطأ جاد سبّب للبرنامج بإغلاقه
Comment[az]=Proqramdan çıxış zamanı ciddi xəta baş verdi
Comment[be]=Адбылася сур'ёзная памылка, якая прывяла да выхаду з праграмы
Comment[bg]=Имаше сериозна грешка, водеща до изход от програмата
Comment[bs]=Došlo je do ozbiljne greške zbog koje je program obustavljen
Comment[ca]=S'ha produït un error seriós que ha provocat la sortida del programa
Comment[ca@valencia]=S'ha produït un error seriós que ha provocat l'eixida del programa
Comment[cs]=Nastala závažná chyba, v důsledku které byl program ukončen
Comment[da]=Der var en alvorlig fejl som forårsagede at programmet afsluttede
Comment[de]=Es ist ein schwerer Fehler aufgetreten, woraufhin das Programm beendet wurde.
Comment[el]=Υπήρξε ένα σοβαρό σφάλμα που προκάλεσε την έξοδο του προγράμματος
Comment[en_GB]=There was a serious error causing the program to exit
Comment[eo]=Estis grava eraro kaŭzanta programon eliri
Comment[es]=Ha ocurrido un error serio que ha causado la salida del programa
Comment[et]=Tekkis tõsine viga, mis sundis programmi tööd lõpetama
Comment[eu]=Errore garrantzitsua bat egon da programa ixtea eragin duena
Comment[fi]=Vakava virhe aiheutti ohjelman päättymisen
Comment[fr]=Une erreur grave a entraîné la fin du programme
Comment[gd]=Chaidh am prògram fhàgail ri linn mearachd dona
Comment[gl]=Produciuse un erro grave que fixo que o programa saíse
Comment[he]=אירעה שגיאה חמורה שגרמה ליישום להיסגר
Comment[hi]=किसी गम्भीर त्रुटि के कारण अनुप्रयोग बन्द हो गया
Comment[hu]=Olyan hiba történt a programban, melynek hatására a program futása befejeződött
Comment[ia]=Il esseva un serie error que il causava le programma a exir
Comment[id]=Ada error serius yang menyebabkan program keluar
Comment[is]=Alvarleg villa átti sér stað sem olli því að forritið slökkti á sér
Comment[it]=Si è verificato un errore grave, che ha causato la chiusura del programma
Comment[ka]=სერიოზული შეცდომა, რომელმაც პროგრამის მუშაობა დაასრულა
Comment[ko]=프로그램이 종료될 정도의 큰 오류가 발생함
Comment[lt]=Įvyko rimta klaida, privertusi programą užbaigti darbą
Comment[lv]=Programmā ir notikusi nopietna kļūda, tāpēc tā izslēdzās
Comment[ml]=ഒരു ഗുരുതര പിശക് കാരണം പ്രോഗ്രാമില്‍ നിന്ന് പുറത്ത് കടന്നിരിക്കുന്നു
Comment[nb]=Det oppsto en alvorlig feil som gjorde at programmet avsluttet
Comment[nds]=Dat geev en swoor Fehler, dat Programm is utstegen
Comment[nl]=Er deed zich een serieuze fout voor die het programma deed afsluiten
Comment[nn]=Det oppstod ein alvorleg feil som gjorde av programmet avslutta
Comment[pa]=ਗੰਭੀਰ ਗ਼ਲਤੀ ਪਰੋਗਰਾਮ ਤੋਂ ਬਾਹਰ ਜਾਣ ਦਾ ਕਾਰਨ ਬਣੀ
Comment[pl]=Wystąpił poważny błąd i program zakończył działanie
Comment[pt]=Ocorreu um erro sério que fez com que o programa saísse
Comment[pt_BR]=Ocorreu um erro grave que causou o fechamento do programa
Comment[ro]=A intervenit o eroare gravă ce a provocat terminarea programului
Comment[ru]=Произошла серьёзная ошибка, приведшая к завершению работы программы
Comment[sa]=गम्भीरदोषः अभवत् यत् कार्यक्रमस्य निर्गमनं जातम्
Comment[sk]=Vyskytla sa vážna chyba, ktorá spôsobila ukončenie programu
Comment[sl]=Prišlo je do resne napake, zaradi katere se je program končal
Comment[sr]=Дошло је до озбиљне грешке због које је програм обустављен
Comment[sr@ijekavian]=Дошло је до озбиљне грешке због које је програм обустављен
Comment[sr@ijekavianlatin]=Došlo je do ozbiljne greške zbog koje je program obustavljen
Comment[sr@latin]=Došlo je do ozbiljne greške zbog koje je program obustavljen
Comment[sv]=Ett allvarligt fel uppstod vilket fick programmet att avslutas
Comment[ta]=செயலியை வெளியேற செய்யும் சிக்கல் ஏற்பட்டுள்ளது
Comment[tg]=Хатои ҷиддие рӯй дод, ки барномаро қатъ кард
Comment[tr]=Uygulamadan çıkacak kadar önemli bir hata oluştu
Comment[uk]=Трапилась серйозна помилка, що призвела до закриття програми
Comment[vi]=Đã có một lỗi nghiêm trọng khiến chương trình phải thoát
Comment[x-test]=xxThere was a serious error causing the program to exitxx
Comment[zh_CN]=有一个严重错误导致程序退出
Comment[zh_TW]=程式發生了嚴重錯誤,必須離開
Action=Popup
[Event/notification]
Name=Notification
Name[ar]=إخطار
Name[az]=Bildiriş
Name[be]=Апавяшчэнне
Name[bg]=Известия
Name[bs]=Obavještenje
Name[ca]=Notificació
Name[ca@valencia]=Notificació
Name[cs]=Upozornění
Name[da]=Bekendtgørelse
Name[de]=Benachrichtigung
Name[el]=Ειδοποίηση
Name[en_GB]=Notification
Name[eo]=Sciigo
Name[es]=Notificación
Name[et]=Märguanne
Name[eu]=Jakinarazpena
Name[fi]=Ilmoitus
Name[fr]=Notification
Name[gd]=Brath
Name[gl]=Notificación
Name[he]=התראה
Name[hi]=अधिसूचना
Name[hu]=Értesítés
Name[ia]=Notification
Name[id]=Notifikasi
Name[is]=Tilkynning
Name[it]=Notifica
Name[ka]=შეტყობინება
Name[ko]=알림
Name[lt]=Pranešimas
Name[lv]=Paziņojums
Name[ml]=മുന്നറിയിപ്പുകള്‍
Name[nb]=Varsling
Name[nds]=Bescheed
Name[nl]=Melding
Name[nn]=Varsling
Name[pa]=ਸੂਚਨਾ
Name[pl]=Powiadomienie
Name[pt]=Notificação
Name[pt_BR]=Notificação
Name[ro]=Notificare
Name[ru]=Уведомление
Name[sa]=अधिसूचना
Name[sk]=Pripomienka
Name[sl]=Obvestila
Name[sr]=Обавештење
Name[sr@ijekavian]=Обавештење
Name[sr@ijekavianlatin]=Obaveštenje
Name[sr@latin]=Obaveštenje
Name[sv]=Underrättelse
Name[ta]=அறிவிப்பு
Name[tg]=Огоҳинома
Name[tr]=Bildirim
Name[uk]=Сповіщення
Name[vi]=Thông báo
Name[x-test]=xxNotificationxx
Name[zh_CN]=通知
Name[zh_TW]=通知
Comment=Something special happened in the program
Comment[ar]=حدث شيء خاص في البرنامج
Comment[az]=Proqramda xüsusi bir şey baş verdi
Comment[be]=У праграме адбылося нешта незразумелае
Comment[bg]=Случи се нещо необичайно при изпълнение на програмата
Comment[bs]=Nešto posebno se dogodilo u programu
Comment[ca]=Quelcom especial ha passat en el programa
Comment[ca@valencia]=Alguna cosa especial ha passat en el programa
Comment[cs]=Něco zvláštního se stalo s programem
Comment[da]=Noget specielt skete i programmet
Comment[de]=Im Programm ist ein besonderes Ereignis eingetreten
Comment[el]=Κάτι ιδιαίτερο συνέβη στο πρόγραμμα
Comment[en_GB]=Something special happened in the program
Comment[eo]=Io speciala okazis en la programo
Comment[es]=Ha ocurrido algo especial en el programa
Comment[et]=Programmis juhtus midagi erilist
Comment[eu]=Zerbait berezia gertatu da programan
Comment[fi]=Ohjelmassa tapahtui jotakin erikoista
Comment[fr]=Quelque chose d'anormal s'est produit dans le programme
Comment[gd]=Thachair rudeigin sònraichte sa phrògram
Comment[gl]=Ocorreu algo especial no programa
Comment[he]=משהו מיוחד קרה ביישום
Comment[hi]=अनुप्रयोग में कुछ विशेष हुआ
Comment[hu]=Valamilyen esemény következett be a programban
Comment[ia]=Il occurreva qualcosa de special in le programma
Comment[id]=Sesuatu yang istimewa terjadi dalam program
Comment[is]=Eitthvað sérstakt gerðist í keyrslu forritsins
Comment[it]=Si è verificato qualcosa di speciale nel programma
Comment[ka]=პროგრამას რაღაც განსაკუთრებული მოუვიდა
Comment[ko]=프로그램에서 특별한 일이 발생함
Comment[lt]=Programoje nutikos kažkas specialaus
Comment[lv]=Programmā ir noticis kaut kas vērā ņemams
Comment[ml]=പ്രത്യേകതയുള്ളയെന്തോ പ്രോഗ്രാമില്‍ സംഭവിച്ചിരിക്കുന്നു
Comment[nb]=Det hendte noe spesielt i programmet
Comment[nds]=Dat geev en afsünnerlich Begeefnis binnen dat Programm
Comment[nl]=Er gebeurde iets bijzonders in het programma
Comment[nn]=Det skjedde noko spesielt i programmet
Comment[pl]=Coś niezwykłego stało się z programem
Comment[pt]=Ocorreu algo de especial no programa
Comment[pt_BR]=Algo especial aconteceu no programa
Comment[ro]=S-a întâmplat ceva deosebit în program
Comment[ru]=В программе что-то произошло
Comment[sa]=कार्यक्रमे किञ्चित् विशेषं घटितम्
Comment[sk]=V programe sa stalo niečo zvláštne
Comment[sl]=V programu se je zgodilo nekaj posebnega
Comment[sr]=Нешто посебно се догодило у програму
Comment[sr@ijekavian]=Нешто посебно се догодило у програму
Comment[sr@ijekavianlatin]=Nešto posebno se dogodilo u programu
Comment[sr@latin]=Nešto posebno se dogodilo u programu
Comment[sv]=Någonting speciellt inträffade i programmet
Comment[ta]=செயலியில் ஏதோ நடந்துள்ளது
Comment[tg]=Чизе махсус дар барнома рӯй дод
Comment[tr]=Uygulamada bazı özel durumlar oluştu
Comment[uk]=Щось особливе сталось у програмі
Comment[vi]=Thứ gì đó đặc biệt đã xảy ra trong chương trình
Comment[x-test]=xxSomething special happened in the programxx
Comment[zh_CN]=这个程序中发生了特殊的事情
Comment[zh_TW]=程式中發生了特殊事件
Action=Popup
[Event/warning]
Name=Warning
Name[ar]=تحذير
Name[az]=Xəbərdarlıq
Name[be]=Папярэджанне
Name[bg]=Предупреждение
Name[bs]=Upozorenje
Name[ca]=Avís
Name[ca@valencia]=Avís
Name[cs]=Varování
Name[da]=Advarsel
Name[de]=Warnung
Name[el]=Προειδοποίηση
Name[en_GB]=Warning
Name[eo]=Averto
Name[es]=Advertencia
Name[et]=Hoiatus
Name[eu]=Abisua
Name[fi]=Varoitus
Name[fr]=Avertissement
Name[gd]=Rabhadh
Name[gl]=Aviso
Name[he]=אזהרה
Name[hi]=चेतावनी
Name[hu]=Figyelmeztetés
Name[ia]=Aviso
Name[id]=Peringatan
Name[is]=Aðvörun
Name[it]=Avviso
Name[ka]=გაფრთხილება
Name[ko]=경고
Name[lt]=Įspėjimas
Name[lv]=Uzmanību
Name[ml]=താക്കീത്0
Name[nb]=Advarsel
Name[nds]=Wohrschoen
Name[nl]=Waarschuwing
Name[nn]=Åtvaring
Name[pa]=ਚੇਤਾਵਨੀ
Name[pl]=Ostrzeżenie
Name[pt]=Aviso
Name[pt_BR]=Aviso
Name[ro]=Avertisment
Name[ru]=Предупреждение
Name[sa]=चेतवानी
Name[sk]=Upozornenie
Name[sl]=Opozorilo
Name[sr]=Упозорење
Name[sr@ijekavian]=Упозорење
Name[sr@ijekavianlatin]=Upozorenje
Name[sr@latin]=Upozorenje
Name[sv]=Varning
Name[ta]=எச்சரிக்கை
Name[tg]=Огоҳӣ
Name[tr]=Uyarı
Name[uk]=Попередження
Name[vi]=Cảnh báo
Name[x-test]=xxWarningxx
Name[zh_CN]=警告
Name[zh_TW]=警告
Comment=There was an error in the program which may cause problems
Comment[ar]=حدث خطأ في البرنامج مما تسبّب بمشاكل
Comment[az]=Proqramda problemlər yarada biləcək xəta baş verdi
Comment[be]=У праграме адбылася памылка, праз якую могуць паўстаць праблемы
Comment[bg]=Имаше грешка в програмата, която може да доведе до проблеми
Comment[bs]=Došlo je do greške u programu koja može izazvati probleme
Comment[ca]=S'ha produït un error en el programa que pot provocar problemes
Comment[ca@valencia]=S'ha produït un error en el programa que pot provocar problemes
Comment[cs]=V programu nastala chyba, která může způsobit problémy
Comment[da]=Der var en fejl i program som kan volde problemer
Comment[de]=Im Programm ist ein Fehler aufgetreten, der Probleme verursachen könnte
Comment[el]=Υπήρξε σφάλμα στο πρόγραμμα που μπορεί να προκαλέσει προβλήματα
Comment[en_GB]=There was an error in the program which may cause problems
Comment[eo]=Estis eraro en la programo, kiu povas kaŭzi problemojn
Comment[es]=Ha ocurrido un error en el programa que puede causar problemas
Comment[et]=Programmis tekkis viga, mis võib tekitada probleeme
Comment[eu]=Errore bat egon da programan arazoak sor ditzakeena
Comment[fi]=Ohjelmassa oli virhe, joka voi aiheuttaa ongelmia
Comment[fr]=Il y a eu une erreur dans le programme qui peut causer des problèmes
Comment[gd]=Bha mearachd sa phrògram a dh'adhbharaicheas trioblaid ma dh'fhaoidte
Comment[gl]=Produciuse un erro no programa que pode causar problemas
Comment[he]=אירעה שגיאה ביישום, מה שעלול לגרום לבעיות
Comment[hi]=अनुप्रयोग में कोई त्रुटि हुई है, जिसके कारण उसमें कुछ परेशानियाँ आ सकती हेैं
Comment[hu]=Hiba történt a programban, de a program futása folytatódhat
Comment[ia]=Il habeva un error in le programma que il pote causar problemas
Comment[id]=Ada sebuah error dalam program yang menyebabkan masalah
Comment[is]=Villa kom upp í forritinu sem valdið vandamálum
Comment[it]=Si è verificato un errore nel programma che potrebbe causare problemi
Comment[ka]=პროგრამის შეცდომა, რომელსაც პრობლემების გამოწვევა შეუძლია
Comment[ko]=프로그램에 문제를 가져올 수 있는 오류가 발생함
Comment[lt]=Programoje įvyko klaida, galinti sukelti problemų
Comment[lv]=Programmā ir notikusi kļūda, kas var radīt problēmas
Comment[ml]=പ്രശ്നങ്ങള്‍ക്ക് കാരണമായേക്കാവുന്ന പിശക് പ്രോഗ്രാമില്‍ സംഭവിച്ചിരിക്കുന്നു
Comment[nb]=I programmet oppsto det en feil som kan gi vanskeligheter
Comment[nds]=Dat geev en Fehler binnen dat Programm, villicht suert dor Problemen bi rut
Comment[nl]=Er deed zich een fout voor in het programma dat problemen kan veroorzaken
Comment[nn]=Det oppstod ein feil i programmet som kan gje problem
Comment[pl]=W programie wystąpił błąd, może powodować problemy
Comment[pt]=Ocorreu um erro no programa que poderá provocar problemas
Comment[pt_BR]=Ocorreu um erro no programa, que pode causar problemas
Comment[ro]=A avut loc o eroare în program ce ar putea provoca probleme
Comment[ru]=В программе произошла ошибка, которая может вызвать проблемы
Comment[sa]=कार्यक्रमे त्रुटिः आसीत् या समस्यां जनयितुं शक्नोति
Comment[sk]=V programe sa vyskytla chyba, ktorá môže spôsobiť problémy
Comment[sl]=V programu je prišlo do napake, ki lahko povzroči težave
Comment[sr]=Дошло је до грешке у програму која може изазвати проблеме
Comment[sr@ijekavian]=Дошло је до грешке у програму која може изазвати проблеме
Comment[sr@ijekavianlatin]=Došlo je do greške u programu koja može izazvati probleme
Comment[sr@latin]=Došlo je do greške u programu koja može izazvati probleme
Comment[sv]=Det uppstod ett fel i programmet vilket kan orsaka problem
Comment[ta]=பிரச்சனைகளை உண்டாக்கக்கூடிய சிக்கல் செயலியில் ஏற்பட்டுள்ளது
Comment[tg]=Хатое дар барнома рӯй дод, ки метавонад мушкилиеро ба вуҷуд орад
Comment[tr]=Uygulamada sorunlara neden olabilecek bir hata oluştu
Comment[uk]=В програмі трапилась помилка, яка може спричинити проблеми
Comment[vi]=Có lỗi trong chương trình, có thể gây ra vấn đề
Comment[x-test]=xxThere was an error in the program which may cause problemsxx
Comment[zh_CN]=程序中有一个错误,它可能导致一些问题
Comment[zh_TW]=程式中發生錯誤,可能造成問題
Action=Popup
[Event/catastrophe]
Name=Catastrophe
Name[ar]=كارثة
Name[az]=Fəlakət
Name[be]=Катастрофа
Name[bg]=Срив
Name[bs]=Katastrofa
Name[ca]=Catàstrofe
Name[ca@valencia]=Catàstrofe
Name[cs]=Katastrofa
Name[da]=Katastrofe
Name[de]=Katastrophe
Name[el]=Καταστροφή
Name[en_GB]=Catastrophe
Name[eo]=Katastrofo
Name[es]=Catástrofe
Name[et]=Katastroof
Name[eu]=Hondamendia
Name[fi]=Katastrofi
Name[fr]=Catastrophe
Name[gd]=Tubaist
Name[gl]=Catástrofe
Name[he]=קטטסטרופה
Name[hi]=दुर्घटना
Name[hu]=Súlyos hiba
Name[ia]=Catastrophe
Name[id]=Malapetaka
Name[is]=Hörmungarástand
Name[it]=Catastrofe
Name[ka]=კატასტროფა
Name[ko]=중대한 오류
Name[lt]=Katastrofa
Name[lv]=Katastrofa
Name[ml]=വിപത്ത്
Name[nb]=Katastrofe
Name[nds]=Groot Malöör
Name[nl]=Catastrofe
Name[nn]=Katastrofe
Name[pl]=Katastrofa
Name[pt]=Catástrofe
Name[pt_BR]=Catástrofe
Name[ro]=Catastrofă
Name[ru]=Катастрофа
Name[sa]=प्रलयम्
Name[sk]=Katastrofa
Name[sl]=Katastrofa
Name[sr]=Катастрофа
Name[sr@ijekavian]=Катастрофа
Name[sr@ijekavianlatin]=Katastrofa
Name[sr@latin]=Katastrofa
Name[sv]=Katastrof
Name[ta]=மிகப்பெரிய சிக்கல்
Name[tg]=Садама
Name[tr]=Felaket
Name[uk]=Катастрофа
Name[vi]=Thảm hoạ
Name[x-test]=xxCatastrophexx
Name[zh_CN]=严重故障
Name[zh_TW]=發生大災難
Comment=A very serious error occurred, at least causing the program to exit
Comment[ar]=حدث خطأ جاد جدًا، تسبّب للبرنامج بإنهائه على أقل تقدير
Comment[az]=Ən azı proqramın bağlanmasına səbəb olan çox ciddi bir səhv baş verdi
Comment[be]=Адбылася вельмі сур'ёзная памылка, якая прывяла да выхаду з праграмы
Comment[bg]=Възникна много сериозна грешка, която доведе до прекратяване на програмата
Comment[bs]=Došlo je do vrlo ozbiljne greške, zbog koje je bar program obustavljen
Comment[ca]=S'ha produït un error molt seriós, com a mínim ha provocat la sortida del programa
Comment[ca@valencia]=S'ha produït un error molt seriós, com a mínim ha provocat l'eixida del programa
Comment[cs]=Nastala velmi závažná chyba, která má za následek minimálně ukončení programu
Comment[da]=En meget alvorlig fejl opstod, som mindst forårsagede at programmet afsluttede
Comment[de]=Ein sehr schwerer Fehler ist aufgetreten, der wenigstens das Programm zum Beenden bringt
Comment[el]=Συνέβη ένα πολύ σοβαρό σφάλμα, που προκάλεσε την έξοδο του προγράμματος
Comment[en_GB]=A very serious error occurred, at least causing the program to exit
Comment[eo]=Tre grava eraro okazis, almenaŭ kaŭzante la eliro de la programo
Comment[es]=Ha ocurrido un error muy serio que ha causado, al menos, la salida del programa
Comment[et]=Tekkis väga tõsine viga, mis sundis programmi tööd lõpetama
Comment[eu]=Errore oso garrantzitsu bat egon da, gutxienez programa ixtea eragin duena
Comment[fi]=Tapahtui erittäin vakava virhe, joka aiheutti ainakin ohjelman päättymisen
Comment[fr]=Une erreur très grave s'est produite, provoquant au minimum la fin du programme
Comment[gd]=Thachair mearachd glè dhona agus chaidh am prògram fhàgail air a char as lugha
Comment[gl]=Produciuse un erro moi grave, que como mínimo fixo que o programa saíse
Comment[he]=אירעה שגיאה חמורה מאוד, שכנראה גרמה ליישום להיסגר
Comment[hi]=एक अत्यन्त गम्भीर त्रुटि हुई है जिसके कारण अनुप्रयोग बन्द हो गया
Comment[hu]=Hiba történt a programban, a program futása befejeződött
Comment[ia]=Il occurreva un error le plus serie, al minus causante le exito del programma
Comment[id]=Sebuah eror yang sangat serius terjadi, setidaknya menyebabkan program keluar
Comment[is]=Mjög alvarleg villa átti sér stað, a.m.k. næg til að forritið slökkti á sér
Comment[it]=Si è verificato un errore molto grave, che ha causato almeno la chiusura del programma
Comment[ka]=ძალიან სერიოზული შეცდომა, რომელმაც, ყველაზე ცოტა, პროგრამის მუშაობა დაასრულა
Comment[ko]=프로그램이 종료될 정도의 매우 큰 오류가 발생함
Comment[lt]=Įvyko labai rimta klaida, privertusi programą bent jau užbaigti darbą
Comment[lv]=Programmā ir notikusi ļoti nopietna kļūda, kas, mazākais, ir likusi programmai izslēgties
Comment[nb]=Det oppsto en meget alvorlig feil som minst førte til at programmet stoppet
Comment[nds]=Dat geev en groot Malöör, tominnst dat Programm is utstegen
Comment[nl]=Er deed zich een serieuze fout voor dat er op zijn minst voor zorgde dat het programma werd afgesloten
Comment[nn]=Det oppstod ein svært alvorleg feil som førte til at programmet avslutta
Comment[pl]=Wystąpił bardzo poważny błąd, co najmniej nastąpiło zakończenie pracy programu
Comment[pt]=Ocorreu um erro muito sério, que fez pelo menos com que o programa saísse
Comment[pt_BR]=Um erro muito sério aconteceu, que causou (no mínimo) o fechamento do programa
Comment[ro]=A intervenit o eroare foarte gravă, provocând cel puțin terminarea programului
Comment[ru]=Произошла очень серьёзная ошибка, как минимум, вызвавшая завершение работы программы
Comment[sa]=अतीव गम्भीरः त्रुटिः अभवत्, न्यूनातिन्यूनं कार्यक्रमस्य निर्गमनस्य कारणम्
Comment[sk]=Nastala veľmi vážna chyba, ktorá zapríčinila minimálne ukončenie programu
Comment[sl]=Prišlo je do zelo resne napake, zaradi katere se je program končal
Comment[sr]=Дошло је до врло озбиљне грешке, због које је бар програм обустављен
Comment[sr@ijekavian]=Дошло је до врло озбиљне грешке, због које је бар програм обустављен
Comment[sr@ijekavianlatin]=Došlo je do vrlo ozbiljne greške, zbog koje je bar program obustavljen
Comment[sr@latin]=Došlo je do vrlo ozbiljne greške, zbog koje je bar program obustavljen
Comment[sv]=Ett mycket allvarligt fel uppstod, som åtminstone fick programmet att avslutas
Comment[ta]=குறைந்தபட்சம் செயலியை வெளியேற வைக்கும் சிக்கல் ஏற்பட்டுள்ளது
Comment[tg]=Хатои хеле ҷиддӣ рӯй дод, ки ақаллан барномаро қатъ кард
Comment[tr]=En azından uygulamadan çıkacak kadar önemli bir hata oluştu
Comment[uk]=Трапилась дуже серйозна помилка, яка призвела до закриття програми
Comment[vi]=Một lỗi rất nghiêm trọng đã xảy ra, ít nhất nó khiến chương trình phải thoát
Comment[x-test]=xxA very serious error occurred, at least causing the program to exitxx
Comment[zh_CN]=发生了一个非常严重的错误,至少已经导致了这个程序退出
Comment[zh_TW]=發生了非常嚴重的問題,至少造成程式被迫結束
Action=Popup
@@ -0,0 +1 @@
#cmakedefine WITH_SNORETOAST
@@ -0,0 +1,78 @@
/*
SPDX-FileCopyrightText: 2009 Canonical
SPDX-FileContributor: Aurélien Gâteau <aurelien.gateau@canonical.com>
SPDX-License-Identifier: LGPL-2.0-only OR LGPL-3.0-only
*/
#include "imageconverter.h"
#include <QDBusArgument>
#include <QDBusMetaType>
#include <QImage>
namespace ImageConverter
{
/**
* A structure representing an image which can be marshalled to fit the
* notification spec.
*/
struct SpecImage {
int width, height, rowStride;
bool hasAlpha;
int bitsPerSample, channels;
QByteArray data;
};
QDBusArgument &operator<<(QDBusArgument &argument, const SpecImage &image)
{
argument.beginStructure();
argument << image.width << image.height << image.rowStride << image.hasAlpha;
argument << image.bitsPerSample << image.channels << image.data;
argument.endStructure();
return argument;
}
const QDBusArgument &operator>>(const QDBusArgument &argument, SpecImage &image)
{
argument.beginStructure();
argument >> image.width >> image.height >> image.rowStride >> image.hasAlpha;
argument >> image.bitsPerSample >> image.channels >> image.data;
argument.endStructure();
return argument;
}
} // namespace
// This must be before the QVariant::fromValue below (#211726)
Q_DECLARE_METATYPE(ImageConverter::SpecImage)
namespace ImageConverter
{
QVariant variantForImage(const QImage &_image)
{
qDBusRegisterMetaType<SpecImage>();
const bool hasAlpha = _image.hasAlphaChannel();
QImage image;
if (hasAlpha) {
image = _image.convertToFormat(QImage::Format_RGBA8888);
} else {
image = _image.convertToFormat(QImage::Format_RGB888);
}
QByteArray data((const char *)image.constBits(), image.sizeInBytes());
SpecImage specImage;
specImage.width = image.width();
specImage.height = image.height();
specImage.rowStride = image.bytesPerLine();
specImage.hasAlpha = hasAlpha;
specImage.bitsPerSample = 8;
specImage.channels = hasAlpha ? 4 : 3;
specImage.data = data;
return QVariant::fromValue(specImage);
}
} // namespace
@@ -0,0 +1,24 @@
/*
SPDX-FileCopyrightText: 2009 Canonical
SPDX-FileContributor: Aurélien Gâteau <aurelien.gateau@canonical.com>
SPDX-License-Identifier: LGPL-2.0-only OR LGPL-3.0-only
*/
#ifndef IMAGECONVERTER_H
#define IMAGECONVERTER_H
class QVariant;
class QImage;
namespace ImageConverter
{
/**
* Returns a variant representing an image using the format describe in the
* freedesktop.org spec
*/
QVariant variantForImage(const QImage &image);
} // namespace
#endif /* IMAGECONVERTER_H */
@@ -0,0 +1,654 @@
/*
This file is part of the KDE libraries
SPDX-FileCopyrightText: 2005-2006 Olivier Goffart <ogoffart at kde.org>
SPDX-FileCopyrightText: 2013-2014 Martin Klapetek <mklapetek@kde.org>
code from KNotify/KNotifyClient
SPDX-FileCopyrightText: 1997 Christian Esken <esken@kde.org>
SPDX-FileCopyrightText: 2000 Charles Samuels <charles@kde.org>
SPDX-FileCopyrightText: 2000 Stefan Schimanski <1Stein@gmx.de>
SPDX-FileCopyrightText: 2000 Matthias Ettrich <ettrich@kde.org>
SPDX-FileCopyrightText: 2000 Waldo Bastian <bastian@kde.org>
SPDX-FileCopyrightText: 2000-2003 Carsten Pfeiffer <pfeiffer@kde.org>
SPDX-FileCopyrightText: 2005 Allan Sandfeld Jensen <kde@carewolf.com>
SPDX-License-Identifier: LGPL-2.0-only
*/
#include "knotification.h"
#include "debug_p.h"
#include "knotification_p.h"
#include "knotificationmanager_p.h"
#include "knotificationreplyaction.h"
#include <config-knotifications.h>
#include <QGuiApplication>
#include <QStringList>
#include <QUrl>
// incremental notification ID
static int notificationIdCounter = 0;
class KNotificationActionPrivate
{
public:
QString label;
QString id;
};
KNotificationAction::KNotificationAction(QObject *parent)
: QObject(parent)
, d(new KNotificationActionPrivate)
{
}
KNotificationAction::KNotificationAction(const QString &label)
: QObject()
, d(new KNotificationActionPrivate)
{
d->label = label;
}
KNotificationAction::~KNotificationAction()
{
}
QString KNotificationAction::label() const
{
return d->label;
}
void KNotificationAction::setLabel(const QString &label)
{
if (d->label != label) {
d->label = label;
Q_EMIT labelChanged(label);
}
}
QString KNotificationAction::id() const
{
return d->id;
}
void KNotificationAction::setId(const QString &id)
{
d->id = id;
}
KNotification::KNotification(const QString &eventId, NotificationFlags flags, QObject *parent)
: QObject(parent)
, d(new Private)
{
d->eventId = eventId;
d->flags = flags;
connect(&d->updateTimer, &QTimer::timeout, this, &KNotification::update);
d->updateTimer.setSingleShot(true);
d->updateTimer.setInterval(100);
d->id = ++notificationIdCounter;
}
KNotification::~KNotification()
{
if (d->ownsActions) {
qDeleteAll(d->actions);
delete d->defaultAction;
}
if (d->id >= 0) {
KNotificationManager::self()->close(d->id);
}
}
QString KNotification::eventId() const
{
return d->eventId;
}
void KNotification::setEventId(const QString &eventId)
{
if (d->eventId != eventId) {
d->eventId = eventId;
Q_EMIT eventIdChanged();
}
}
QString KNotification::title() const
{
return d->title;
}
QString KNotification::text() const
{
return d->text;
}
void KNotification::setTitle(const QString &title)
{
if (title == d->title) {
return;
}
d->needUpdate = true;
d->title = title;
Q_EMIT titleChanged();
if (d->id >= 0) {
d->updateTimer.start();
}
}
void KNotification::setText(const QString &text)
{
if (text == d->text) {
return;
}
d->needUpdate = true;
d->text = text;
Q_EMIT textChanged();
if (d->id >= 0) {
d->updateTimer.start();
}
}
void KNotification::setIconName(const QString &icon)
{
if (icon == d->iconName) {
return;
}
d->needUpdate = true;
d->iconName = icon;
Q_EMIT iconNameChanged();
if (d->id >= 0) {
d->updateTimer.start();
}
}
QString KNotification::iconName() const
{
return d->iconName;
}
QPixmap KNotification::pixmap() const
{
return d->pixmap;
}
void KNotification::setPixmap(const QPixmap &pix)
{
d->needUpdate = true;
d->pixmap = pix;
if (d->id >= 0) {
d->updateTimer.start();
}
}
QList<KNotificationAction *> KNotification::actions() const
{
return d->actions;
}
void KNotification::clearActions()
{
if (d->ownsActions) {
qDeleteAll(d->actions);
}
d->actions.clear();
d->actionIdCounter = 1;
d->needUpdate = true;
if (d->id >= 0) {
d->updateTimer.start();
}
}
KNotificationAction *KNotification::addAction(const QString &label)
{
d->needUpdate = true;
KNotificationAction *action = new KNotificationAction(label);
action->setId(QString::number(d->actionIdCounter));
d->actionIdCounter++;
d->actions << action;
d->ownsActions = true;
Q_EMIT actionsChanged();
if (d->id >= 0) {
d->updateTimer.start();
}
return action;
}
void KNotification::setActionsQml(QList<KNotificationAction *> actions)
{
if (actions == d->actions) {
return;
}
d->actions.clear();
d->needUpdate = true;
d->actions = actions;
d->ownsActions = false;
Q_EMIT actionsChanged();
int idCounter = 1;
for (KNotificationAction *action : d->actions) {
action->setId(QString::number(idCounter));
++idCounter;
}
if (d->id >= 0) {
d->updateTimer.start();
}
}
KNotificationReplyAction *KNotification::replyAction() const
{
return d->replyAction.get();
}
void KNotification::setReplyAction(std::unique_ptr<KNotificationReplyAction> replyAction)
{
if (replyAction == d->replyAction) {
return;
}
d->needUpdate = true;
d->replyAction = std::move(replyAction);
if (d->id >= 0) {
d->updateTimer.start();
}
}
KNotificationAction *KNotification::addDefaultAction(const QString &label)
{
if (d->ownsActions) {
delete d->defaultAction;
}
d->needUpdate = true;
d->ownsActions = true;
d->defaultAction = new KNotificationAction(label);
d->defaultAction->setId(QStringLiteral("default"));
Q_EMIT defaultActionChanged();
if (d->id >= 0) {
d->updateTimer.start();
}
return d->defaultAction;
}
void KNotification::setDefaultActionQml(KNotificationAction *defaultAction)
{
if (defaultAction == d->defaultAction) {
return;
}
d->needUpdate = true;
d->defaultAction = defaultAction;
d->ownsActions = false;
d->defaultAction->setId(QStringLiteral("default"));
Q_EMIT defaultActionChanged();
if (d->id >= 0) {
d->updateTimer.start();
}
}
KNotificationAction *KNotification::defaultAction() const
{
return d->defaultAction;
}
KNotification::NotificationFlags KNotification::flags() const
{
return d->flags;
}
void KNotification::setFlags(const NotificationFlags &flags)
{
if (d->flags == flags) {
return;
}
d->needUpdate = true;
d->flags = flags;
Q_EMIT flagsChanged();
if (d->id >= 0) {
d->updateTimer.start();
}
}
QString KNotification::componentName() const
{
return d->componentName;
}
void KNotification::setComponentName(const QString &c)
{
if (d->componentName != c) {
d->componentName = c;
Q_EMIT componentNameChanged();
}
}
QList<QUrl> KNotification::urls() const
{
return QUrl::fromStringList(d->hints[QStringLiteral("x-kde-urls")].toStringList());
}
void KNotification::setUrls(const QList<QUrl> &urls)
{
setHint(QStringLiteral("x-kde-urls"), QUrl::toStringList(urls));
Q_EMIT urlsChanged();
}
KNotification::Urgency KNotification::urgency() const
{
return d->urgency;
}
void KNotification::setUrgency(Urgency urgency)
{
if (d->urgency == urgency) {
return;
}
d->needUpdate = true;
d->urgency = urgency;
Q_EMIT urgencyChanged();
if (d->id >= 0) {
d->updateTimer.start();
}
}
void KNotification::activate(const QString &actionId)
{
if (d->defaultAction && actionId == QLatin1String("default")) {
Q_EMIT d->defaultAction->activated();
}
for (KNotificationAction *action : d->actions) {
if (action->id() == actionId) {
Q_EMIT action->activated();
}
}
}
void KNotification::close()
{
if (d->id >= 0) {
KNotificationManager::self()->close(d->id);
}
if (d->id == -1) {
d->id = -2;
Q_EMIT closed();
if (d->autoDelete) {
deleteLater();
} else {
// reset for being reused
d->isNew = true;
d->id = ++notificationIdCounter;
}
}
}
static QString defaultComponentName()
{
#if defined(Q_OS_ANDROID)
return QStringLiteral("android_defaults");
#else
return QStringLiteral("plasma_workspace");
#endif
}
KNotification *KNotification::event(const QString &eventid,
const QString &title,
const QString &text,
const QPixmap &pixmap,
const NotificationFlags &flags,
const QString &componentName)
{
KNotification *notify = new KNotification(eventid, flags);
notify->setTitle(title);
notify->setText(text);
notify->setPixmap(pixmap);
notify->setComponentName((flags & DefaultEvent) ? defaultComponentName() : componentName);
QTimer::singleShot(0, notify, &KNotification::sendEvent);
return notify;
}
KNotification *
KNotification::event(const QString &eventid, const QString &text, const QPixmap &pixmap, const NotificationFlags &flags, const QString &componentName)
{
return event(eventid, QString(), text, pixmap, flags, componentName);
}
KNotification *KNotification::event(StandardEvent eventid, const QString &title, const QString &text, const QPixmap &pixmap, const NotificationFlags &flags)
{
return event(standardEventToEventId(eventid), title, text, pixmap, flags | DefaultEvent);
}
KNotification *KNotification::event(StandardEvent eventid, const QString &text, const QPixmap &pixmap, const NotificationFlags &flags)
{
return event(eventid, QString(), text, pixmap, flags);
}
KNotification *KNotification::event(const QString &eventid,
const QString &title,
const QString &text,
const QString &iconName,
const NotificationFlags &flags,
const QString &componentName)
{
KNotification *notify = new KNotification(eventid, flags);
notify->setTitle(title);
notify->setText(text);
notify->setIconName(iconName);
notify->setComponentName((flags & DefaultEvent) ? defaultComponentName() : componentName);
QTimer::singleShot(0, notify, &KNotification::sendEvent);
return notify;
}
KNotification *KNotification::event(StandardEvent eventid, const QString &title, const QString &text, const QString &iconName, const NotificationFlags &flags)
{
return event(standardEventToEventId(eventid), title, text, iconName, flags | DefaultEvent);
}
KNotification *KNotification::event(StandardEvent eventid, const QString &title, const QString &text, const NotificationFlags &flags)
{
return event(standardEventToEventId(eventid), title, text, standardEventToIconName(eventid), flags | DefaultEvent);
}
void KNotification::ref()
{
d->ref++;
}
void KNotification::deref()
{
Q_ASSERT(d->ref > 0);
d->ref--;
if (d->ref == 0) {
d->id = -1;
close();
}
}
void KNotification::beep(const QString &reason)
{
event(QStringLiteral("beep"), reason, QPixmap(), CloseOnTimeout | DefaultEvent);
}
void KNotification::sendEvent()
{
d->needUpdate = false;
if (d->isNew) {
d->isNew = false;
KNotificationManager::self()->notify(this);
} else {
KNotificationManager::self()->reemit(this);
}
}
int KNotification::id()
{
if (!d) {
return -1;
}
return d->id;
}
QString KNotification::appName() const
{
QString appname;
if (d->flags & DefaultEvent) {
appname = defaultComponentName();
} else if (!d->componentName.isEmpty()) {
appname = d->componentName;
} else {
appname = QCoreApplication::applicationName();
}
return appname;
}
bool KNotification::isAutoDelete() const
{
return d->autoDelete;
}
void KNotification::setAutoDelete(bool autoDelete)
{
if (d->autoDelete != autoDelete) {
d->autoDelete = autoDelete;
Q_EMIT autoDeleteChanged();
}
}
void KNotification::update()
{
if (d->needUpdate) {
KNotificationManager::self()->update(this);
}
}
QString KNotification::standardEventToEventId(KNotification::StandardEvent event)
{
QString eventId;
switch (event) {
case Warning:
eventId = QStringLiteral("warning");
break;
case Error:
eventId = QStringLiteral("fatalerror");
break;
case Catastrophe:
eventId = QStringLiteral("catastrophe");
break;
case Notification: // fall through
default:
eventId = QStringLiteral("notification");
break;
}
return eventId;
}
QString KNotification::standardEventToIconName(KNotification::StandardEvent event)
{
QString iconName;
switch (event) {
case Warning:
iconName = QStringLiteral("dialog-warning");
break;
case Error:
iconName = QStringLiteral("dialog-error");
break;
case Catastrophe:
iconName = QStringLiteral("dialog-error");
break;
case Notification: // fall through
default:
iconName = QStringLiteral("dialog-information");
break;
}
return iconName;
}
void KNotification::setHint(const QString &hint, const QVariant &value)
{
if (value == d->hints.value(hint)) {
return;
}
d->needUpdate = true;
d->hints[hint] = value;
if (d->id >= 0) {
d->updateTimer.start();
}
Q_EMIT hintsChanged();
}
QVariantMap KNotification::hints() const
{
return d->hints;
}
void KNotification::setHints(const QVariantMap &hints)
{
if (hints == d->hints) {
return;
}
d->needUpdate = true;
d->hints = hints;
if (d->id >= 0) {
d->updateTimer.start();
}
Q_EMIT hintsChanged();
}
QString KNotification::xdgActivationToken() const
{
return d->xdgActivationToken;
}
void KNotification::setWindow(QWindow *window)
{
if (window == d->window) {
return;
}
disconnect(d->window, &QWindow::activeChanged, this, &KNotification::slotWindowActiveChanged);
d->window = window;
connect(d->window, &QWindow::activeChanged, this, &KNotification::slotWindowActiveChanged);
}
void KNotification::slotWindowActiveChanged()
{
if (d->window->isActive() && (d->flags & CloseWhenWindowActivated)) {
close();
}
}
QWindow *KNotification::window() const
{
return d->window;
}
#include "moc_knotification.cpp"
@@ -0,0 +1,827 @@
/*
This file is part of the KDE libraries
SPDX-FileCopyrightText: 2005-2006 Olivier Goffart <ogoffart at kde.org>
SPDX-FileCopyrightText: 2013-2015 Martin Klapetek <mklapetek@kde.org>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#ifndef KNOTIFICATION_H
#define KNOTIFICATION_H
#include <knotifications_export.h>
#include <QList>
#include <QObject>
#include <QPair>
#include <QPixmap>
#include <QUrl>
#include <QVariant>
#include <QWindow>
#include <memory>
class KNotificationReplyAction;
class KNotificationAction;
class KNotificationActionPrivate;
/**
* @class KNotificationAction knotification.h KNotificationAction
*
* This class represents a notification. This can be a button on the notification
* popup, or triggered by clicking the notification popup itself.
*
* @since 6.0
*/
class KNOTIFICATIONS_EXPORT KNotificationAction : public QObject
{
Q_OBJECT
/**
* @copydoc label
*/
Q_PROPERTY(QString label READ label WRITE setLabel NOTIFY labelChanged)
public:
explicit KNotificationAction(QObject *parent = nullptr);
/**
* Creates an action with given label
* @param label The label for the action
*/
explicit KNotificationAction(const QString &label);
~KNotificationAction() override;
/**
* The user-facing label for the action
*/
QString label() const;
/**
* Set the user-facing label for the action
*/
void setLabel(const QString &label);
Q_SIGNALS:
/**
* Emitted when the user activates the action
*/
void activated();
/**
* Emitted when @p label changed.
*/
void labelChanged(const QString &label);
private:
friend class KNotification;
friend class NotifyByPortalPrivate;
friend class NotifyByPopup;
friend class NotifyBySnore;
friend class NotifyByAndroid;
void setId(const QString &id);
QString id() const;
std::unique_ptr<KNotificationActionPrivate> const d;
};
/**
* @class KNotification knotification.h KNotification
*
* KNotification is the main class for creating notifications.
*/
class KNOTIFICATIONS_EXPORT KNotification : public QObject
{
Q_OBJECT
/**
* @copydoc setEventId
* @since 5.88
*/
Q_PROPERTY(QString eventId READ eventId WRITE setEventId NOTIFY eventIdChanged)
/**
* @copydoc setTitle
* @since 5.88
*/
Q_PROPERTY(QString title READ title WRITE setTitle NOTIFY titleChanged)
/**
* @copydoc setText
* @since 5.88
*/
Q_PROPERTY(QString text READ text WRITE setText NOTIFY textChanged)
/**
* @copydoc setIconName
* @since 5.88
*/
Q_PROPERTY(QString iconName READ iconName WRITE setIconName NOTIFY iconNameChanged)
/**
* @copydoc setFlags
* @since 5.88
*/
Q_PROPERTY(NotificationFlags flags READ flags WRITE setFlags NOTIFY flagsChanged)
/**
* @copydoc setComponentName
* @since 5.88
*/
Q_PROPERTY(QString componentName READ componentName WRITE setComponentName NOTIFY componentNameChanged)
/**
* @copydoc setUrls
* @since 5.88
*/
Q_PROPERTY(QList<QUrl> urls READ urls WRITE setUrls NOTIFY urlsChanged)
/**
* @copydoc setUrgency
* @since 5.88
*/
Q_PROPERTY(Urgency urgency READ urgency WRITE setUrgency NOTIFY urgencyChanged)
/**
* @copydoc setAutoDelete
* @since 5.88
*/
Q_PROPERTY(bool autoDelete READ isAutoDelete WRITE setAutoDelete NOTIFY autoDeleteChanged)
/**
* @since 5.90
*/
Q_PROPERTY(QString xdgActivationToken READ xdgActivationToken NOTIFY xdgActivationTokenChanged)
/**
* @copydoc setHint
* @since 5.101
*/
Q_PROPERTY(QVariantMap hints READ hints WRITE setHints NOTIFY hintsChanged)
public:
/**
* @see NotificationFlags
*/
enum NotificationFlag {
/**
* The notification will be automatically closed after a timeout. (this is the default)
*/
CloseOnTimeout = 0x00,
/**
* The notification will NOT be automatically closed after a timeout.
* You will have to track the notification, and close it with the
* close function manually when the event is done, otherwise there will be a memory leak
*/
Persistent = 0x02,
/**
* The audio plugin will loop the sound until the notification is closed
*/
LoopSound = 0x08,
/**
* Sends a hint to Plasma to skip grouping for this notification
*
* @since 5.18
*/
SkipGrouping = 0x10,
/**
* The notification will be automatically closed if the window() becomes
* activated.
*
* You need to set a window using setWindow().
*
* @since 6.0
*/
CloseWhenWindowActivated = 0x20,
/**
* @internal
* The event is a standard kde event, and not an event of the application
*/
DefaultEvent = 0xF000,
};
/**
* Stores a combination of #NotificationFlag values.
*/
Q_DECLARE_FLAGS(NotificationFlags, NotificationFlag)
Q_FLAG(NotificationFlags)
/**
* default events you can use in the event function
*/
enum StandardEvent {
Notification,
Warning,
Error,
Catastrophe,
};
/**
* The urgency of a notification.
*
* @since 5.58
* @sa setUrgency
*/
enum Urgency {
DefaultUrgency = -1,
LowUrgency = 10,
NormalUrgency = 50,
HighUrgency = 70,
CriticalUrgency = 90,
};
Q_ENUM(Urgency)
/**
* Create a new notification.
*
* You have to use sendEvent to show the notification.
*
* The pointer is automatically deleted when the event is closed.
*
* @since 4.4
*
* @param eventId is the name of the event
* @param flags is a bitmask of NotificationFlag
* @param parent parent object
*/
explicit KNotification(const QString &eventId, NotificationFlags flags = CloseOnTimeout, QObject *parent = nullptr);
~KNotification() override;
/**
* @return the name of the event
*/
QString eventId() const;
/**
* Set the event id, if not already passed to the constructor.
* @since 5.88
*/
void setEventId(const QString &eventId);
/**
* @return the notification title
* @see setTitle
* @since 4.3
*/
QString title() const;
/**
* Set the title of the notification popup.
* If no title is set, the application name will be used.
*
* @param title The title of the notification
* @since 4.3
*/
void setTitle(const QString &title);
/**
* @return the notification text
* @see setText
*/
QString text() const;
/**
* Set the notification text that will appear in the popup.
*
* In Plasma workspace, the text is shown in a QML label which uses Text.StyledText,
* ie. it supports a small subset of HTML entities (mostly just formatting tags)
*
* If the notifications server does not advertise "body-markup" capability,
* all HTML tags are stripped before sending it to the server
*
* @param text The text to display in the notification popup
*/
void setText(const QString &text);
/**
* \return the icon shown in the popup
* \see setIconName
* \since 5.4
*/
QString iconName() const;
/**
* Set the icon that will be shown in the popup.
*
* @param icon the icon
* @since 5.4
*/
void setIconName(const QString &icon);
/**
* \return the pixmap shown in the popup
* \see setPixmap
*/
QPixmap pixmap() const;
/**
* Set the pixmap that will be shown in the popup. If you want to use an icon from the icon theme use setIconName instead.
*
* @param pix the pixmap
*/
void setPixmap(const QPixmap &pix);
/**
* @return the default action, or nullptr if none is set
* @since 6.0
*/
KNotificationAction *defaultAction() const;
/**
* Add a default action that will be triggered when the notification is
* activated (typically, by clicking on the notification popup). The default
* action typically raises a window belonging to the application that sent it.
*
* The string will be used as a label for the action, so ideally it should
* be wrapped in i18n() or tr() calls.
*
* The visual representation of actions depends on the notification server.
* In Plasma and Gnome desktops, the actions are performed by clicking on
* the notification popup, and the label is not presented to the user.
*
* Calling this overrides the current default action
*
* @since 6.0
*/
[[nodiscard]] KNotificationAction *addDefaultAction(const QString &label);
/**
* Add an action to the notification.
*
* The visual representation of actions depends
* on the notification server.
*
* @param label the user-visible label of the action
*
* @see KNotificationAction
*
* @since 6.0
*/
[[nodiscard]] KNotificationAction *addAction(const QString &label);
/**
* Removes all actions previously added by addAction()
* from the notification.
*
* @see addAction
*
* @since 6.0
*/
void clearActions();
/**
* @return the inline reply action.
* @since 5.81
*/
KNotificationReplyAction *replyAction() const;
/**
* @brief Add an inline reply action to the notification.
*
* On supported platforms this lets the user type a reply to a notification,
* such as replying to a chat message, from the notification popup, for example:
*
* @code{.cpp}
* KNotification *notification = new KNotification(QStringLiteral("notification"));
* ...
* auto replyAction = std::make_unique<KNotificationReplyAction>(i18nc("@action:button", "Reply"));
* replyAction->setPlaceholderText(i18nc("@info:placeholder", "Reply to Dave..."));
* QObject::connect(replyAction.get(), &KNotificationReplyAction::replied, [](const QString &text) {
* qDebug() << "you replied with" << text;
* });
* notification->setReplyAction(std::move(replyAction));
* @endcode
*
* @param replyAction the reply action to set
* @since 5.81
*/
void setReplyAction(std::unique_ptr<KNotificationReplyAction> replyAction);
/**
* @return the notification flags.
*/
NotificationFlags flags() const;
/**
* Set the notification flags.
* These must be set before calling sendEvent()
*/
void setFlags(const NotificationFlags &flags);
/**
* Returns the component name used to determine the location of the configuration file.
* @since 5.88
*/
QString componentName() const;
/**
* The componentData is used to determine the location of the config file.
*
* If no componentName is set, the app name is used by default
*
* @param componentName the new component name
*/
void setComponentName(const QString &componentName);
/**
* URLs associated with this notification
* @since 5.29
*/
QList<QUrl> urls() const;
/**
* Sets URLs associated with this notification
*
* For example, a screenshot application might want to provide the
* URL to the file that was just taken so the notification service
* can show a preview.
*
* @note This feature might not be supported by the user's notification service
*
* @param urls A list of URLs
* @since 5.29
*/
void setUrls(const QList<QUrl> &urls);
/**
* The urgency of the notification.
* @since 5.58
*/
Urgency urgency() const;
/**
* Sets the urgency of the notification.
*
* This defines the importance of the notification. For example,
* a track change in a media player would be a low urgency.
* "You have new mail" would be normal urgency. "Your battery level
* is low" would be a critical urgency.
*
* Use critical notifications with care as they might be shown even
* when giving a presentation or when notifications are turned off.
*
* @param urgency The urgency.
* @since 5.58
*/
void setUrgency(Urgency urgency);
/**
* Sets the window associated with this notification.
* This is relevant when using the CloseWhenWindowActivated flag.
*
* @since 6.0
*/
void setWindow(QWindow *window);
/**
* The window associated with this notification. nullptr by default.
* @return the window set by setWindow()
*
* @since 6.0
*/
QWindow *window() const;
/**
* @internal
* appname used for the D-Bus object
*/
QString appName() const;
/**
* Returns whether this notification object will be automatically deleted after closing.
* @since 5.88
*/
bool isAutoDelete() const;
/**
* Sets whether this notification object will be automatically deleted after closing.
* This is on by default for C++, and off by default for QML.
* @since 5.88
*/
void setAutoDelete(bool autoDelete);
/**
* Returns the activation token to use to activate a window.
* @since 5.90
*/
QString xdgActivationToken() const;
Q_SIGNALS:
/**
* Emitted when the notification is closed.
*
* Can be closed either by the user clicking the close button,
* the timeout running out or when an action was triggered.
*/
void closed();
/**
* The notification has been ignored
*/
void ignored();
/**
* Emitted when @c eventId changed.
* @since 5.88
*/
void eventIdChanged();
/**
* Emitted when @c title changed.
* @since 5.88
*/
void titleChanged();
/**
* Emitted when @c text changed.
* @since 5.88
*/
void textChanged();
/**
* Emitted when @c iconName changed.
* @since 5.88
*/
void iconNameChanged();
/**
* Emitted when @c defaultAction changed.
* @since 5.88
*/
void defaultActionChanged();
/**
* Emitted when @c actions changed.
* @since 5.88
*/
void actionsChanged();
/**
* Emitted when @p flags changed.
* @since 5.88
*/
void flagsChanged();
/**
* Emitted when @p componentName changed.
* @since 5.88
*/
void componentNameChanged();
/**
* Emitted when @p urls changed.
* @since 5.88
*/
void urlsChanged();
/**
* Emitted when @p urgency changed.
* @since 5.88
*/
void urgencyChanged();
/**
* Emitted when @p autoDelete changed.
* @since 5.88
*/
void autoDeleteChanged();
/**
* Emitted when @p xdgActivationToken changes.
* @since 5.90
*/
void xdgActivationTokenChanged();
/**
* Emitted when @p hints changes.
* @since 5.101
*/
void hintsChanged();
public Q_SLOTS:
/**
* Close the notification without activating it.
*
* This will delete the notification.
*/
void close();
/**
* Send the notification to the server.
*
* This will cause all the configured plugins to execute their actions on this notification
* (eg. a sound will play, a popup will show, a command will be executed etc).
*/
void sendEvent();
/**
* @since 5.57
* Adds a custom hint to the notification. Those are key-value pairs that can be interpreted by the respective notification backend to trigger additional,
* non-standard features.
* @param hint the hint's key
* @param value the hint's value
*/
Q_INVOKABLE void setHint(const QString &hint, const QVariant &value);
/**
* @since 5.57
* Returns the custom hints set by setHint()
*/
QVariantMap hints() const;
/**
* @since 5.101
* Set custom hints on the notification.
* @sa setHint
*/
void setHints(const QVariantMap &hints);
private:
friend class KNotificationManager;
friend class NotificationWrapper;
friend class NotifyByPopup;
friend class NotifyByPortal;
friend class NotifyByPortalPrivate;
friend class NotifyByExecute;
friend class NotifyBySnore;
friend class NotifyByAndroid;
friend class NotifyByMacOSNotificationCenter;
struct Private;
KNOTIFICATIONS_NO_EXPORT void slotWindowActiveChanged();
/**
* @brief Activate the action specified action
* If the action is zero, then the default action is activated
*/
KNOTIFICATIONS_NO_EXPORT void activate(const QString &action);
/**
* @internal
* the id given by the notification manager
*/
KNOTIFICATIONS_NO_EXPORT int id();
/**
* @internal
* update the texts, the icon, and the actions of one existing notification
*/
KNOTIFICATIONS_NO_EXPORT void update();
/**
* The notification will automatically be closed if all presentations are finished.
* if you want to show your own presentation in your application, you should use this
* function, so it will not be automatically closed when there is nothing to show.
*
* Don't forgot to deref, or the notification may be never closed if there is no timeout.
*
* @see deref
*/
KNOTIFICATIONS_NO_EXPORT void ref();
/**
* Remove a reference made with ref(). If the ref counter hits zero,
* the notification will be closed and deleted.
*
* @see ref
*/
KNOTIFICATIONS_NO_EXPORT void deref();
// Like setActions, but doesn't take ownership
void setActionsQml(QList<KNotificationAction *> actions);
void setDefaultActionQml(KNotificationAction *action);
QList<KNotificationAction *> actions() const;
static QString standardEventToEventId(StandardEvent event);
static QString standardEventToIconName(StandardEvent event);
std::unique_ptr<Private> const d;
public:
/**
* @brief emit an event
*
* This method creates the KNotification, setting every parameter, and fire the event.
* You don't need to call sendEvent
*
* A popup may be displayed or a sound may be played, depending the config.
*
* @return a KNotification . You may use that pointer to connect some signals or slot.
* the pointer is automatically deleted when the event is closed.
*
* @param eventId is the name of the event
* @param title is title of the notification to show in the popup.
* @param text is the text of the notification to show in the popup.
* @param pixmap is a picture which may be shown in the popup.
* @param flags is a bitmask of NotificationFlag
* @param componentName used to determine the location of the config file. by default, appname is used
* @since 4.4
*/
static KNotification *event(const QString &eventId,
const QString &title,
const QString &text,
const QPixmap &pixmap = QPixmap(),
const NotificationFlags &flags = CloseOnTimeout,
const QString &componentName = QString());
/**
* @brief emit a standard event
*
* @overload
*
* This will emit a standard event
*
* @param eventId is the name of the event
* @param text is the text of the notification to show in the popup.
* @param pixmap is a picture which may be shown in the popup.
* @param flags is a bitmask of NotificationFlag
* @param componentName used to determine the location of the config file. by default, plasma_workspace is used
*/
static KNotification *event(const QString &eventId,
const QString &text = QString(),
const QPixmap &pixmap = QPixmap(),
const NotificationFlags &flags = CloseOnTimeout,
const QString &componentName = QString());
/**
* @brief emit a standard event
*
* @overload
*
* This will emit a standard event
*
* @param eventId is the name of the event
* @param text is the text of the notification to show in the popup
* @param pixmap is a picture which may be shown in the popup
* @param flags is a bitmask of NotificationFlag
*/
static KNotification *
event(StandardEvent eventId, const QString &text = QString(), const QPixmap &pixmap = QPixmap(), const NotificationFlags &flags = CloseOnTimeout);
/**
* @brief emit a standard event
*
* @overload
*
* This will emit a standard event
*
* @param eventId is the name of the event
* @param title is title of the notification to show in the popup.
* @param text is the text of the notification to show in the popup
* @param pixmap is a picture which may be shown in the popup
* @param flags is a bitmask of NotificationFlag
* @since 4.4
*/
static KNotification *
event(StandardEvent eventId, const QString &title, const QString &text, const QPixmap &pixmap, const NotificationFlags &flags = CloseOnTimeout);
/**
* @brief emit a standard event with the possibility of setting an icon by icon name
*
* @overload
*
* This will emit a standard event
*
* @param eventId is the name of the event
* @param title is title of the notification to show in the popup.
* @param text is the text of the notification to show in the popup
* @param iconName a Freedesktop compatible icon name to be shown in the popup
* @param flags is a bitmask of NotificationFlag
* @param componentName used to determine the location of the config file. by default, plasma_workspace is used
* @since 5.4
*/
static KNotification *event(const QString &eventId,
const QString &title,
const QString &text,
const QString &iconName,
const NotificationFlags &flags = CloseOnTimeout,
const QString &componentName = QString());
/**
* @brief emit a standard event with the possibility of setting an icon by icon name
*
* @overload
*
* This will emit a standard event with a custom icon
*
* @param eventId the type of the standard (not app-defined) event
* @param title is title of the notification to show in the popup.
* @param text is the text of the notification to show in the popup
* @param iconName a Freedesktop compatible icon name to be shown in the popup
* @param flags is a bitmask of NotificationFlag
* @since 5.9
*/
static KNotification *
event(StandardEvent eventId, const QString &title, const QString &text, const QString &iconName, const NotificationFlags &flags = CloseOnTimeout);
/**
* @brief emit a standard event
*
* @overload
*
* This will emit a standard event with its standard icon
*
* @param eventId the type of the standard (not app-defined) event
* @param title is title of the notification to show in the popup.
* @param text is the text of the notification to show in the popup
* @param flags is a bitmask of NotificationFlag
* @since 5.9
*/
static KNotification *event(StandardEvent eventId, const QString &title, const QString &text, const NotificationFlags &flags = CloseOnTimeout);
/**
* This is a simple substitution for QApplication::beep()
*
* @param reason a short text explaining what has happened (may be empty)
*/
static void beep(const QString &reason = QString());
// prevent warning
using QObject::event;
};
Q_DECLARE_OPERATORS_FOR_FLAGS(KNotification::NotificationFlags)
#endif
@@ -0,0 +1,50 @@
/*
This file is part of the KDE libraries
SPDX-FileCopyrightText: 2005-2006 Olivier Goffart <ogoffart at kde.org>
SPDX-FileCopyrightText: 2013-2014 Martin Klapetek <mklapetek@kde.org>
code from KNotify/KNotifyClient
SPDX-FileCopyrightText: 1997 Christian Esken <esken@kde.org>
SPDX-FileCopyrightText: 2000 Charles Samuels <charles@kde.org>
SPDX-FileCopyrightText: 2000 Stefan Schimanski <1Stein@gmx.de>
SPDX-FileCopyrightText: 2000 Matthias Ettrich <ettrich@kde.org>
SPDX-FileCopyrightText: 2000 Waldo Bastian <bastian@kde.org>
SPDX-FileCopyrightText: 2000-2003 Carsten Pfeiffer <pfeiffer@kde.org>
SPDX-FileCopyrightText: 2005 Allan Sandfeld Jensen <kde@carewolf.com>
SPDX-License-Identifier: LGPL-2.0-only
*/
#ifndef KNOITIFICATION_P_H
#define KNOITIFICATION_P_H
#include <QTimer>
struct Q_DECL_HIDDEN KNotification::Private {
QString eventId;
int id = -1;
int ref = 0;
QString title;
QString text;
QString iconName;
KNotificationAction *defaultAction = nullptr;
QList<KNotificationAction *> actions;
bool ownsActions = true;
QString xdgActivationToken;
std::unique_ptr<KNotificationReplyAction> replyAction;
QPixmap pixmap;
NotificationFlags flags = KNotification::CloseOnTimeout;
QString componentName;
KNotification::Urgency urgency = KNotification::DefaultUrgency;
QVariantMap hints;
QTimer updateTimer;
bool needUpdate = false;
bool isNew = true;
bool autoDelete = true;
QWindow *window = nullptr;
int actionIdCounter = 1;
};
#endif
@@ -0,0 +1,327 @@
/*
This file is part of the KDE libraries
SPDX-FileCopyrightText: 2005 Olivier Goffart <ogoffart at kde.org>
SPDX-FileCopyrightText: 2013-2015 Martin Klapetek <mklapetek@kde.org>
SPDX-FileCopyrightText: 2017 Eike Hein <hein@kde.org>
SPDX-FileCopyrightText: 2022 Harald Sitter <sitter@kde.org>
SPDX-License-Identifier: LGPL-2.0-only
*/
#include "knotification.h"
#include "knotification_p.h"
#include "knotificationmanager_p.h"
#include <config-knotifications.h>
#include <QFileInfo>
#include <QHash>
#ifdef HAVE_DBUS
#include <QDBusConnection>
#include <QDBusConnectionInterface>
#endif
#include "knotificationplugin.h"
#include "knotificationreplyaction.h"
#include "knotifyconfig.h"
#if defined(Q_OS_ANDROID)
#include "notifybyandroid.h"
#elif defined(Q_OS_MACOS)
#include "notifybymacosnotificationcenter.h"
#elif defined(WITH_SNORETOAST)
#include "notifybysnore.h"
#elif defined(HAVE_DBUS)
#include "notifybypopup.h"
#include "notifybyportal.h"
#endif
#include "debug_p.h"
#if defined(HAVE_CANBERRA)
#include "notifybyaudio.h"
#endif
typedef QHash<QString, QString> Dict;
struct Q_DECL_HIDDEN KNotificationManager::Private {
QHash<int, KNotification *> notifications;
QHash<QString, KNotificationPlugin *> notifyPlugins;
QStringList dirtyConfigCache;
bool portalDBusServiceExists = false;
};
class KNotificationManagerSingleton
{
public:
KNotificationManager instance;
};
Q_GLOBAL_STATIC(KNotificationManagerSingleton, s_self)
KNotificationManager *KNotificationManager::self()
{
return &s_self()->instance;
}
KNotificationManager::KNotificationManager()
: d(new Private)
{
qDeleteAll(d->notifyPlugins);
d->notifyPlugins.clear();
#ifdef HAVE_DBUS
if (isInsideSandbox()) {
QDBusConnectionInterface *interface = QDBusConnection::sessionBus().interface();
d->portalDBusServiceExists = interface->isServiceRegistered(QStringLiteral("org.freedesktop.portal.Desktop"));
}
QDBusConnection::sessionBus().connect(QString(),
QStringLiteral("/Config"),
QStringLiteral("org.kde.knotification"),
QStringLiteral("reparseConfiguration"),
this,
SLOT(reparseConfiguration(QString)));
#endif
}
KNotificationManager::~KNotificationManager() = default;
KNotificationPlugin *KNotificationManager::pluginForAction(const QString &action)
{
KNotificationPlugin *plugin = d->notifyPlugins.value(action);
// We already loaded a plugin for this action.
if (plugin) {
return plugin;
}
auto addPlugin = [this](KNotificationPlugin *plugin) {
d->notifyPlugins[plugin->optionName()] = plugin;
connect(plugin, &KNotificationPlugin::finished, this, &KNotificationManager::notifyPluginFinished);
connect(plugin, &KNotificationPlugin::xdgActivationTokenReceived, this, &KNotificationManager::xdgActivationTokenReceived);
connect(plugin, &KNotificationPlugin::actionInvoked, this, &KNotificationManager::notificationActivated);
connect(plugin, &KNotificationPlugin::replied, this, &KNotificationManager::notificationReplied);
};
// Load plugin.
// We have a series of built-ins up first, and fall back to trying
// to instantiate an externally supplied plugin.
if (action == QLatin1String("Popup")) {
#if defined(Q_OS_ANDROID)
plugin = new NotifyByAndroid(this);
#elif defined(WITH_SNORETOAST)
plugin = new NotifyBySnore(this);
#elif defined(Q_OS_MACOS)
plugin = new NotifyByMacOSNotificationCenter(this);
#elif defined(HAVE_DBUS)
if (d->portalDBusServiceExists) {
plugin = new NotifyByPortal(this);
} else {
plugin = new NotifyByPopup(this);
}
#endif
addPlugin(plugin);
} else if (action == QLatin1String("Sound")) {
#if defined(HAVE_CANBERRA)
plugin = new NotifyByAudio(this);
addPlugin(plugin);
#endif
}
return plugin;
}
void KNotificationManager::notifyPluginFinished(KNotification *notification)
{
if (!notification || !d->notifications.contains(notification->id())) {
return;
}
notification->deref();
}
void KNotificationManager::notificationActivated(int id, const QString &actionId)
{
if (d->notifications.contains(id)) {
qCDebug(LOG_KNOTIFICATIONS) << id << " " << actionId;
KNotification *n = d->notifications[id];
n->activate(actionId);
// Resident actions delegate control over notification lifetime to the client
if (!n->hints().value(QStringLiteral("resident")).toBool()) {
close(id);
}
}
}
void KNotificationManager::xdgActivationTokenReceived(int id, const QString &token)
{
KNotification *n = d->notifications.value(id);
if (n) {
qCDebug(LOG_KNOTIFICATIONS) << "Token received for" << id << token;
n->d->xdgActivationToken = token;
Q_EMIT n->xdgActivationTokenChanged();
}
}
void KNotificationManager::notificationReplied(int id, const QString &text)
{
if (KNotification *n = d->notifications.value(id)) {
if (auto *replyAction = n->replyAction()) {
// cannot really send out a "activate inline-reply" signal from plugin to manager
// so we instead assume empty reply is not supported and means normal invocation
if (text.isEmpty() && replyAction->fallbackBehavior() == KNotificationReplyAction::FallbackBehavior::UseRegularAction) {
Q_EMIT replyAction->activated();
} else {
Q_EMIT replyAction->replied(text);
}
close(id);
}
}
}
void KNotificationManager::notificationClosed()
{
KNotification *notification = qobject_cast<KNotification *>(sender());
if (!notification) {
return;
}
// We cannot do d->notifications.find(notification->id()); here because the
// notification->id() is -1 or -2 at this point, so we need to look for value
for (auto iter = d->notifications.begin(); iter != d->notifications.end(); ++iter) {
if (iter.value() == notification) {
d->notifications.erase(iter);
break;
}
}
}
void KNotificationManager::close(int id)
{
if (d->notifications.contains(id)) {
KNotification *n = d->notifications.value(id);
qCDebug(LOG_KNOTIFICATIONS) << "Closing notification" << id;
// Find plugins that are actually acting on this notification
// call close() only on those, otherwise each KNotificationPlugin::close()
// will call finish() which may close-and-delete the KNotification object
// before it finishes calling close on all the other plugins.
// For example: Action=Popup is a single actions but there is 5 loaded
// plugins, calling close() on the second would already close-and-delete
// the notification
KNotifyConfig notifyConfig(n->appName(), n->eventId());
QString notifyActions = notifyConfig.readEntry(QStringLiteral("Action"));
const auto listActions = notifyActions.split(QLatin1Char('|'));
for (const QString &action : listActions) {
if (!d->notifyPlugins.contains(action)) {
qCDebug(LOG_KNOTIFICATIONS) << "No plugin for action" << action;
continue;
}
d->notifyPlugins[action]->close(n);
}
}
}
void KNotificationManager::notify(KNotification *n)
{
KNotifyConfig notifyConfig(n->appName(), n->eventId());
if (d->dirtyConfigCache.contains(n->appName())) {
notifyConfig.reparseSingleConfiguration(n->appName());
d->dirtyConfigCache.removeOne(n->appName());
}
if (!notifyConfig.isValid()) {
qCWarning(LOG_KNOTIFICATIONS) << "No event config could be found for event id" << n->eventId() << "under notifyrc file for app" << n->appName();
}
const QString notifyActions = notifyConfig.readEntry(QStringLiteral("Action"));
if (notifyActions.isEmpty() || notifyActions == QLatin1String("None")) {
// this will cause KNotification closing itself fast
n->ref();
n->deref();
return;
}
d->notifications.insert(n->id(), n);
// TODO KF6 d-pointer KNotifyConfig and add this there
if (n->urgency() == KNotification::DefaultUrgency) {
const QString urgency = notifyConfig.readEntry(QStringLiteral("Urgency"));
if (urgency == QLatin1String("Low")) {
n->setUrgency(KNotification::LowUrgency);
} else if (urgency == QLatin1String("Normal")) {
n->setUrgency(KNotification::NormalUrgency);
} else if (urgency == QLatin1String("High")) {
n->setUrgency(KNotification::HighUrgency);
} else if (urgency == QLatin1String("Critical")) {
n->setUrgency(KNotification::CriticalUrgency);
}
}
const auto actionsList = notifyActions.split(QLatin1Char('|'));
// Make sure all plugins can ref the notification
// otherwise a plugin may finish and deref before everyone got a chance to ref
for (const QString &action : actionsList) {
KNotificationPlugin *notifyPlugin = pluginForAction(action);
if (!notifyPlugin) {
qCDebug(LOG_KNOTIFICATIONS) << "No plugin for action" << action;
continue;
}
n->ref();
}
for (const QString &action : actionsList) {
KNotificationPlugin *notifyPlugin = pluginForAction(action);
if (!notifyPlugin) {
qCDebug(LOG_KNOTIFICATIONS) << "No plugin for action" << action;
continue;
}
qCDebug(LOG_KNOTIFICATIONS) << "Calling notify on" << notifyPlugin->optionName();
notifyPlugin->notify(n, notifyConfig);
}
connect(n, &KNotification::closed, this, &KNotificationManager::notificationClosed);
}
void KNotificationManager::update(KNotification *n)
{
KNotifyConfig notifyConfig(n->appName(), n->eventId());
for (KNotificationPlugin *p : std::as_const(d->notifyPlugins)) {
p->update(n, notifyConfig);
}
}
void KNotificationManager::reemit(KNotification *n)
{
notify(n);
}
void KNotificationManager::reparseConfiguration(const QString &app)
{
if (!d->dirtyConfigCache.contains(app)) {
d->dirtyConfigCache << app;
}
}
bool KNotificationManager::isInsideSandbox()
{
// logic is taken from KSandbox::isInside()
static const bool isFlatpak = QFileInfo::exists(QStringLiteral("/.flatpak-info"));
static const bool isSnap = qEnvironmentVariableIsSet("SNAP");
return isFlatpak || isSnap;
}
#include "moc_knotificationmanager_p.cpp"
@@ -0,0 +1,74 @@
/*
This file is part of the KDE libraries
SPDX-FileCopyrightText: 2005 Olivier Goffart <ogoffart at kde.org>
SPDX-FileCopyrightText: 2006 Thiago Macieira <thiago@kde.org>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#ifndef KNOTIFICATIONMANAGER_H
#define KNOTIFICATIONMANAGER_H
#include <knotification.h>
#include <memory>
class KNotification;
class QPixmap;
class KNotificationPlugin;
/**
* @internal
* @author Olivier Goffart
*/
class KNotificationManager : public QObject
{
Q_OBJECT
public:
static KNotificationManager *self();
~KNotificationManager() override;
KNotificationPlugin *pluginForAction(const QString &action);
/**
* send the dbus call to the knotify server
*/
void notify(KNotification *n);
/**
* send the close dcop call to the knotify server for the notification with the identifier @p id .
* And remove the notification from the internal map
* @param id the id of the notification
* @param force if false, only close registered notification
*/
void close(int id);
/**
* update one notification text and pixmap and actions
*/
void update(KNotification *n);
/**
* re-emit the notification
*/
void reemit(KNotification *n);
private Q_SLOTS:
void notificationClosed();
void xdgActivationTokenReceived(int id, const QString &token);
void notificationActivated(int id, const QString &action);
void notificationReplied(int id, const QString &text);
void notifyPluginFinished(KNotification *notification);
void reparseConfiguration(const QString &app);
private:
bool isInsideSandbox();
struct Private;
std::unique_ptr<Private> const d;
KNotificationManager();
friend class KNotificationManagerSingleton;
};
#endif
@@ -0,0 +1,50 @@
/*
SPDX-FileCopyrightText: 2023 Volker Krause <vkrause@kde.org>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include "knotificationpermission.h"
#include <qnamespace.h>
#ifdef Q_OS_ANDROID
#include <QtCore/private/qandroidextras_p.h>
Qt::PermissionStatus KNotificationPermission::checkPermission()
{
if (QtAndroidPrivate::androidSdkVersion() < 33) {
return Qt::PermissionStatus::Granted;
}
return QtAndroidPrivate::checkPermission(QStringLiteral("android.permission.POST_NOTIFICATIONS")).result() == QtAndroidPrivate::PermissionResult::Authorized
? Qt::PermissionStatus::Granted
: Qt::PermissionStatus::Denied;
}
void KNotificationPermission::requestPermission(QObject *context, const std::function<void(Qt::PermissionStatus)> &callback)
{
if (QtAndroidPrivate::androidSdkVersion() < 33) {
callback(Qt::PermissionStatus::Granted);
}
QtAndroidPrivate::requestPermission(QStringLiteral("android.permission.POST_NOTIFICATIONS"))
.then(context, [callback, context](QtAndroidPrivate::PermissionResult res) {
QMetaObject::invokeMethod(
context,
[res, callback]() {
callback(res == QtAndroidPrivate::PermissionResult::Authorized ? Qt::PermissionStatus::Granted : Qt::PermissionStatus::Denied);
},
Qt::QueuedConnection);
});
}
#else
Qt::PermissionStatus KNotificationPermission::checkPermission()
{
return Qt::PermissionStatus::Granted;
}
void KNotificationPermission::requestPermission([[maybe_unused]] QObject *context, const std::function<void(Qt::PermissionStatus)> &callback)
{
callback(Qt::PermissionStatus::Granted);
}
#endif
@@ -0,0 +1,31 @@
/*
SPDX-FileCopyrightText: 2023 Volker Krause <vkrause@kde.org>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#ifndef KNOTIFICATIONPERMISSION_H
#define KNOTIFICATIONPERMISSION_H
#include <knotifications_export.h>
#include <qnamespace.h>
#include <functional>
/** Check or request permissions to show notifications on platforms where
* that is necessary.
*
* @since 6.0
*/
namespace KNotificationPermission
{
/** Check if the current application has permissions to show notifications. */
KNOTIFICATIONS_EXPORT Qt::PermissionStatus checkPermission();
/** Request notification permissions. */
KNOTIFICATIONS_EXPORT void requestPermission(QObject *context, const std::function<void(Qt::PermissionStatus)> &callback);
}
#endif
@@ -0,0 +1,40 @@
/*
SPDX-FileCopyrightText: 2005-2006 Olivier Goffart <ogoffart at kde.org>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#include "knotificationplugin.h"
class KNotificationPluginPrivate
{
};
KNotificationPlugin::KNotificationPlugin(QObject *parent, const QVariantList &args)
: QObject(parent)
, d(nullptr)
{
Q_UNUSED(args);
}
KNotificationPlugin::~KNotificationPlugin()
{
}
void KNotificationPlugin::update(KNotification *notification, const KNotifyConfig &notifyConfig)
{
Q_UNUSED(notification);
Q_UNUSED(notifyConfig);
}
void KNotificationPlugin::close(KNotification *notification)
{
Q_EMIT finished(notification);
}
void KNotificationPlugin::finish(KNotification *notification)
{
Q_EMIT finished(notification);
}
#include "moc_knotificationplugin.cpp"
@@ -0,0 +1,117 @@
/*
SPDX-FileCopyrightText: 2005-2006 Olivier Goffart <ogoffart at kde.org>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#ifndef KNOTIFICATIONPLUGIN_H
#define KNOTIFICATIONPLUGIN_H
#include <QObject>
#include <QTextDocumentFragment>
#include <memory>
class KNotification;
class KNotificationPluginPrivate;
class KNotifyConfig;
/**
* @class KNotificationPlugin knotificationplugin.h KNotificationPlugin
*
* @brief abstract class for KNotification actions
*
* A KNotificationPlugin is responsible of notification presentation.
* You can subclass it to have your own presentation of a notification.
*
* You should reimplement the KNotificationPlugin::notify method to display the notification.
*
* Porting from KF5 to KF6:
*
* The signature of the virtual method
* KNotificationPlugin::notify(KNotification *notification, KNotifyConfig *)
* was changed to
* KNotificationPlugin::notify(KNotification *notification, const KNotifyConfig &).
*
* The signature of the virtual method
* KNotificationPlugin::update(KNotification *notification, KNotifyConfig *)
* was changed to
* KNotificationPlugin::update(KNotification *notification, const KNotifyConfig &).
*
* @author Olivier Goffart <ogoffart at kde.org>
*/
class KNotificationPlugin : public QObject
{
Q_OBJECT
public:
KNotificationPlugin(QObject *parent = nullptr, const QVariantList &args = QVariantList());
~KNotificationPlugin() override;
/**
* @brief return the name of this plugin.
*
* this is the name that should appear in the .notifyrc file,
* in the field Action=... if a notification is set to use this plugin
*/
virtual QString optionName() = 0;
/**
* This function is called when the notification is sent.
* (or re-sent)
* You should implement this function to display a notification
*
* for each call to this function (even for re-notification), you MUST call finish(KNotification*)
*
* @param notification is the KNotification object
* @param notifyConfig is the configuration of the notification
*/
virtual void notify(KNotification *notification, const KNotifyConfig &notifyConfig) = 0;
/**
* This function is called when the notification has changed (such as the text or the icon)
*/
virtual void update(KNotification *notification, const KNotifyConfig &notifyConfig);
/**
* This function is called when the notification has been closed
*/
virtual void close(KNotification *notification);
protected:
/**
* emit the finished signal
* you MUST call this function for each call to notify(), even if you do nothing there
*
* call it when the presentation is finished (because the user closed the popup or the sound is finished)
*
* If your presentation is synchronous, you can even call this function from the notify() call itself
*/
void finish(KNotification *notification);
static inline QString stripRichText(const QString &s)
{
return QTextDocumentFragment::fromHtml(s).toPlainText();
}
Q_SIGNALS:
/**
* the presentation is finished.
*/
void finished(KNotification *notification);
/**
* emit this signal if one action was invoked
* @param id is the id of the notification
* @param action is the action number. zero for the default action
*/
void actionInvoked(int id, const QString &action);
void xdgActivationTokenReceived(int id, const QString &token);
void replied(int id, const QString &text);
private:
std::unique_ptr<KNotificationPluginPrivate> const d;
};
#endif
@@ -0,0 +1,96 @@
/*
This file is part of the KDE Frameworks
SPDX-FileCopyrightText: 2021 Kai Uwe Broulik <kde@broulik.de>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include "knotificationreplyaction.h"
#include <QString>
class KNotificationReplyActionPrivate
{
public:
QString label;
QString placeholderText;
QString submitButtonText;
QString submitButtonIconName;
KNotificationReplyAction::FallbackBehavior fallbackBehavior = KNotificationReplyAction::FallbackBehavior::HideAction;
};
KNotificationReplyAction::KNotificationReplyAction(const QString &label)
: QObject()
, d(new KNotificationReplyActionPrivate)
{
d->label = label;
}
KNotificationReplyAction::~KNotificationReplyAction() = default;
QString KNotificationReplyAction::label() const
{
return d->label;
}
void KNotificationReplyAction::setLabel(const QString &label)
{
if (d->label != label) {
d->label = label;
Q_EMIT labelChanged();
}
}
QString KNotificationReplyAction::placeholderText() const
{
return d->placeholderText;
}
void KNotificationReplyAction::setPlaceholderText(const QString &placeholderText)
{
if (d->placeholderText != placeholderText) {
d->placeholderText = placeholderText;
Q_EMIT placeholderTextChanged();
}
}
QString KNotificationReplyAction::submitButtonText() const
{
return d->submitButtonText;
}
void KNotificationReplyAction::setSubmitButtonText(const QString &submitButtonText)
{
if (d->submitButtonText != submitButtonText) {
d->submitButtonText = submitButtonText;
Q_EMIT submitButtonTextChanged();
}
}
QString KNotificationReplyAction::submitButtonIconName() const
{
return d->submitButtonIconName;
}
void KNotificationReplyAction::setSubmitButtonIconName(const QString &submitButtonIconName)
{
if (d->submitButtonIconName != submitButtonIconName) {
d->submitButtonIconName = submitButtonIconName;
Q_EMIT submitButtonIconNameChanged();
}
}
KNotificationReplyAction::FallbackBehavior KNotificationReplyAction::fallbackBehavior() const
{
return d->fallbackBehavior;
}
void KNotificationReplyAction::setFallbackBehavior(FallbackBehavior fallbackBehavior)
{
if (d->fallbackBehavior != fallbackBehavior) {
d->fallbackBehavior = fallbackBehavior;
Q_EMIT fallbackBehaviorChanged();
}
}
#include "moc_knotificationreplyaction.cpp"
@@ -0,0 +1,180 @@
/*
This file is part of the KDE Frameworks
SPDX-FileCopyrightText: 2021 Kai Uwe Broulik <kde@broulik.de>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#ifndef KNOTIFICATIONREPLYACTION_H
#define KNOTIFICATIONREPLYACTION_H
#include <knotifications_export.h>
#include <QObject>
#include <memory>
class QString;
class KNotificationReplyActionPrivate;
/**
* @class KNotificationReplyAction knotificationreplyaction.h KNotificationReplyAction
*
* @brief An inline reply action
*
* This class represents an inline reply action, which lets the user type a
* reply to a chat message or email in the notification popup.
*/
class KNOTIFICATIONS_EXPORT KNotificationReplyAction : public QObject
{
Q_OBJECT
/**
* @copydoc label
* @since 5.88
*/
Q_PROPERTY(QString label READ label WRITE setLabel NOTIFY labelChanged)
/**
* @copydoc placeholderText
* @since 5.88
*/
Q_PROPERTY(QString placeholderText READ placeholderText WRITE setPlaceholderText NOTIFY placeholderTextChanged)
/**
* @copydoc submitButtonText
* @since 5.88
*/
Q_PROPERTY(QString submitButtonText READ submitButtonText WRITE setSubmitButtonText NOTIFY submitButtonTextChanged)
/**
* @copydoc submitButtonIconName
* @since 5.88
*/
Q_PROPERTY(QString submitButtonIconName READ submitButtonIconName WRITE setSubmitButtonIconName NOTIFY submitButtonIconNameChanged)
/**
* @copydoc fallbackBehavior
* @since 5.88
*/
Q_PROPERTY(FallbackBehavior fallbackBehavior READ fallbackBehavior WRITE setFallbackBehavior NOTIFY fallbackBehaviorChanged)
public:
/**
* Creates a inline reply action with given label
* @param label The label for the action
*/
explicit KNotificationReplyAction(const QString &label);
/**
* Destroys this inline reply action
*/
~KNotificationReplyAction() override;
/**
* The label for the action button
*/
QString label() const;
/**
* Set the label for the action button
*/
void setLabel(const QString &label);
/**
* The placeholder text for the inline reply text field
*/
QString placeholderText() const;
/**
* Set the placeholder text for the inline reply text field, for example "Reply to Konqi..."
*/
void setPlaceholderText(const QString &placeholderText);
/**
* The label for the button to send the typed reply
*/
QString submitButtonText() const;
/**
* Set the label for the button to send the typed reply
*/
void setSubmitButtonText(const QString &submitButtonText);
/**
* The icon name for the button to send the typed reply
*/
QString submitButtonIconName() const;
/**
* Set the icon name for the button to send the typed reply
*/
void setSubmitButtonIconName(const QString &submitButtonIconName);
/**
* Behavior when the notification server does not support inline replies
*/
enum class FallbackBehavior {
/**
* Don't add the reply action (default)
*/
HideAction,
/**
* Add the reply action as regular button
*
* Use this if you want to provide your own reply functionality
*
* @note The @c activated signal is emitted instead of @c replied!
*/
UseRegularAction,
};
Q_ENUM(FallbackBehavior)
/**
* Gets the fallback behavior when the notification server does not support inline replies
*/
FallbackBehavior fallbackBehavior() const;
/**
* Set the fallback behavior for when the notification server does not support inline replies
*/
void setFallbackBehavior(FallbackBehavior fallbackBehavior);
Q_SIGNALS:
/**
* Emitted when the user has submitted a reply
*
* @note This is never emitted when the notification server does not support inline replies
*
* @param text The text the user entered
*/
void replied(const QString &text);
/**
* Emitted when the user clicks the reply fallback button
*
* @note This is emitted when the notification server does not support inline replies
* and fallbackBehavior is set to @c UseRegularAction.
*/
void activated();
/**
* Emitted when @p label changed.
* @since 5.88
*/
void labelChanged();
/**
* Emitted when @p placeholderText changed.
* @since 5.88
*/
void placeholderTextChanged();
/**
* Emitted when @p submitButtonText changed.
* @since 5.88
*/
void submitButtonTextChanged();
/**
* Emitted when @p submitButtonIconName changed.
* @since 5.88
*/
void submitButtonIconNameChanged();
/**
* Emitted when @p fallbackBehavior changed.
* @since 5.88
*/
void fallbackBehaviorChanged();
private:
std::unique_ptr<KNotificationReplyActionPrivate> const d;
};
#endif // KNOTIFICATIONREPLYACTION_H
@@ -0,0 +1,5 @@
<RCC>
<qresource prefix="/knotifications6">
<file>android_defaults.notifyrc</file>
</qresource>
</RCC>
@@ -0,0 +1,141 @@
/*
SPDX-FileCopyrightText: 2005-2009 Olivier Goffart <ogoffart at kde.org>
SPDX-FileCopyrightText: 2023 Kai Uwe Broulik <kde@broulik.de>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#include "knotifyconfig.h"
#include <KConfigGroup>
#include <KSharedConfig>
#include <QCache>
#include <QStandardPaths>
typedef QCache<QString, KSharedConfig::Ptr> ConfigCache;
Q_GLOBAL_STATIC_WITH_ARGS(ConfigCache, static_cache, (15))
class KNotifyConfigPrivate : public QSharedData
{
public:
QString readEntry(const QString &group, const QString &key, bool path) const;
QString applicationName;
QString eventId;
KSharedConfig::Ptr eventsFile;
KSharedConfig::Ptr configFile;
};
QString KNotifyConfigPrivate::readEntry(const QString &group, const QString &key, bool path) const
{
if (configFile->hasGroup(group)) {
KConfigGroup cg(configFile, group);
const QString value = path ? cg.readPathEntry(key, QString()) : cg.readEntry(key, QString());
if (!value.isNull()) {
return value;
}
}
if (eventsFile->hasGroup(group)) {
KConfigGroup cg(eventsFile, group);
const QString value = path ? cg.readPathEntry(key, QString()) : cg.readEntry(key, QString());
if (!value.isNull()) {
return value;
}
}
return QString();
}
static KSharedConfig::Ptr retrieve_from_cache(const QString &filename, QStandardPaths::StandardLocation type = QStandardPaths::GenericConfigLocation)
{
QCache<QString, KSharedConfig::Ptr> &cache = *static_cache;
if (cache.contains(filename)) {
return *cache[filename];
}
KSharedConfig::Ptr m = KSharedConfig::openConfig(filename, KConfig::NoGlobals, type);
// also search for event config files in qrc resources
if (type == QStandardPaths::GenericDataLocation) {
m->addConfigSources({QStringLiteral(":/") + filename});
}
cache.insert(filename, new KSharedConfig::Ptr(m));
return m;
}
void KNotifyConfig::reparseConfiguration()
{
QCache<QString, KSharedConfig::Ptr> &cache = *static_cache;
const auto listFiles = cache.keys();
for (const QString &filename : listFiles) {
(*cache[filename])->reparseConfiguration();
}
}
void KNotifyConfig::reparseSingleConfiguration(const QString &app)
{
QCache<QString, KSharedConfig::Ptr> &cache = *static_cache;
const QString appCacheKey = app + QStringLiteral(".notifyrc");
if (cache.contains(appCacheKey)) {
(*cache[appCacheKey])->reparseConfiguration();
}
}
KNotifyConfig::KNotifyConfig(const QString &applicationName, const QString &eventId)
: d(new KNotifyConfigPrivate)
{
d->applicationName = applicationName;
d->eventId = eventId;
d->eventsFile = retrieve_from_cache(QLatin1String("knotifications6/") + applicationName + QLatin1String(".notifyrc"), QStandardPaths::GenericDataLocation);
d->configFile = retrieve_from_cache(applicationName + QStringLiteral(".notifyrc"));
}
KNotifyConfig::KNotifyConfig(const KNotifyConfig &other)
: d(other.d)
{
}
KNotifyConfig &KNotifyConfig::operator=(const KNotifyConfig &other)
{
d = other.d;
return *this;
}
KNotifyConfig::~KNotifyConfig() = default;
QString KNotifyConfig::applicationName() const
{
return d->applicationName;
}
QString KNotifyConfig::eventId() const
{
return d->eventId;
}
bool KNotifyConfig::isValid() const
{
const QString group = QLatin1String("Event/") + d->eventId;
return d->configFile->hasGroup(group) || d->eventsFile->hasGroup(group);
}
QString KNotifyConfig::readGlobalEntry(const QString &key) const
{
return d->readEntry(QStringLiteral("Global"), key, false);
}
QString KNotifyConfig::readEntry(const QString &key) const
{
const QString group = QLatin1String("Event/") + d->eventId;
return d->readEntry(group, key, false);
}
QString KNotifyConfig::readPathEntry(const QString &key) const
{
const QString group = QLatin1String("Event/") + d->eventId;
return d->readEntry(group, key, true);
}
@@ -0,0 +1,91 @@
/*
SPDX-FileCopyrightText: 2005-2009 Olivier Goffart <ogoffart at kde.org>
SPDX-FileCopyrightText: 2023 Kai Uwe Broulik <kde@broulik.de>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#ifndef KNOTIFYCONFIG_H
#define KNOTIFYCONFIG_H
#include "knotifications_export.h"
#include <QSharedDataPointer>
class KNotifyConfigPrivate;
/**
* @class KNotifyConfig knotifyconfig.h KNotifyConfig
*
* Represent the configuration for an event
*
* @author Olivier Goffart <ogoffart@kde.org>
* @author Kai Uwe Broulik <kde@broulik.de>
*/
class KNOTIFICATIONS_EXPORT KNotifyConfig
{
public:
/**
* Creates a notify config for the given application name and event id
* @param applicationName The application name, typically the name of the notifyrc file without its extension.
* @param eventId The notification event ID, i.e. the part after Event/ in its notifyrc file.
*/
KNotifyConfig(const QString &applicationName, const QString &eventId);
~KNotifyConfig();
KNotifyConfig(const KNotifyConfig &other);
KNotifyConfig &operator=(const KNotifyConfig &other);
/**
* the name of the application that triggered the notification
*/
QString applicationName() const;
/**
* the name of the notification
*/
QString eventId() const;
/**
* Whether there exists an event with the given id under the given application name.
*/
bool isValid() const;
/**
* @return entry from the relevant Global notifyrc config group
*
* This will return the configuration from the user for the given key.
* It first look into the user config file, and then in the global config file.
*
* return a null string if the entry doesn't exist
*/
QString readGlobalEntry(const QString &key) const;
/**
* @return entry from the relevant Event/ notifyrc config group
*
* This will return the configuration from the user for the given key.
* It first look into the user config file, and then in the global config file.
*
* return a null string if the entry doesn't exist
*/
QString readEntry(const QString &key) const;
/**
* @return path entry from the relevant Event/ notifyrc config group
*
* This will return the configuration from the user for the given key
* and interpret it as a path.
*/
QString readPathEntry(const QString &key) const;
/**
* reparse the cached configs. to be used when the config may have changed
*/
static void reparseConfiguration();
static void reparseSingleConfiguration(const QString &app);
private:
QSharedDataPointer<KNotifyConfigPrivate> d;
};
#endif
@@ -0,0 +1,209 @@
/*
SPDX-FileCopyrightText: 2018 Volker Krause <vkrause@kde.org>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include "notifybyandroid.h"
#include "debug_p.h"
#include "knotification.h"
#include "knotificationreplyaction.h"
#include "knotifyconfig.h"
#include <QCoreApplication>
#include <QJniEnvironment>
#include <QBuffer>
#include <QIcon>
static NotifyByAndroid *s_instance = nullptr;
static void notificationFinished(JNIEnv *env, jobject that, jint notificationId)
{
Q_UNUSED(env);
Q_UNUSED(that);
if (s_instance) {
s_instance->notificationFinished(notificationId);
}
}
static void notificationActionInvoked(JNIEnv *env, jobject that, jint id, jstring action)
{
Q_UNUSED(env);
Q_UNUSED(that);
if (s_instance) {
const char *str = env->GetStringUTFChars(action, nullptr);
s_instance->notificationActionInvoked(id, QString::fromUtf8(str));
env->ReleaseStringUTFChars(action, str);
}
}
static void notificationInlineReply(JNIEnv *env, jobject that, jint id, jstring text)
{
Q_UNUSED(that);
if (s_instance) {
const char *str = env->GetStringUTFChars(text, nullptr);
s_instance->notificationInlineReply(id, QString::fromUtf8(str));
env->ReleaseStringUTFChars(text, str);
}
}
static const JNINativeMethod methods[] = {
{"notificationFinished", "(I)V", (void *)notificationFinished},
{"notificationActionInvoked", "(ILjava/lang/String;)V", (void *)notificationActionInvoked},
{"notificationInlineReply", "(ILjava/lang/String;)V", (void *)notificationInlineReply},
};
KNOTIFICATIONS_EXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *)
{
static bool initialized = false;
if (initialized) {
return JNI_VERSION_1_4;
}
initialized = true;
JNIEnv *env = nullptr;
if (vm->GetEnv((void **)&env, JNI_VERSION_1_4) != JNI_OK) {
qCWarning(LOG_KNOTIFICATIONS) << "Failed to get JNI environment.";
return -1;
}
jclass cls = env->FindClass("org/kde/knotifications/NotifyByAndroid");
if (env->RegisterNatives(cls, methods, sizeof(methods) / sizeof(JNINativeMethod)) < 0) {
qCWarning(LOG_KNOTIFICATIONS) << "Failed to register native functions.";
return -1;
}
return JNI_VERSION_1_4;
}
NotifyByAndroid::NotifyByAndroid(QObject *parent)
: KNotificationPlugin(parent)
{
s_instance = this;
QJniObject context = QNativeInterface::QAndroidApplication::context();
m_backend = QJniObject("org/kde/knotifications/NotifyByAndroid", "(Landroid/content/Context;)V", context.object<jobject>());
}
NotifyByAndroid::~NotifyByAndroid()
{
s_instance = nullptr;
}
QString NotifyByAndroid::optionName()
{
return QStringLiteral("Popup");
}
void NotifyByAndroid::notify(KNotification *notification, const KNotifyConfig &notifyConfig)
{
Q_UNUSED(notifyConfig);
// HACK work around that notification->id() is only populated after returning from here
// note that config will be invalid at that point, so we can't pass that along
QMetaObject::invokeMethod(
this,
[this, notification]() {
notifyDeferred(notification);
},
Qt::QueuedConnection);
}
QJniObject NotifyByAndroid::createAndroidNotification(KNotification *notification, const KNotifyConfig &notifyConfig) const
{
QJniEnvironment env;
QJniObject n("org/kde/knotifications/KNotification", "()V");
n.setField("id", notification->id());
n.setField("text", QJniObject::fromString(stripRichText(notification->text())).object<jstring>());
n.setField("richText", QJniObject::fromString(notification->text()).object<jstring>());
n.setField("title", QJniObject::fromString(notification->title()).object<jstring>());
n.setField("urgency", (jint)(notification->urgency() == KNotification::DefaultUrgency ? KNotification::HighUrgency : notification->urgency()));
n.setField("visibility", QJniObject::fromString(notification->hints().value(QLatin1String("x-kde-visibility")).toString().toLower()).object<jstring>());
n.setField("channelId", QJniObject::fromString(notification->eventId()).object<jstring>());
n.setField("channelName", QJniObject::fromString(notifyConfig.readEntry(QLatin1String("Name"))).object<jstring>());
n.setField("channelDescription", QJniObject::fromString(notifyConfig.readEntry(QLatin1String("Comment"))).object<jstring>());
if ((notification->flags() & KNotification::SkipGrouping) == 0) {
n.setField("group", QJniObject::fromString(notification->eventId()).object<jstring>());
}
// icon
QPixmap pixmap;
if (!notification->iconName().isEmpty()) {
const auto icon = QIcon::fromTheme(notification->iconName());
pixmap = icon.pixmap(32, 32);
} else {
pixmap = notification->pixmap();
}
QByteArray iconData;
QBuffer buffer(&iconData);
buffer.open(QIODevice::WriteOnly);
pixmap.save(&buffer, "PNG");
auto jIconData = env->NewByteArray(iconData.length());
env->SetByteArrayRegion(jIconData, 0, iconData.length(), reinterpret_cast<const jbyte *>(iconData.constData()));
n.callMethod<void>("setIconFromData", "([BI)V", jIconData, iconData.length());
env->DeleteLocalRef(jIconData);
// actions
const auto actions = notification->actions();
for (const KNotificationAction *action : actions) {
n.callMethod<void>("addAction",
"(Ljava/lang/String;Ljava/lang/String;)V",
QJniObject::fromString(action->id()).object<jstring>(),
QJniObject::fromString(action->label()).object<jstring>());
}
if (notification->replyAction()) {
n.setField("inlineReplyLabel", QJniObject::fromString(notification->replyAction()->label()).object<jstring>());
n.setField("inlineReplyPlaceholder", QJniObject::fromString(notification->replyAction()->placeholderText()).object<jstring>());
}
return n;
}
void NotifyByAndroid::notifyDeferred(KNotification *notification)
{
KNotifyConfig config(notification->appName(), notification->eventId());
const auto n = createAndroidNotification(notification, config);
m_notifications.insert(notification->id(), notification);
m_backend.callMethod<void>("notify", "(Lorg/kde/knotifications/KNotification;)V", n.object<jobject>());
}
void NotifyByAndroid::update(KNotification *notification, const KNotifyConfig &notifyConfig)
{
const auto n = createAndroidNotification(notification, notifyConfig);
m_backend.callMethod<void>("notify", "(Lorg/kde/knotifications/KNotification;)V", n.object<jobject>());
}
void NotifyByAndroid::close(KNotification *notification)
{
m_backend.callMethod<void>("close", "(ILjava/lang/String;)V", notification->id(), QJniObject::fromString(notification->eventId()).object<jstring>());
KNotificationPlugin::close(notification);
}
void NotifyByAndroid::notificationFinished(int id)
{
qCDebug(LOG_KNOTIFICATIONS) << id;
const auto it = m_notifications.find(id);
if (it == m_notifications.end()) {
return;
}
m_notifications.erase(it);
if (it.value()) {
finish(it.value());
}
}
void NotifyByAndroid::notificationActionInvoked(int id, const QString &action)
{
qCDebug(LOG_KNOTIFICATIONS) << id << action;
Q_EMIT actionInvoked(id, action);
}
void NotifyByAndroid::notificationInlineReply(int id, const QString &text)
{
qCDebug(LOG_KNOTIFICATIONS) << id << text;
Q_EMIT replied(id, text);
}
#include "moc_notifybyandroid.cpp"
@@ -0,0 +1,42 @@
/*
SPDX-FileCopyrightText: 2018 Volker Krause <vkrause@kde.org>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#ifndef NOTIFYBYANDROID_H
#define NOTIFYBYANDROID_H
#include "knotificationplugin.h"
#include <QJniObject>
#include <QPointer>
/** Android notification backend. */
class NotifyByAndroid : public KNotificationPlugin
{
Q_OBJECT
public:
explicit NotifyByAndroid(QObject *parent = nullptr);
~NotifyByAndroid() override;
// interface of KNotificationPlugin
QString optionName() override;
void notify(KNotification *notification, const KNotifyConfig &notifyConfig) override;
void update(KNotification *notification, const KNotifyConfig &notifyConfig) override;
void close(KNotification *notification) override;
// interface from Java
void notificationFinished(int id);
void notificationActionInvoked(int id, const QString &action);
void notificationInlineReply(int id, const QString &text);
private:
void notifyDeferred(KNotification *notification);
QJniObject createAndroidNotification(KNotification *notification, const KNotifyConfig &notifyConfig) const;
QJniObject m_backend;
QHash<int, QPointer<KNotification>> m_notifications;
};
#endif // NOTIFYBYANDROID_H
@@ -0,0 +1,229 @@
/*
This file is part of the KDE libraries
SPDX-FileCopyrightText: 2014-2015 Martin Klapetek <mklapetek@kde.org>
SPDX-FileCopyrightText: 2018 Kai Uwe Broulik <kde@privat.broulik.de>
SPDX-FileCopyrightText: 2023 Ismael Asensio <isma.af@gmail.com>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#include "notifybyaudio.h"
#include "debug_p.h"
#include <QFile>
#include <QFileInfo>
#include <QGuiApplication>
#include <QIcon>
#include <QString>
#include "knotification.h"
#include "knotifyconfig.h"
#include <canberra.h>
const QString DEFAULT_SOUND_THEME = QStringLiteral("ocean");
NotifyByAudio::NotifyByAudio(QObject *parent)
: KNotificationPlugin(parent)
, m_soundTheme(DEFAULT_SOUND_THEME)
, m_enabled(true)
{
qRegisterMetaType<uint32_t>("uint32_t");
m_settingsWatcher = KConfigWatcher::create(KSharedConfig::openConfig(QStringLiteral("kdeglobals")));
connect(m_settingsWatcher.get(), &KConfigWatcher::configChanged, this, [this](const KConfigGroup &group, const QByteArrayList &names) {
if (group.name() != QLatin1String("Sounds")) {
return;
}
if (names.contains(QByteArrayLiteral("Theme"))) {
m_soundTheme = group.readEntry("Theme", DEFAULT_SOUND_THEME);
}
if (names.contains(QByteArrayLiteral("Enable"))) {
m_enabled = group.readEntry("Enable", true);
}
});
const KConfigGroup group = m_settingsWatcher->config()->group(QStringLiteral("Sounds"));
m_soundTheme = group.readEntry("Theme", DEFAULT_SOUND_THEME);
m_enabled = group.readEntry("Enable", true);
}
NotifyByAudio::~NotifyByAudio()
{
if (m_context) {
ca_context_destroy(m_context);
}
m_context = nullptr;
}
ca_context *NotifyByAudio::context()
{
if (m_context) {
return m_context;
}
int ret = ca_context_create(&m_context);
if (ret != CA_SUCCESS) {
qCWarning(LOG_KNOTIFICATIONS) << "Failed to initialize canberra context for audio notification:" << ca_strerror(ret);
m_context = nullptr;
return nullptr;
}
QString desktopFileName = QGuiApplication::desktopFileName();
// handle apps which set the desktopFileName property with filename suffix,
// due to unclear API dox (https://bugreports.qt.io/browse/QTBUG-75521)
if (desktopFileName.endsWith(QLatin1String(".desktop"))) {
desktopFileName.chop(8);
}
ret = ca_context_change_props(m_context,
CA_PROP_APPLICATION_NAME,
qUtf8Printable(qApp->applicationDisplayName()),
CA_PROP_APPLICATION_ID,
qUtf8Printable(desktopFileName),
CA_PROP_APPLICATION_ICON_NAME,
qUtf8Printable(qApp->windowIcon().name()),
nullptr);
if (ret != CA_SUCCESS) {
qCWarning(LOG_KNOTIFICATIONS) << "Failed to set application properties on canberra context for audio notification:" << ca_strerror(ret);
}
return m_context;
}
void NotifyByAudio::notify(KNotification *notification, const KNotifyConfig &notifyConfig)
{
if (!m_enabled) {
qCDebug(LOG_KNOTIFICATIONS) << "Notification sounds are globally disabled";
return;
}
const QString soundName = notifyConfig.readEntry(QStringLiteral("Sound"));
if (soundName.isEmpty()) {
qCWarning(LOG_KNOTIFICATIONS) << "Audio notification requested, but no sound name provided in notifyrc file, aborting audio notification";
finish(notification);
return;
}
// Legacy implementation. Fallback lookup for a full path within the `$XDG_DATA_LOCATION/sounds` dirs
QUrl fallbackUrl;
const auto dataLocations = QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation);
for (const QString &dataLocation : dataLocations) {
fallbackUrl = QUrl::fromUserInput(soundName, dataLocation + QStringLiteral("/sounds"), QUrl::AssumeLocalFile);
if (fallbackUrl.isLocalFile() && QFileInfo::exists(fallbackUrl.toLocalFile())) {
break;
} else if (!fallbackUrl.isLocalFile() && fallbackUrl.isValid()) {
break;
}
fallbackUrl.clear();
}
// Looping happens in the finishCallback
if (!playSound(m_currentId, soundName, fallbackUrl)) {
finish(notification);
return;
}
if (notification->flags() & KNotification::LoopSound) {
m_loopSoundUrls.insert(m_currentId, {soundName, fallbackUrl});
}
Q_ASSERT(!m_notifications.value(m_currentId));
m_notifications.insert(m_currentId, notification);
++m_currentId;
}
bool NotifyByAudio::playSound(quint32 id, const QString &soundName, const QUrl &fallbackUrl)
{
if (!context()) {
qCWarning(LOG_KNOTIFICATIONS) << "Cannot play notification sound without canberra context";
return false;
}
ca_proplist *props = nullptr;
ca_proplist_create(&props);
ca_proplist_sets(props, CA_PROP_EVENT_ID, soundName.toLatin1().constData());
ca_proplist_sets(props, CA_PROP_CANBERRA_XDG_THEME_NAME, m_soundTheme.toLatin1().constData());
// Fallback to filename
if (!fallbackUrl.isEmpty()) {
ca_proplist_sets(props, CA_PROP_MEDIA_FILENAME, QFile::encodeName(fallbackUrl.toLocalFile()).constData());
}
// We'll also want this cached for a time. volatile makes sure the cache is
// dropped after some time or when the cache is under pressure.
ca_proplist_sets(props, CA_PROP_CANBERRA_CACHE_CONTROL, "volatile");
int ret = ca_context_play_full(context(), id, props, &ca_finish_callback, this);
ca_proplist_destroy(props);
if (ret != CA_SUCCESS) {
qCWarning(LOG_KNOTIFICATIONS) << "Failed to play sound with canberra:" << ca_strerror(ret);
return false;
}
return true;
}
void NotifyByAudio::ca_finish_callback(ca_context *c, uint32_t id, int error_code, void *userdata)
{
Q_UNUSED(c);
QMetaObject::invokeMethod(static_cast<NotifyByAudio *>(userdata), "finishCallback", Q_ARG(uint32_t, id), Q_ARG(int, error_code));
}
void NotifyByAudio::finishCallback(uint32_t id, int error_code)
{
KNotification *notification = m_notifications.value(id, nullptr);
if (!notification) {
// We may have gotten a late finish callback.
return;
}
if (error_code == CA_SUCCESS) {
// Loop the sound now if we have one
auto soundInfoIt = m_loopSoundUrls.constFind(id);
if (soundInfoIt != m_loopSoundUrls.constEnd()) {
if (!playSound(id, soundInfoIt->first, soundInfoIt->second)) {
finishNotification(notification, id);
}
return;
}
} else if (error_code != CA_ERROR_CANCELED) {
qCWarning(LOG_KNOTIFICATIONS) << "Playing audio notification failed:" << ca_strerror(error_code);
}
finishNotification(notification, id);
}
void NotifyByAudio::close(KNotification *notification)
{
if (!m_notifications.values().contains(notification)) {
return;
}
const auto id = m_notifications.key(notification);
if (m_context) {
int ret = ca_context_cancel(m_context, id);
if (ret != CA_SUCCESS) {
qCWarning(LOG_KNOTIFICATIONS) << "Failed to cancel canberra context for audio notification:" << ca_strerror(ret);
return;
}
}
// Consider the notification finished. ca_context_cancel schedules a cancel
// but we need to stop using the notification immediately or we could access
// a notification past its lifetime (close() may, or indeed must,
// schedule deletion of the notification).
// https://bugs.kde.org/show_bug.cgi?id=398695
finishNotification(notification, id);
}
void NotifyByAudio::finishNotification(KNotification *notification, quint32 id)
{
m_notifications.remove(id);
m_loopSoundUrls.remove(id);
finish(notification);
}
#include "moc_notifybyaudio.cpp"
@@ -0,0 +1,59 @@
/*
This file is part of the KDE libraries
SPDX-FileCopyrightText: 2014 Martin Klapetek <mklapetek@kde.org>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#ifndef NOTIFYBYAUDIO_H
#define NOTIFYBYAUDIO_H
#include "knotificationplugin.h"
#include <QHash>
#include <QUrl>
#include <KConfigWatcher>
class KNotification;
struct ca_context;
class NotifyByAudio : public KNotificationPlugin
{
Q_OBJECT
public:
explicit NotifyByAudio(QObject *parent = nullptr);
~NotifyByAudio() override;
QString optionName() override
{
return QStringLiteral("Sound");
}
void notify(KNotification *notification, const KNotifyConfig &notifyConfig) override;
void close(KNotification *notification) override;
private Q_SLOTS:
void finishCallback(uint32_t id, int error_code);
private:
static void ca_finish_callback(ca_context *c, uint32_t id, int error_code, void *userdata);
ca_context *context();
void finishNotification(KNotification *notification, quint32 id);
bool playSound(quint32 id, const QString &eventName, const QUrl &fallbackUrl);
ca_context *m_context = nullptr;
quint32 m_currentId = 0;
QHash<quint32, KNotification *> m_notifications;
// in case we loop we store the URL for the notification to be able to replay it
QHash<quint32, std::pair<QString, QUrl>> m_loopSoundUrls;
KConfigWatcher::Ptr m_settingsWatcher;
QString m_soundTheme;
bool m_enabled = true;
};
#endif
@@ -0,0 +1,29 @@
/*
SPDX-FileCopyrightText: 2019-2020 Weixuan XIAO <veyx.shaw at gmail.com>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#ifndef NOTIFYBYMACOSNOTIFICATIONCENTER_H
#define NOTIFYBYMACOSNOTIFICATIONCENTER_H
#include "knotificationplugin.h"
class NotifyByMacOSNotificationCenter : public KNotificationPlugin
{
Q_OBJECT
public:
NotifyByMacOSNotificationCenter(QObject *parent);
~NotifyByMacOSNotificationCenter() override;
QString optionName() override
{
return QStringLiteral("Popup");
}
void notify(KNotification *notification, const KNotifyConfig &notifyConfig) override;
void update(KNotification *notification, const KNotifyConfig &notifyConfig) override;
void close(KNotification *notification) override;
};
#endif // NOTIFYBYMACOSNOTIFICATIONCENTER_H
@@ -0,0 +1,187 @@
/*
SPDX-FileCopyrightText: 2019-2020 Weixuan XIAO <veyx.shaw at gmail.com>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#include "notifybymacosnotificationcenter.h"
#include "knotification.h"
#include "knotifyconfig.h"
#include "debug_p.h"
#include <QIcon>
#include <QDebug>
#import <AppKit/AppKit.h>
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
Q_FORWARD_DECLARE_OBJC_CLASS(MacOSNotificationCenterDelegate);
class MacOSNotificationCenterPrivate
{
public:
static MacOSNotificationCenterPrivate *instance();
~MacOSNotificationCenterPrivate();
QMap<int, KNotification*> m_notifications;
int m_internalCounter;
private:
MacOSNotificationCenterPrivate();
MacOSNotificationCenterDelegate *m_delegate;
static MacOSNotificationCenterPrivate *m_instance;
};
@interface MacOSNotificationCenterDelegate : NSObject<NSUserNotificationCenterDelegate> {}
@end
@implementation MacOSNotificationCenterDelegate
- (BOOL)userNotificationCenter:(NSUserNotificationCenter *)center shouldPresentNotification:(NSUserNotification *)notification
{
Q_UNUSED(center);
Q_UNUSED(notification);
return YES;
}
- (void)userNotificationCenter:(NSUserNotificationCenter *)center didDeliverNotification:(NSUserNotification *)notification
{
Q_UNUSED(center);
Q_UNUSED(notification);
qCDebug(LOG_KNOTIFICATIONS) << "Send notification " << [notification.userInfo[@"id"] intValue];
}
@end
MacOSNotificationCenterPrivate *MacOSNotificationCenterPrivate::m_instance = nullptr;
MacOSNotificationCenterPrivate::MacOSNotificationCenterPrivate()
: m_internalCounter(0)
{
// Set delegate
m_delegate = [MacOSNotificationCenterDelegate alloc];
[[NSUserNotificationCenter defaultUserNotificationCenter] setDelegate:m_delegate];
}
MacOSNotificationCenterPrivate::~MacOSNotificationCenterPrivate()
{
[[NSUserNotificationCenter defaultUserNotificationCenter] setDelegate: nil];
[m_delegate release];
// Try to finish all NSNotification
NSArray<NSUserNotification *> *deliveredNotifications = [NSUserNotificationCenter defaultUserNotificationCenter].deliveredNotifications;
for (NSUserNotification *deliveredNotification in deliveredNotifications) {
// Remove NSNotification in notification center
[[NSUserNotificationCenter defaultUserNotificationCenter] removeDeliveredNotification: deliveredNotification];
}
}
MacOSNotificationCenterPrivate *MacOSNotificationCenterPrivate::instance() {
if (!m_instance) {
m_instance = new MacOSNotificationCenterPrivate();
}
return m_instance;
}
NotifyByMacOSNotificationCenter::NotifyByMacOSNotificationCenter(QObject* parent)
: KNotificationPlugin(parent)
{
// Clear notifications
NSArray<NSUserNotification *> *deliveredNotifications = [NSUserNotificationCenter defaultUserNotificationCenter].deliveredNotifications;
for (NSUserNotification *deliveredNotification in deliveredNotifications) {
// Remove NSNotification in notification center
[[NSUserNotificationCenter defaultUserNotificationCenter] removeDeliveredNotification: deliveredNotification];
}
}
NotifyByMacOSNotificationCenter::~NotifyByMacOSNotificationCenter() {}
void NotifyByMacOSNotificationCenter::notify(KNotification *notification, const KNotifyConfig &notifyConfig)
{
Q_UNUSED(notifyConfig);
int internalId = MacOSNotificationCenterPrivate::instance()->m_internalCounter++;
NSUserNotification *osxNotification = [[[NSUserNotification alloc] init] autorelease];
NSString *notificationId = [NSString stringWithFormat: @"%d", notification->id()];
NSString *internalNotificationId = [NSString stringWithFormat: @"%d", internalId];
NSString *title = notification->title().toNSString();
NSString *text = notification->text().toNSString();
osxNotification.title = title;
osxNotification.userInfo = [NSDictionary dictionaryWithObjectsAndKeys: notificationId, @"id",
internalNotificationId, @"internalId", nil];
osxNotification.informativeText = text;
if (notification->pixmap().isNull()) {
QIcon notificationIcon = QIcon::fromTheme(notification->iconName());
if (!notificationIcon.isNull()) {
osxNotification.contentImage = [[NSImage alloc]
initWithCGImage: notificationIcon.pixmap(QSize(64, 64)).toImage().toCGImage() size: NSMakeSize(64, 64)];
}
} else {
osxNotification.contentImage = [[NSImage alloc]
initWithCGImage: notification->pixmap().toImage().toCGImage() size: NSMakeSize(64, 64)];
}
if (notification->actions().isEmpty()) {
// Remove all buttons
osxNotification.hasReplyButton = false;
osxNotification.hasActionButton = false;
} else {
osxNotification.hasActionButton = true;
// Workaround: this "API" will cause refuse from Apple
// [osxNotification setValue:[NSNumber numberWithBool:YES] forKey: @"_alwaysShowAlternateActionMenu"];
// Assign first action to action button
if (!notification->actions().empty()) {
osxNotification.actionButtonTitle = notification->actions().at(0)->label().toNSString();
}
// Construct a list for all actions left for additional buttons
NSMutableArray<NSUserNotificationAction*> *actions = [[NSMutableArray alloc] init];
for (int index = 1; index < notification->actions().length(); index++) {
NSUserNotificationAction *action =
[NSUserNotificationAction actionWithIdentifier: [NSString stringWithFormat:@"%d", index]
title: notification->actions().at(index)->label().toNSString()];
[actions addObject: action];
}
osxNotification.additionalActions = actions;
}
[[NSUserNotificationCenter defaultUserNotificationCenter] deliverNotification: osxNotification];
MacOSNotificationCenterPrivate::instance()->m_notifications.insert(internalId, notification);
}
void NotifyByMacOSNotificationCenter::close(KNotification *notification)
{
qCDebug(LOG_KNOTIFICATIONS) << "Remove notification " << notification->id();
NSArray<NSUserNotification *> *deliveredNotifications = [NSUserNotificationCenter defaultUserNotificationCenter].deliveredNotifications;
for (NSUserNotification *deliveredNotification in deliveredNotifications) {
if ([deliveredNotification.userInfo[@"id"] intValue] == notification->id()) {
// Remove KNotification in mapping
int internalId = [deliveredNotification.userInfo[@"id"] intValue];
MacOSNotificationCenterPrivate::instance()->m_notifications.remove(internalId);
// Remove NSNotification in notification center
[[NSUserNotificationCenter defaultUserNotificationCenter] removeDeliveredNotification: deliveredNotification];
finish(notification);
return;
}
}
qCDebug(LOG_KNOTIFICATIONS) << "Notification " << notification->id() << " not found";
finish(notification);
}
void NotifyByMacOSNotificationCenter::update(KNotification *notification, const KNotifyConfig &notifyConfig)
{
close(notification);
notify(notification, notifyConfig);
}
#include "moc_notifybymacosnotificationcenter.cpp"
@@ -0,0 +1,361 @@
/*
SPDX-FileCopyrightText: 2005-2009 Olivier Goffart <ogoffart at kde.org>
SPDX-FileCopyrightText: 2008 Dmitry Suzdalev <dimsuz@gmail.com>
SPDX-FileCopyrightText: 2014 Martin Klapetek <mklapetek@kde.org>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#include "notifybypopup.h"
#include "debug_p.h"
#include "imageconverter.h"
#include "knotification.h"
#include "knotificationreplyaction.h"
#include <QBuffer>
#include <QDBusConnection>
#include <QGuiApplication>
#include <QHash>
#include <QIcon>
#include <QMutableListIterator>
#include <QPointer>
#include <QUrl>
#include <KConfigGroup>
NotifyByPopup::NotifyByPopup(QObject *parent)
: KNotificationPlugin(parent)
, m_dbusInterface(QStringLiteral("org.freedesktop.Notifications"), QStringLiteral("/org/freedesktop/Notifications"), QDBusConnection::sessionBus())
{
m_dbusServiceCapCacheDirty = true;
connect(&m_dbusInterface, &org::freedesktop::Notifications::ActionInvoked, this, &NotifyByPopup::onNotificationActionInvoked);
connect(&m_dbusInterface, &org::freedesktop::Notifications::ActivationToken, this, &NotifyByPopup::onNotificationActionTokenReceived);
// TODO can we check if this actually worked?
// probably not as this just does a DBus filter which will work but the signal might still get caught in apparmor :/
connect(&m_dbusInterface, &org::freedesktop::Notifications::NotificationReplied, this, &NotifyByPopup::onNotificationReplied);
connect(&m_dbusInterface, &org::freedesktop::Notifications::NotificationClosed, this, &NotifyByPopup::onNotificationClosed);
}
NotifyByPopup::~NotifyByPopup()
{
if (!m_notificationQueue.isEmpty()) {
qCWarning(LOG_KNOTIFICATIONS) << "Had queued notifications on destruction. Was the eventloop running?";
}
}
void NotifyByPopup::notify(KNotification *notification, const KNotifyConfig &notifyConfig)
{
if (m_dbusServiceCapCacheDirty) {
// if we don't have the server capabilities yet, we need to query for them first;
// as that is an async dbus operation, we enqueue the notification and process them
// when we receive dbus reply with the server capabilities
m_notificationQueue.append(qMakePair(notification, notifyConfig));
queryPopupServerCapabilities();
} else {
if (!sendNotificationToServer(notification, notifyConfig)) {
finish(notification); // an error occurred.
}
}
}
void NotifyByPopup::update(KNotification *notification, const KNotifyConfig &notifyConfig)
{
sendNotificationToServer(notification, notifyConfig, true);
}
void NotifyByPopup::close(KNotification *notification)
{
QMutableListIterator<QPair<KNotification *, KNotifyConfig>> iter(m_notificationQueue);
while (iter.hasNext()) {
auto &item = iter.next();
if (item.first == notification) {
iter.remove();
}
}
uint id = m_notifications.key(notification, 0);
if (id == 0) {
qCDebug(LOG_KNOTIFICATIONS) << "not found dbus id to close" << notification->id();
return;
}
m_dbusInterface.CloseNotification(id);
}
void NotifyByPopup::onNotificationActionTokenReceived(uint notificationId, const QString &xdgActivationToken)
{
auto iter = m_notifications.find(notificationId);
if (iter == m_notifications.end()) {
return;
}
KNotification *n = *iter;
if (n) {
Q_EMIT xdgActivationTokenReceived(n->id(), xdgActivationToken);
}
}
void NotifyByPopup::onNotificationActionInvoked(uint notificationId, const QString &actionKey)
{
auto iter = m_notifications.find(notificationId);
if (iter == m_notifications.end()) {
return;
}
KNotification *n = *iter;
if (n) {
if (actionKey == QLatin1String("inline-reply") && n->replyAction()) {
Q_EMIT replied(n->id(), QString());
} else {
Q_EMIT actionInvoked(n->id(), actionKey);
}
} else {
m_notifications.erase(iter);
}
}
void NotifyByPopup::onNotificationClosed(uint dbus_id, uint reason)
{
auto iter = m_notifications.find(dbus_id);
if (iter == m_notifications.end()) {
return;
}
KNotification *n = *iter;
m_notifications.remove(dbus_id);
if (n) {
Q_EMIT finished(n);
// The popup bubble is the only user facing part of a notification,
// if the user closes the popup, it means he wants to get rid
// of the notification completely, including playing sound etc
// Therefore we close the KNotification completely after closing
// the popup, but only if the reason is 2, which means "user closed"
if (reason == 2) {
n->close();
}
}
}
void NotifyByPopup::onNotificationReplied(uint notificationId, const QString &text)
{
auto iter = m_notifications.find(notificationId);
if (iter == m_notifications.end()) {
return;
}
KNotification *n = *iter;
if (n) {
if (n->replyAction()) {
Q_EMIT replied(n->id(), text);
}
} else {
m_notifications.erase(iter);
}
}
void NotifyByPopup::getAppCaptionAndIconName(const KNotifyConfig &notifyConfig, QString *appCaption, QString *iconName)
{
*appCaption = notifyConfig.readGlobalEntry(QStringLiteral("Name"));
if (appCaption->isEmpty()) {
*appCaption = notifyConfig.readGlobalEntry(QStringLiteral("Comment"));
}
if (appCaption->isEmpty()) {
*appCaption = notifyConfig.applicationName();
}
*iconName = notifyConfig.readEntry(QStringLiteral("IconName"));
if (iconName->isEmpty()) {
*iconName = notifyConfig.readGlobalEntry(QStringLiteral("IconName"));
}
if (iconName->isEmpty()) {
*iconName = qGuiApp->windowIcon().name();
}
if (iconName->isEmpty()) {
*iconName = notifyConfig.applicationName();
}
}
bool NotifyByPopup::sendNotificationToServer(KNotification *notification, const KNotifyConfig &notifyConfig_nocheck, bool update)
{
uint updateId = m_notifications.key(notification, 0);
if (update) {
if (updateId == 0) {
// we have nothing to update; the notification we're trying to update
// has been already closed
return false;
}
}
QString appCaption;
QString iconName;
getAppCaptionAndIconName(notifyConfig_nocheck, &appCaption, &iconName);
// did the user override the icon name?
if (!notification->iconName().isEmpty()) {
iconName = notification->iconName();
}
QString title = notification->title().isEmpty() ? appCaption : notification->title();
QString text = notification->text();
if (!m_popupServerCapabilities.contains(QLatin1String("body-markup"))) {
text = stripRichText(text);
}
QVariantMap hintsMap;
// freedesktop.org spec defines action list to be list like
// (act_id1, action1, act_id2, action2, ...)
//
// assign id's to actions like it's done in fillPopup() method
// (i.e. starting from 1)
QStringList actionList;
if (m_popupServerCapabilities.contains(QLatin1String("actions"))) {
if (notification->defaultAction()) {
actionList.append(QStringLiteral("default"));
actionList.append(notification->defaultAction()->label());
}
int actId = 0;
const auto listActions = notification->actions();
for (const KNotificationAction *action : listActions) {
actId++;
actionList.append(action->id());
actionList.append(action->label());
}
if (auto *replyAction = notification->replyAction()) {
const bool supportsInlineReply = m_popupServerCapabilities.contains(QLatin1String("inline-reply"));
if (supportsInlineReply || replyAction->fallbackBehavior() == KNotificationReplyAction::FallbackBehavior::UseRegularAction) {
actionList.append(QStringLiteral("inline-reply"));
actionList.append(replyAction->label());
if (supportsInlineReply) {
if (!replyAction->placeholderText().isEmpty()) {
hintsMap.insert(QStringLiteral("x-kde-reply-placeholder-text"), replyAction->placeholderText());
}
if (!replyAction->submitButtonText().isEmpty()) {
hintsMap.insert(QStringLiteral("x-kde-reply-submit-button-text"), replyAction->submitButtonText());
}
if (replyAction->submitButtonIconName().isEmpty()) {
hintsMap.insert(QStringLiteral("x-kde-reply-submit-button-icon-name"), replyAction->submitButtonIconName());
}
}
}
}
}
// Add the application name to the hints.
// According to freedesktop.org spec, the app_name is supposed to be the application's "pretty name"
// but in some places it's handy to know the application name itself
if (!notification->appName().isEmpty()) {
hintsMap[QStringLiteral("x-kde-appname")] = notification->appName();
}
if (!notification->eventId().isEmpty()) {
hintsMap[QStringLiteral("x-kde-eventId")] = notification->eventId();
}
if (notification->flags() & KNotification::SkipGrouping) {
hintsMap[QStringLiteral("x-kde-skipGrouping")] = 1;
}
QString desktopFileName = QGuiApplication::desktopFileName();
if (!desktopFileName.isEmpty()) {
// handle apps which set the desktopFileName property with filename suffix,
// due to unclear API dox (https://bugreports.qt.io/browse/QTBUG-75521)
if (desktopFileName.endsWith(QLatin1String(".desktop"))) {
desktopFileName.chop(8);
}
hintsMap[QStringLiteral("desktop-entry")] = desktopFileName;
}
int urgency = -1;
switch (notification->urgency()) {
case KNotification::DefaultUrgency:
break;
case KNotification::LowUrgency:
urgency = 0;
break;
case KNotification::NormalUrgency:
Q_FALLTHROUGH();
// freedesktop.org m_notifications only know low, normal, critical
case KNotification::HighUrgency:
urgency = 1;
break;
case KNotification::CriticalUrgency:
urgency = 2;
break;
}
if (urgency > -1) {
hintsMap[QStringLiteral("urgency")] = urgency;
}
const QVariantMap hints = notification->hints();
for (auto it = hints.constBegin(); it != hints.constEnd(); ++it) {
hintsMap[it.key()] = it.value();
}
// FIXME - re-enable/fix
// let's see if we've got an image, and store the image in the hints map
if (!notification->pixmap().isNull()) {
QByteArray pixmapData;
QBuffer buffer(&pixmapData);
buffer.open(QIODevice::WriteOnly);
notification->pixmap().save(&buffer, "PNG");
buffer.close();
hintsMap[QStringLiteral("image_data")] = ImageConverter::variantForImage(QImage::fromData(pixmapData));
}
// Persistent => 0 == infinite timeout
// CloseOnTimeout => -1 == let the server decide
int timeout = (notification->flags() & KNotification::Persistent) ? 0 : -1;
const QDBusPendingReply<uint> reply = m_dbusInterface.Notify(appCaption, updateId, iconName, title, text, actionList, hintsMap, timeout);
// parent is set to the notification so that no-one ever accesses a dangling pointer on the notificationObject property
QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(reply, notification);
QObject::connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, notification](QDBusPendingCallWatcher *watcher) {
watcher->deleteLater();
QDBusPendingReply<uint> reply = *watcher;
m_notifications.insert(reply.argumentAt<0>(), notification);
});
return true;
}
void NotifyByPopup::queryPopupServerCapabilities()
{
if (!m_dbusServiceCapCacheDirty) {
return;
}
QDBusPendingReply<QStringList> call = m_dbusInterface.GetCapabilities();
QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(call);
QObject::connect(watcher, &QDBusPendingCallWatcher::finished, this, [this](QDBusPendingCallWatcher *watcher) {
watcher->deleteLater();
const QDBusPendingReply<QStringList> reply = *watcher;
const QStringList capabilities = reply.argumentAt<0>();
m_popupServerCapabilities = capabilities;
m_dbusServiceCapCacheDirty = false;
// re-run notify() on all enqueued m_notifications
for (const QPair<KNotification *, KNotifyConfig> &noti : std::as_const(m_notificationQueue)) {
notify(noti.first, noti.second);
}
m_notificationQueue.clear();
});
}
#include "moc_notifybypopup.cpp"
@@ -0,0 +1,96 @@
/*
SPDX-FileCopyrightText: 2005-2006 Olivier Goffart <ogoffart at kde.org>
SPDX-FileCopyrightText: 2008 Dmitry Suzdalev <dimsuz@gmail.com>
SPDX-FileCopyrightText: 2014 Martin Klapetek <mklapetek@kde.org>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#ifndef NOTIFYBYPOPUP_H
#define NOTIFYBYPOPUP_H
#include "knotificationplugin.h"
#include "knotifyconfig.h"
#include <QStringList>
#include "notifications_interface.h"
class KNotification;
class QDBusPendingCallWatcher;
class NotifyByPopup : public KNotificationPlugin
{
Q_OBJECT
public:
explicit NotifyByPopup(QObject *parent = nullptr);
~NotifyByPopup() override;
QString optionName() override
{
return QStringLiteral("Popup");
}
void notify(KNotification *notification, const KNotifyConfig &notifyConfig) override;
void close(KNotification *notification) override;
void update(KNotification *notification, const KNotifyConfig &notifyConfig) override;
private Q_SLOTS:
// slot which gets called when DBus signals that some notification action was invoked
void onNotificationActionInvoked(uint notificationId, const QString &actionKey);
void onNotificationActionTokenReceived(uint notificationId, const QString &xdgActionToken);
// slot which gets called when DBus signals that some notification was closed
void onNotificationClosed(uint, uint);
void onNotificationReplied(uint notificationId, const QString &text);
private:
/**
* Sends notification to DBus "org.freedesktop.notifications" interface.
* @param id knotify-sid identifier of notification
* @param config notification data
* @param update If true, will request the DBus service to update
the notification with new data from \c notification
* Otherwise will put new notification on screen
* @return true for success or false if there was an error.
*/
bool sendNotificationToServer(KNotification *notification, const KNotifyConfig &config, bool update = false);
/**
* Find the caption and the icon name of the application
*/
void getAppCaptionAndIconName(const KNotifyConfig &config, QString *appCaption, QString *iconName);
/*
* Query the dbus server for notification capabilities
*/
void queryPopupServerCapabilities();
/**
* DBus notification daemon capabilities cache.
* Do not use this variable. Use #popupServerCapabilities() instead.
* @see popupServerCapabilities
*/
QStringList m_popupServerCapabilities;
/**
* In case we still don't know notification server capabilities,
* we need to query those first. That's done in an async way
* so we queue all notifications while waiting for the capabilities
* to return, then process them from this queue
*/
QList<QPair<KNotification *, KNotifyConfig>> m_notificationQueue;
/**
* Whether the DBus notification daemon capability cache is up-to-date.
*/
bool m_dbusServiceCapCacheDirty;
/*
* As we communicate with the notification server over dbus
* we use only ids, this is for fast KNotifications lookup
*/
QHash<uint, QPointer<KNotification>> m_notifications;
org::freedesktop::Notifications m_dbusInterface;
Q_DISABLE_COPY_MOVE(NotifyByPopup)
};
#endif
@@ -0,0 +1,361 @@
/*
SPDX-FileCopyrightText: 2005-2006 Olivier Goffart <ogoffart at kde.org>
SPDX-FileCopyrightText: 2008 Dmitry Suzdalev <dimsuz@gmail.com>
SPDX-FileCopyrightText: 2014 Martin Klapetek <mklapetek@kde.org>
SPDX-FileCopyrightText: 2016 Jan Grulich <jgrulich@redhat.com>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#include "notifybyportal.h"
#include "debug_p.h"
#include "knotification.h"
#include "knotifyconfig.h"
#include <QBuffer>
#include <QDBusConnection>
#include <QDBusConnectionInterface>
#include <QDBusError>
#include <QDBusMessage>
#include <QDBusMetaType>
#include <QDBusServiceWatcher>
#include <QGuiApplication>
#include <QHash>
#include <QIcon>
#include <QMap>
#include <QPointer>
#include <KConfigGroup>
static const char portalDbusServiceName[] = "org.freedesktop.portal.Desktop";
static const char portalDbusInterfaceName[] = "org.freedesktop.portal.Notification";
static const char portalDbusPath[] = "/org/freedesktop/portal/desktop";
class NotifyByPortalPrivate
{
public:
struct PortalIcon {
QString str;
QDBusVariant data;
};
NotifyByPortalPrivate(NotifyByPortal *parent)
: dbusServiceExists(false)
, q(parent)
{
}
/**
* Sends notification to DBus "org.freedesktop.notifications" interface.
* @param id knotify-sid identifier of notification
* @param config notification data
* @param update If true, will request the DBus service to update
the notification with new data from \c notification
* Otherwise will put new notification on screen
* @return true for success or false if there was an error.
*/
bool sendNotificationToPortal(KNotification *notification, const KNotifyConfig &config);
/**
* Sends request to close Notification with id to DBus "org.freedesktop.notifications" interface
* @param id knotify-side notification ID to close
*/
void closePortalNotification(KNotification *notification);
/**
* Find the caption and the icon name of the application
*/
void getAppCaptionAndIconName(const KNotifyConfig &config, QString *appCaption, QString *iconName);
/**
* Specifies if DBus Notifications interface exists on session bus
*/
bool dbusServiceExists;
/*
* As we communicate with the notification server over dbus
* we use only ids, this is for fast KNotifications lookup
*/
QHash<uint, QPointer<KNotification>> portalNotifications;
/*
* Holds the id that will be assigned to the next notification source
* that will be created
*/
uint nextId;
NotifyByPortal *const q;
};
QDBusArgument &operator<<(QDBusArgument &argument, const NotifyByPortalPrivate::PortalIcon &icon)
{
argument.beginStructure();
argument << icon.str << icon.data;
argument.endStructure();
return argument;
}
const QDBusArgument &operator>>(const QDBusArgument &argument, NotifyByPortalPrivate::PortalIcon &icon)
{
argument.beginStructure();
argument >> icon.str >> icon.data;
argument.endStructure();
return argument;
}
Q_DECLARE_METATYPE(NotifyByPortalPrivate::PortalIcon)
//---------------------------------------------------------------------------------------
NotifyByPortal::NotifyByPortal(QObject *parent)
: KNotificationPlugin(parent)
, d(new NotifyByPortalPrivate(this))
{
// check if service already exists on plugin instantiation
QDBusConnectionInterface *interface = QDBusConnection::sessionBus().interface();
d->dbusServiceExists = interface && interface->isServiceRegistered(QString::fromLatin1(portalDbusServiceName));
if (d->dbusServiceExists) {
onServiceOwnerChanged(QString::fromLatin1(portalDbusServiceName), QString(), QStringLiteral("_")); // connect signals
}
// to catch register/unregister events from service in runtime
QDBusServiceWatcher *watcher = new QDBusServiceWatcher(this);
watcher->setConnection(QDBusConnection::sessionBus());
watcher->setWatchMode(QDBusServiceWatcher::WatchForOwnerChange);
watcher->addWatchedService(QString::fromLatin1(portalDbusServiceName));
connect(watcher, &QDBusServiceWatcher::serviceOwnerChanged, this, &NotifyByPortal::onServiceOwnerChanged);
}
NotifyByPortal::~NotifyByPortal() = default;
void NotifyByPortal::notify(KNotification *notification, const KNotifyConfig &notifyConfig)
{
if (d->portalNotifications.contains(notification->id())) {
// notification is already on the screen, do nothing
finish(notification);
return;
}
// check if Notifications DBus service exists on bus, use it if it does
if (d->dbusServiceExists) {
if (!d->sendNotificationToPortal(notification, notifyConfig)) {
finish(notification); // an error occurred.
}
}
}
void NotifyByPortal::close(KNotification *notification)
{
if (d->dbusServiceExists) {
d->closePortalNotification(notification);
}
}
void NotifyByPortal::update(KNotification *notification, const KNotifyConfig &notifyConfig)
{
// TODO not supported by portals
Q_UNUSED(notification);
Q_UNUSED(notifyConfig);
}
void NotifyByPortal::onServiceOwnerChanged(const QString &serviceName, const QString &oldOwner, const QString &newOwner)
{
Q_UNUSED(serviceName);
// close all notifications we currently hold reference to
for (KNotification *n : std::as_const(d->portalNotifications)) {
if (n) {
Q_EMIT finished(n);
}
}
d->portalNotifications.clear();
if (newOwner.isEmpty()) {
d->dbusServiceExists = false;
} else if (oldOwner.isEmpty()) {
d->dbusServiceExists = true;
d->nextId = 1;
// connect to action invocation signals
bool connected = QDBusConnection::sessionBus().connect(QString(), // from any service
QString::fromLatin1(portalDbusPath),
QString::fromLatin1(portalDbusInterfaceName),
QStringLiteral("ActionInvoked"),
this,
SLOT(onPortalNotificationActionInvoked(QString, QString, QVariantList)));
if (!connected) {
qCWarning(LOG_KNOTIFICATIONS) << "warning: failed to connect to ActionInvoked dbus signal";
}
}
}
void NotifyByPortal::onPortalNotificationActionInvoked(const QString &id, const QString &action, const QVariantList &parameter)
{
Q_UNUSED(parameter);
auto iter = d->portalNotifications.find(id.toUInt());
if (iter == d->portalNotifications.end()) {
return;
}
KNotification *n = *iter;
if (n) {
Q_EMIT actionInvoked(n->id(), action);
} else {
d->portalNotifications.erase(iter);
}
}
void NotifyByPortalPrivate::getAppCaptionAndIconName(const KNotifyConfig &notifyConfig, QString *appCaption, QString *iconName)
{
*appCaption = notifyConfig.readGlobalEntry(QStringLiteral("Name"));
if (appCaption->isEmpty()) {
*appCaption = notifyConfig.readGlobalEntry(QStringLiteral("Comment"));
}
if (appCaption->isEmpty()) {
*appCaption = notifyConfig.applicationName();
}
*iconName = notifyConfig.readEntry(QStringLiteral("IconName"));
if (iconName->isEmpty()) {
*iconName = notifyConfig.readGlobalEntry(QStringLiteral("IconName"));
}
if (iconName->isEmpty()) {
*iconName = qGuiApp->windowIcon().name();
}
if (iconName->isEmpty()) {
*iconName = notifyConfig.applicationName();
}
}
bool NotifyByPortalPrivate::sendNotificationToPortal(KNotification *notification, const KNotifyConfig &notifyConfig_nocheck)
{
QDBusMessage dbusNotificationMessage;
dbusNotificationMessage = QDBusMessage::createMethodCall(QString::fromLatin1(portalDbusServiceName),
QString::fromLatin1(portalDbusPath),
QString::fromLatin1(portalDbusInterfaceName),
QStringLiteral("AddNotification"));
QVariantList args;
// Will be used only with xdg-desktop-portal
QVariantMap portalArgs;
QString appCaption;
QString iconName;
getAppCaptionAndIconName(notifyConfig_nocheck, &appCaption, &iconName);
// did the user override the icon name?
if (!notification->iconName().isEmpty()) {
iconName = notification->iconName();
}
QString title = notification->title().isEmpty() ? appCaption : notification->title();
QString text = notification->text();
if (notification->defaultAction()) {
portalArgs.insert(QStringLiteral("default-action"), QStringLiteral("default"));
portalArgs.insert(QStringLiteral("default-action-target"), QStringLiteral("0"));
}
QString priority;
switch (notification->urgency()) {
case KNotification::DefaultUrgency:
break;
case KNotification::LowUrgency:
priority = QStringLiteral("low");
break;
case KNotification::NormalUrgency:
priority = QStringLiteral("normal");
break;
case KNotification::HighUrgency:
priority = QStringLiteral("high");
break;
case KNotification::CriticalUrgency:
priority = QStringLiteral("urgent");
break;
}
if (!priority.isEmpty()) {
portalArgs.insert(QStringLiteral("priority"), priority);
}
// freedesktop.org spec defines action list to be list like
// (act_id1, action1, act_id2, action2, ...)
//
// assign id's to actions like it's done in fillPopup() method
// (i.e. starting from 1)
QList<QVariantMap> buttons;
buttons.reserve(notification->actions().count());
const auto listActions = notification->actions();
for (KNotificationAction *action : listActions) {
QVariantMap button = {{QStringLiteral("action"), action->id()}, //
{QStringLiteral("label"), action->label()}};
buttons << button;
}
qDBusRegisterMetaType<QList<QVariantMap>>();
qDBusRegisterMetaType<PortalIcon>();
if (!notification->pixmap().isNull()) {
QByteArray pixmapData;
QBuffer buffer(&pixmapData);
buffer.open(QIODevice::WriteOnly);
notification->pixmap().save(&buffer, "PNG");
buffer.close();
PortalIcon icon;
icon.str = QStringLiteral("bytes");
icon.data.setVariant(pixmapData);
portalArgs.insert(QStringLiteral("icon"), QVariant::fromValue<PortalIcon>(icon));
} else {
// Use this for now for backwards compatibility, we can as well set the variant to be (sv) where the
// string is keyword "themed" and the variant is an array of strings with icon names
portalArgs.insert(QStringLiteral("icon"), iconName);
}
portalArgs.insert(QStringLiteral("title"), title);
portalArgs.insert(QStringLiteral("body"), text);
portalArgs.insert(QStringLiteral("buttons"), QVariant::fromValue<QList<QVariantMap>>(buttons));
args.append(QString::number(nextId));
args.append(portalArgs);
dbusNotificationMessage.setArguments(args);
QDBusPendingCall notificationCall = QDBusConnection::sessionBus().asyncCall(dbusNotificationMessage, -1);
// If we are in sandbox we don't need to wait for returned notification id
portalNotifications.insert(nextId++, notification);
return true;
}
void NotifyByPortalPrivate::closePortalNotification(KNotification *notification)
{
uint id = portalNotifications.key(notification, 0);
qCDebug(LOG_KNOTIFICATIONS) << "ID: " << id;
if (id == 0) {
qCDebug(LOG_KNOTIFICATIONS) << "not found dbus id to close" << notification->id();
return;
}
QDBusMessage m = QDBusMessage::createMethodCall(QString::fromLatin1(portalDbusServiceName),
QString::fromLatin1(portalDbusPath),
QString::fromLatin1(portalDbusInterfaceName),
QStringLiteral("RemoveNotification"));
m.setArguments({QString::number(id)});
// send(..) does not block
bool queued = QDBusConnection::sessionBus().send(m);
if (!queued) {
qCWarning(LOG_KNOTIFICATIONS) << "Failed to queue dbus message for closing a notification";
}
}
#include "moc_notifybyportal.cpp"
@@ -0,0 +1,48 @@
/*
SPDX-FileCopyrightText: 2005-2006 Olivier Goffart <ogoffart at kde.org>
SPDX-FileCopyrightText: 2008 Dmitry Suzdalev <dimsuz@gmail.com>
SPDX-FileCopyrightText: 2014 Martin Klapetek <mklapetek@kde.org>
SPDX-FileCopyrightText: 2016 Jan Grulich <jgrulich@redhat.com>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#ifndef NOTIFYBYPORTAL_H
#define NOTIFYBYPORTAL_H
#include "knotificationplugin.h"
#include <QVariantList>
#include <memory>
class KNotification;
class NotifyByPortalPrivate;
class NotifyByPortal : public KNotificationPlugin
{
Q_OBJECT
public:
explicit NotifyByPortal(QObject *parent = nullptr);
~NotifyByPortal() override;
QString optionName() override
{
return QStringLiteral("Popup");
}
void notify(KNotification *notification, const KNotifyConfig &notifyConfig) override;
void close(KNotification *notification) override;
void update(KNotification *notification, const KNotifyConfig &notifyConfig) override;
private Q_SLOTS:
// slot to catch appearance or disappearance of org.freedesktop.Desktop DBus service
void onServiceOwnerChanged(const QString &, const QString &, const QString &);
void onPortalNotificationActionInvoked(const QString &, const QString &, const QVariantList &);
private:
std::unique_ptr<NotifyByPortalPrivate> const d;
};
#endif
@@ -0,0 +1,254 @@
/*
SPDX-FileCopyrightText: 2019 Piyush Aggarwal <piyushaggarwal002@gmail.com>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include "notifybysnore.h"
#include "debug_p.h"
#include "knotification.h"
#include "knotifyconfig.h"
#include "knotificationreplyaction.h"
#include <QGuiApplication>
#include <QIcon>
#include <QLocalSocket>
#include <snoretoastactions.h>
/*
* On Windows a shortcut to your app is needed to be installed in the Start Menu
* (and subsequently, registered with the OS) in order to show notifications.
* Since KNotifications is a library, an app using it can't (feasibly) be properly
* registered with the OS. It is possible we could come up with some complicated solution
* which would require every KNotification-using app to do some special and probably
* difficult to understand change to support Windows. Or we can have SnoreToast.exe
* take care of all that nonsense for us.
* Note that, up to this point, there have been no special
* KNotifications changes to the generic application codebase to make this work,
* just some tweaks to the Craft blueprint and packaging script
* to pull in SnoreToast and trigger shortcut building respectively.
* Be sure to have a shortcut installed in Windows Start Menu by SnoreToast.
*
* So the location doesn't matter, but it's only possible to register the internal COM server in an executable.
* We could make it a static lib and link it in all KDE applications,
* but to make the action center integration work, we would need to also compile a class
* into the executable using a compile time uuid.
*
* The used header is meant to help with parsing the response.
* The cmake target for LibSnoreToast is a INTERFACE lib, it only provides the include path.
*
*
* Trigger the shortcut installation during the installation of your app; syntax for shortcut installation is -
* ./SnoreToast.exe -install <absolute\address\of\shortcut> <absolute\address\to\app.exe> <appID>
*
* appID: use as-is from your app's QCoreApplication::applicationName() when installing the shortcut.
* NOTE: Install the shortcut in Windows Start Menu folder.
* For example, check out Craft Blueprint for Quassel-IRC or KDE Connect.
*/
namespace
{
const QString SnoreToastExecName()
{
return QStringLiteral("SnoreToast.exe");
}
}
NotifyBySnore::NotifyBySnore(QObject *parent)
: KNotificationPlugin(parent)
{
m_server.listen(QString::number(qHash(qApp->applicationDirPath())));
connect(&m_server, &QLocalServer::newConnection, this, [this]() {
QLocalSocket *responseSocket = m_server.nextPendingConnection();
connect(responseSocket, &QLocalSocket::readyRead, [this, responseSocket]() {
const QByteArray rawNotificationResponse = responseSocket->readAll();
responseSocket->deleteLater();
const QString notificationResponse = QString::fromWCharArray(reinterpret_cast<const wchar_t *>(rawNotificationResponse.constData()),
rawNotificationResponse.size() / sizeof(wchar_t));
qCDebug(LOG_KNOTIFICATIONS) << notificationResponse;
QMap<QString, QStringView> notificationResponseMap;
for (const auto str : QStringView(notificationResponse).split(QLatin1Char(';'))) {
const int equalIndex = str.indexOf(QLatin1Char('='));
notificationResponseMap.insert(str.mid(0, equalIndex).toString(), str.mid(equalIndex + 1));
}
const QString responseAction = notificationResponseMap[QStringLiteral("action")].toString();
const int responseNotificationId = notificationResponseMap[QStringLiteral("notificationId")].toInt();
qCDebug(LOG_KNOTIFICATIONS) << "The notification ID is : " << responseNotificationId;
KNotification *notification;
const auto iter = m_notifications.constFind(responseNotificationId);
if (iter != m_notifications.constEnd()) {
notification = iter.value();
} else {
qCWarning(LOG_KNOTIFICATIONS) << "Received a response for an unknown notification.";
return;
}
std::wstring w_action(responseAction.size(), 0);
responseAction.toWCharArray(const_cast<wchar_t *>(w_action.data()));
switch (SnoreToastActions::getAction(w_action)) {
case SnoreToastActions::Actions::Clicked:
qCDebug(LOG_KNOTIFICATIONS) << "User clicked on the toast.";
break;
case SnoreToastActions::Actions::Hidden:
qCDebug(LOG_KNOTIFICATIONS) << "The toast got hidden.";
break;
case SnoreToastActions::Actions::Dismissed:
qCDebug(LOG_KNOTIFICATIONS) << "User dismissed the toast.";
break;
case SnoreToastActions::Actions::Timedout:
qCDebug(LOG_KNOTIFICATIONS) << "The toast timed out.";
break;
case SnoreToastActions::Actions::ButtonClicked: {
qCDebug(LOG_KNOTIFICATIONS) << "User clicked an action button in the toast.";
const QString responseButton = notificationResponseMap[QStringLiteral("button")].toString();
Q_EMIT actionInvoked(responseNotificationId, responseButton);
break;
}
case SnoreToastActions::Actions::TextEntered: {
qCWarning(LOG_KNOTIFICATIONS) << "User entered some text in the toast.";
const QString replyText = notificationResponseMap[QStringLiteral("text")].toString();
qCWarning(LOG_KNOTIFICATIONS) << "Text entered was :: " << replyText;
Q_EMIT replied(responseNotificationId, replyText);
break;
}
default:
qCWarning(LOG_KNOTIFICATIONS) << "Unexpected behaviour with the toast. Please file a bug report / feature request.";
break;
}
// Action Center callbacks are not yet supported so just close the notification once done
if (notification != nullptr) {
NotifyBySnore::close(notification);
}
});
});
}
NotifyBySnore::~NotifyBySnore()
{
m_server.close();
}
void NotifyBySnore::notify(KNotification *notification, const KNotifyConfig &notifyConfig)
{
Q_UNUSED(notifyConfig);
// HACK work around that notification->id() is only populated after returning from here
// note that config will be invalid at that point, so we can't pass that along
QMetaObject::invokeMethod(
this,
[this, notification]() {
NotifyBySnore::notifyDeferred(notification);
},
Qt::QueuedConnection);
}
void NotifyBySnore::notifyDeferred(KNotification *notification)
{
m_notifications.insert(notification->id(), notification);
const QString notificationTitle = ((!notification->title().isEmpty()) ? notification->title() : qApp->applicationDisplayName());
QStringList snoretoastArgsList{QStringLiteral("-id"),
QString::number(notification->id()),
QStringLiteral("-t"),
notificationTitle,
QStringLiteral("-m"),
stripRichText(notification->text()),
QStringLiteral("-appID"),
qApp->applicationName(),
QStringLiteral("-pid"),
QString::number(qApp->applicationPid()),
QStringLiteral("-pipename"),
m_server.fullServerName()};
// handle the icon for toast notification
const QString iconPath = m_iconDir.path() + QLatin1Char('/') + QString::number(notification->id());
const bool hasIcon = (notification->pixmap().isNull()) ? qApp->windowIcon().pixmap(1024, 1024).save(iconPath, "PNG") //
: notification->pixmap().save(iconPath, "PNG");
if (hasIcon) {
snoretoastArgsList << QStringLiteral("-p") << iconPath;
}
// if'd below, because SnoreToast currently doesn't support both textbox and buttons in the same notification
if (notification->replyAction()) {
snoretoastArgsList << QStringLiteral("-tb");
} else if (!notification->actions().isEmpty()) {
// add actions if any
const auto actions = notification->actions();
QStringList actionLabels;
for (KNotificationAction *action : actions) {
action->setId(action->label());
actionLabels << action->label();
}
snoretoastArgsList << QStringLiteral("-b") << actionLabels.join(QLatin1Char(';'));
}
QProcess *snoretoastProcess = new QProcess();
connect(snoretoastProcess, &QProcess::readyReadStandardError, [snoretoastProcess, snoretoastArgsList]() {
const auto data = snoretoastProcess->readAllStandardError();
qCDebug(LOG_KNOTIFICATIONS) << "SnoreToast process stderr:" << snoretoastArgsList << data;
});
connect(snoretoastProcess, &QProcess::readyReadStandardOutput, [snoretoastProcess, snoretoastArgsList]() {
const auto data = snoretoastProcess->readAllStandardOutput();
qCDebug(LOG_KNOTIFICATIONS) << "SnoreToast process stdout:" << snoretoastArgsList << data;
});
connect(snoretoastProcess, &QProcess::errorOccurred, this, [this, snoretoastProcess, snoretoastArgsList, iconPath](QProcess::ProcessError error) {
qCWarning(LOG_KNOTIFICATIONS) << "SnoreToast process errored:" << snoretoastArgsList << error;
snoretoastProcess->deleteLater();
QFile::remove(iconPath);
});
connect(snoretoastProcess,
qOverload<int, QProcess::ExitStatus>(&QProcess::finished),
this,
[this, snoretoastProcess, snoretoastArgsList, iconPath](int exitCode, QProcess::ExitStatus exitStatus) {
qCDebug(LOG_KNOTIFICATIONS) << "SnoreToast process finished:" << snoretoastArgsList;
qCDebug(LOG_KNOTIFICATIONS) << "code:" << exitCode << "status:" << exitStatus;
snoretoastProcess->deleteLater();
QFile::remove(iconPath);
});
qCDebug(LOG_KNOTIFICATIONS) << "SnoreToast process starting:" << snoretoastArgsList;
snoretoastProcess->start(SnoreToastExecName(), snoretoastArgsList);
}
void NotifyBySnore::close(KNotification *notification)
{
qCDebug(LOG_KNOTIFICATIONS) << "Requested to close notification with ID:" << notification->id();
if (m_notifications.constFind(notification->id()) == m_notifications.constEnd()) {
qCWarning(LOG_KNOTIFICATIONS) << "Couldn't find the notification in m_notifications. Nothing to close.";
return;
}
m_notifications.remove(notification->id());
const QStringList snoretoastArgsList{QStringLiteral("-close"), QString::number(notification->id()), QStringLiteral("-appID"), qApp->applicationName()};
qCDebug(LOG_KNOTIFICATIONS) << "Closing notification; SnoreToast process arguments:" << snoretoastArgsList;
QProcess::startDetached(SnoreToastExecName(), snoretoastArgsList);
finish(notification);
}
void NotifyBySnore::update(KNotification *notification, const KNotifyConfig &notifyConfig)
{
Q_UNUSED(notification);
Q_UNUSED(notifyConfig);
qCWarning(LOG_KNOTIFICATIONS) << "updating a notification is not supported yet.";
}
#include "moc_notifybysnore.cpp"
@@ -0,0 +1,40 @@
/*
SPDX-FileCopyrightText: 2019 Piyush Aggarwal <piyushaggarwal002@gmail.com>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#ifndef NOTIFYBYSNORE_H
#define NOTIFYBYSNORE_H
#include "knotificationplugin.h"
#include <QLocalServer>
#include <QPointer>
#include <QProcess>
#include <QTemporaryDir>
class NotifyBySnore : public KNotificationPlugin
{
Q_OBJECT
public:
explicit NotifyBySnore(QObject *parent = nullptr);
~NotifyBySnore() override;
QString optionName() override
{
return QStringLiteral("Popup");
}
void notify(KNotification *notification, const KNotifyConfig &notifyConfig) override;
void notifyDeferred(KNotification *notification);
void close(KNotification *notification) override;
void update(KNotification *notification, const KNotifyConfig &notifyConfig) override;
private:
QHash<int, QPointer<KNotification>> m_notifications;
QLocalServer m_server;
QTemporaryDir m_iconDir;
};
#endif // NOTIFYBYSNORE_H
@@ -0,0 +1,69 @@
<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN" "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
<node>
<interface name="org.freedesktop.Notifications">
<signal name="NotificationClosed">
<arg name="id" type="u" direction="out"/>
<arg name="reason" type="u" direction="out"/>
</signal>
<signal name="ActionInvoked">
<arg name="id" type="u" direction="out"/>
<arg name="action_key" type="s" direction="out"/>
</signal>
<!-- non-standard -->
<signal name="NotificationReplied">
<arg name="id" type="u" direction="out"/>
<arg name="text" type="s" direction="out"/>
</signal>
<signal name="ActivationToken">
<arg name="id" type="u" direction="out"/>
<arg name="activation_token" type="s" direction="out"/>
</signal>
<method name="Notify">
<annotation name="org.qtproject.QtDBus.QtTypeName.In6" value="QVariantMap"/>
<arg type="u" direction="out"/>
<arg name="app_name" type="s" direction="in"/>
<arg name="replaces_id" type="u" direction="in"/>
<arg name="app_icon" type="s" direction="in"/>
<arg name="summary" type="s" direction="in"/>
<arg name="body" type="s" direction="in"/>
<arg name="actions" type="as" direction="in"/>
<arg name="hints" type="a{sv}" direction="in"/>
<arg name="timeout" type="i" direction="in"/>
</method>
<method name="CloseNotification">
<arg name="id" type="u" direction="in"/>
</method>
<method name="GetCapabilities">
<arg type="as" name="caps" direction="out"/>
</method>
<method name="GetServerInformation">
<arg type="s" name="name" direction="out"/>
<arg type="s" name="vendor" direction="out"/>
<arg type="s" name="version" direction="out"/>
<arg type="s" name="spec_version" direction="out"/>
</method>
<!-- Inhibitions -->
<method name="Inhibit">
<annotation name="org.qtproject.QtDBus.QtTypeName.In2" value="QVariantMap"/>
<arg type="u" direction="out"/>
<arg name="desktop_entry" type="s" direction="in"/>
<arg name="reason" type="s" direction="in"/>
<arg name="hints" type="a{sv}" direction="in"/>
</method>
<method name="UnInhibit">
<arg type="u" direction="in"/>
</method>
<property name="Inhibited" type="b" access="read">
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="true"/>
</property>
<!--<method name="ListInhibitors">
<annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="QList&lt;Inhibition&gt;"/>
<arg name="inhibitors" type="a(ssa{sv})" direction="out"/>
</method>-->
</interface>
</node>
@@ -0,0 +1,15 @@
# SPDX-FileCopyrightText: 2021 Volker Krause <vkrause@kde.org>
# SPDX-License-Identifier: BSD-3-Clause
ecm_add_qml_module(knotificationqmlplugin URI org.kde.notification VERSION 1.0 GENERATE_PLUGIN_SOURCE)
target_sources(knotificationqmlplugin PRIVATE
knotificationqmlplugin.cpp
)
target_link_libraries(knotificationqmlplugin PRIVATE
KF6Notifications
Qt6::Qml
)
ecm_finalize_qml_module(knotificationqmlplugin)
@@ -0,0 +1,69 @@
/*
SPDX-FileCopyrightText: 2021 Volker Krause <vkrause@kde.org>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include "knotificationqmlplugin.h"
NotificationWrapper::NotificationWrapper(QObject *parent)
: KNotification(QString(), KNotification::CloseOnTimeout, parent)
{
setAutoDelete(false);
m_actionsProperty = QQmlListProperty<KNotificationAction>(this,
nullptr,
&NotificationWrapper::appendAction,
&NotificationWrapper::actionsCount,
&NotificationWrapper::actionAt,
&NotificationWrapper::clearActions);
}
KNotificationReplyAction *NotificationWrapper::replyActionFactory()
{
if (!replyAction()) {
setReplyAction(std::make_unique<KNotificationReplyAction>(QString()));
}
return replyAction();
}
int NotificationWrapper::actionCount() const
{
return actions().count();
}
KNotificationAction *NotificationWrapper::actionAt(qsizetype index)
{
return actions().at(index);
}
QQmlListProperty<KNotificationAction> NotificationWrapper::actionsProperty() const
{
return m_actionsProperty;
}
qsizetype NotificationWrapper::actionsCount(QQmlListProperty<KNotificationAction> *list)
{
return static_cast<NotificationWrapper *>(list->object)->actionCount();
}
void NotificationWrapper::appendAction(QQmlListProperty<KNotificationAction> *list, KNotificationAction *value)
{
auto notification = static_cast<NotificationWrapper *>(list->object);
auto actions = notification->actions();
actions << value;
notification->setActionsQml(actions);
}
KNotificationAction *NotificationWrapper::actionAt(QQmlListProperty<KNotificationAction> *list, qsizetype index)
{
return static_cast<NotificationWrapper *>(list->object)->actionAt(index);
}
void NotificationWrapper::clearActions(QQmlListProperty<KNotificationAction> *list)
{
auto notification = static_cast<KNotification *>(list->object);
notification->clearActions();
}
#include "moc_knotificationqmlplugin.cpp"
@@ -0,0 +1,78 @@
/*
SPDX-FileCopyrightText: 2021 Volker Krause <vkrause@kde.org>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#ifndef KNOTIFICATIONQMLPLUGIN_H
#define KNOTIFICATIONQMLPLUGIN_H
#include <QQmlEngine>
#include <KNotification>
#include <KNotificationPermission>
#include <KNotificationReplyAction>
struct NotificationActionForeign {
Q_GADGET
QML_NAMED_ELEMENT(NotificationAction)
QML_FOREIGN(KNotificationAction);
};
struct NotificationReplyActionForeign {
Q_GADGET
QML_NAMED_ELEMENT(NotificationReplyAction)
QML_UNCREATABLE("")
QML_FOREIGN(KNotificationReplyAction);
};
class NotificationWrapper : public KNotification
{
Q_OBJECT
QML_NAMED_ELEMENT(Notification)
Q_PROPERTY(KNotificationReplyAction *replyAction READ replyActionFactory CONSTANT)
Q_PROPERTY(QQmlListProperty<KNotificationAction> actions READ actionsProperty NOTIFY actionsChanged)
Q_PROPERTY(KNotificationAction *defaultAction READ defaultAction WRITE setDefaultActionQml NOTIFY defaultActionChanged)
public:
explicit NotificationWrapper(QObject *parent = nullptr);
KNotificationReplyAction *replyActionFactory();
int actionCount() const;
KNotificationAction *actionAt(qsizetype index);
QQmlListProperty<KNotificationAction> actionsProperty() const;
static qsizetype actionsCount(QQmlListProperty<KNotificationAction> *list);
static void appendAction(QQmlListProperty<KNotificationAction> *list, KNotificationAction *value);
static KNotificationAction *actionAt(QQmlListProperty<KNotificationAction> *list, qsizetype index);
static void clearActions(QQmlListProperty<KNotificationAction> *list);
private:
QQmlListProperty<KNotificationAction> m_actionsProperty;
};
class NotificationPermissionWrapper : public QObject
{
Q_OBJECT
QML_NAMED_ELEMENT(NotificationPermission)
QML_SINGLETON
public:
Q_INVOKABLE bool checkPermission()
{
return KNotificationPermission::checkPermission() == Qt::PermissionStatus::Granted;
}
Q_INVOKABLE void requestPermission(const QJSValue &callback)
{
KNotificationPermission::requestPermission(this, [callback](Qt::PermissionStatus status) {
callback.call({status == Qt::PermissionStatus::Granted});
});
}
};
#endif // KNOTIFICATIONQMLPLUGIN_H