Roles/kde: remove patches

All of this patches are in 6.3, so they're not needed anymore
This commit is contained in:
Toast 2025-02-15 16:04:10 +01:00
parent 789157e3bf
commit 5640408d9f
10 changed files with 0 additions and 2973 deletions

File diff suppressed because it is too large Load diff

View file

@ -1,41 +0,0 @@
diff --git a/src/plugins/private/expolayout.cpp b/src/plugins/private/expolayout.cpp
index 52e8bd6616..d4f2a41acc 100644
--- a/src/plugins/private/expolayout.cpp
+++ b/src/plugins/private/expolayout.cpp
@@ -7,6 +7,7 @@
#include "expolayout.h"
+#include <QQmlProperty>
#include <cmath>
#include <deque>
#include <tuple>
@@ -41,6 +42,12 @@ ExpoCell::~ExpoCell()
void ExpoCell::componentComplete()
{
QQuickItem::componentComplete();
+
+ QQmlProperty xProperty(this, "Kirigami.ScenePosition.x", qmlContext(this));
+ xProperty.connectNotifySignal(this, SLOT(updateContentItemGeometry()));
+ QQmlProperty yProperty(this, "Kirigami.ScenePosition.y", qmlContext(this));
+ yProperty.connectNotifySignal(this, SLOT(updateContentItemGeometry()));
+
updateContentItemGeometry();
}
diff --git a/src/plugins/private/expolayout.h b/src/plugins/private/expolayout.h
index 0d4f9d5cde..d980c2aabb 100644
--- a/src/plugins/private/expolayout.h
+++ b/src/plugins/private/expolayout.h
@@ -264,8 +264,10 @@ Q_SIGNALS:
void persistentKeyChanged();
void bottomMarginChanged();
-private:
+private Q_SLOTS:
void updateContentItemGeometry();
+
+private:
void updateLayout();
QString m_persistentKey;

View file

@ -1,753 +0,0 @@
diff --git a/src/plugins/zoom/CMakeLists.txt b/src/plugins/zoom/CMakeLists.txt
index c27bdbbb2d..b5fb343e17 100644
--- a/src/plugins/zoom/CMakeLists.txt
+++ b/src/plugins/zoom/CMakeLists.txt
@@ -4,6 +4,7 @@
set(zoom_SOURCES
main.cpp
zoom.cpp
+ zoom.qrc
)
if (HAVE_ACCESSIBILITY)
diff --git a/src/plugins/zoom/shaders/pixelgrid.frag b/src/plugins/zoom/shaders/pixelgrid.frag
new file mode 100644
index 0000000000..1e9ee9431f
--- /dev/null
+++ b/src/plugins/zoom/shaders/pixelgrid.frag
@@ -0,0 +1,25 @@
+#include "colormanagement.glsl"
+
+uniform sampler2D sampler;
+uniform int textureWidth;
+uniform int textureHeight;
+
+varying vec2 texcoord0;
+
+void main()
+{
+ vec2 texSize = vec2(textureWidth, textureHeight);
+ vec2 samplePosition = texcoord0 * texSize;
+ vec2 pixelCenter = floor(samplePosition) + vec2(0.5);
+ vec2 pixelCenterDistance = abs(samplePosition - pixelCenter);
+
+ vec4 tex;
+ if (pixelCenterDistance.x > 0.4 || pixelCenterDistance.y > 0.4) {
+ tex = vec4(0, 0, 0, 1);
+ } else {
+ tex = texture2D(sampler, pixelCenter / texSize);
+ }
+
+ tex = sourceEncodingToNitsInDestinationColorspace(tex);
+ gl_FragColor = nitsToDestinationEncoding(tex);
+}
diff --git a/src/plugins/zoom/shaders/pixelgrid_core.frag b/src/plugins/zoom/shaders/pixelgrid_core.frag
new file mode 100644
index 0000000000..a1cc7f27f2
--- /dev/null
+++ b/src/plugins/zoom/shaders/pixelgrid_core.frag
@@ -0,0 +1,24 @@
+#version 140
+
+#include "colormanagement.glsl"
+
+uniform sampler2D sampler;
+uniform int textureWidth;
+uniform int textureHeight;
+
+in vec2 texcoord0;
+
+out vec4 fragColor;
+
+void main()
+{
+ vec2 texSize = vec2(textureWidth, textureHeight);
+ vec2 samplePosition = texcoord0 * texSize;
+ vec2 pixelCenter = floor(samplePosition) + vec2(0.5);
+ vec2 pixelCenterDistance = abs(samplePosition - pixelCenter);
+
+ float t = smoothstep(0.4, 0.5, max(pixelCenterDistance.x, pixelCenterDistance.y));
+ vec4 tex = mix(texture(sampler, pixelCenter / texSize), vec4(0, 0, 0, 1), t);
+ tex = sourceEncodingToNitsInDestinationColorspace(tex);
+ fragColor = nitsToDestinationEncoding(tex);
+}
diff --git a/src/plugins/zoom/ui_zoom_config.h b/src/plugins/zoom/ui_zoom_config.h
new file mode 100644
index 0000000000..7e5944a902
--- /dev/null
+++ b/src/plugins/zoom/ui_zoom_config.h
@@ -0,0 +1,195 @@
+/********************************************************************************
+** Form generated from reading UI file 'zoom_config.ui'
+**
+** Created by: Qt User Interface Compiler version 6.8.1
+**
+** WARNING! All changes made in this file will be lost when recompiling UI file!
+********************************************************************************/
+
+#ifndef UI_ZOOM_CONFIG_H
+#define UI_ZOOM_CONFIG_H
+
+#include <QtCore/QVariant>
+#include <QtWidgets/QApplication>
+#include <QtWidgets/QCheckBox>
+#include <QtWidgets/QComboBox>
+#include <QtWidgets/QDoubleSpinBox>
+#include <QtWidgets/QGridLayout>
+#include <QtWidgets/QGroupBox>
+#include <QtWidgets/QHBoxLayout>
+#include <QtWidgets/QLabel>
+#include <QtWidgets/QVBoxLayout>
+#include <QtWidgets/QWidget>
+#include "kshortcutseditor.h"
+
+namespace KWin {
+
+class Ui_ZoomEffectConfigForm
+{
+public:
+ QVBoxLayout *verticalLayout_2;
+ QGroupBox *groupSize;
+ QHBoxLayout *horizontalLayout;
+ QGridLayout *gridLayout;
+ QLabel *label;
+ QDoubleSpinBox *kcfg_ZoomFactor;
+ QCheckBox *kcfg_EnableFocusTracking;
+ QCheckBox *kcfg_EnableTextCaretTracking;
+ QLabel *label_2;
+ QComboBox *kcfg_MousePointer;
+ QComboBox *kcfg_MouseTracking;
+ QLabel *label_3;
+ KShortcutsEditor *editor;
+
+ void setupUi(QWidget *KWin__ZoomEffectConfigForm)
+ {
+ if (KWin__ZoomEffectConfigForm->objectName().isEmpty())
+ KWin__ZoomEffectConfigForm->setObjectName("KWin__ZoomEffectConfigForm");
+ KWin__ZoomEffectConfigForm->resize(304, 288);
+ verticalLayout_2 = new QVBoxLayout(KWin__ZoomEffectConfigForm);
+ verticalLayout_2->setObjectName("verticalLayout_2");
+ groupSize = new QGroupBox(KWin__ZoomEffectConfigForm);
+ groupSize->setObjectName("groupSize");
+ horizontalLayout = new QHBoxLayout(groupSize);
+ horizontalLayout->setObjectName("horizontalLayout");
+ gridLayout = new QGridLayout();
+ gridLayout->setObjectName("gridLayout");
+ label = new QLabel(groupSize);
+ label->setObjectName("label");
+ label->setAlignment(Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter);
+
+ gridLayout->addWidget(label, 0, 0, 1, 1);
+
+ kcfg_ZoomFactor = new QDoubleSpinBox(groupSize);
+ kcfg_ZoomFactor->setObjectName("kcfg_ZoomFactor");
+ kcfg_ZoomFactor->setDecimals(2);
+ kcfg_ZoomFactor->setMaximum(9999.000000000000000);
+ kcfg_ZoomFactor->setSingleStep(0.050000000000000);
+ kcfg_ZoomFactor->setValue(1.250000000000000);
+
+ gridLayout->addWidget(kcfg_ZoomFactor, 0, 1, 1, 1);
+
+ kcfg_EnableFocusTracking = new QCheckBox(groupSize);
+ kcfg_EnableFocusTracking->setObjectName("kcfg_EnableFocusTracking");
+
+ gridLayout->addWidget(kcfg_EnableFocusTracking, 4, 0, 1, 2);
+
+ kcfg_EnableTextCaretTracking = new QCheckBox(groupSize);
+ kcfg_EnableTextCaretTracking->setObjectName("kcfg_EnableTextCaretTracking");
+
+ gridLayout->addWidget(kcfg_EnableTextCaretTracking, 5, 0, 1, 2);
+
+ label_2 = new QLabel(groupSize);
+ label_2->setObjectName("label_2");
+ label_2->setAlignment(Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter);
+
+ gridLayout->addWidget(label_2, 2, 0, 1, 1);
+
+ kcfg_MousePointer = new QComboBox(groupSize);
+ kcfg_MousePointer->addItem(QString());
+ kcfg_MousePointer->addItem(QString());
+ kcfg_MousePointer->addItem(QString());
+ kcfg_MousePointer->setObjectName("kcfg_MousePointer");
+
+ gridLayout->addWidget(kcfg_MousePointer, 2, 1, 1, 1);
+
+ kcfg_MouseTracking = new QComboBox(groupSize);
+ kcfg_MouseTracking->addItem(QString());
+ kcfg_MouseTracking->addItem(QString());
+ kcfg_MouseTracking->addItem(QString());
+ kcfg_MouseTracking->addItem(QString());
+ kcfg_MouseTracking->setObjectName("kcfg_MouseTracking");
+
+ gridLayout->addWidget(kcfg_MouseTracking, 3, 1, 1, 1);
+
+ label_3 = new QLabel(groupSize);
+ label_3->setObjectName("label_3");
+ label_3->setAlignment(Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter);
+
+ gridLayout->addWidget(label_3, 3, 0, 1, 1);
+
+
+ horizontalLayout->addLayout(gridLayout);
+
+
+ verticalLayout_2->addWidget(groupSize);
+
+ editor = new KShortcutsEditor(KWin__ZoomEffectConfigForm);
+ editor->setObjectName("editor");
+ QSizePolicy sizePolicy(QSizePolicy::Policy::Preferred, QSizePolicy::Policy::Expanding);
+ sizePolicy.setHorizontalStretch(0);
+ sizePolicy.setVerticalStretch(0);
+ sizePolicy.setHeightForWidth(editor->sizePolicy().hasHeightForWidth());
+ editor->setSizePolicy(sizePolicy);
+ editor->setActionTypes(KShortcutsEditor::GlobalAction);
+
+ verticalLayout_2->addWidget(editor);
+
+#if QT_CONFIG(shortcut)
+ label->setBuddy(kcfg_ZoomFactor);
+ label_2->setBuddy(kcfg_MousePointer);
+ label_3->setBuddy(kcfg_MouseTracking);
+#endif // QT_CONFIG(shortcut)
+ QWidget::setTabOrder(kcfg_ZoomFactor, kcfg_MousePointer);
+ QWidget::setTabOrder(kcfg_MousePointer, kcfg_MouseTracking);
+ QWidget::setTabOrder(kcfg_MouseTracking, kcfg_EnableFocusTracking);
+ QWidget::setTabOrder(kcfg_EnableFocusTracking, kcfg_EnableTextCaretTracking);
+
+ retranslateUi(KWin__ZoomEffectConfigForm);
+
+ QMetaObject::connectSlotsByName(KWin__ZoomEffectConfigForm);
+ } // setupUi
+
+ void retranslateUi(QWidget *KWin__ZoomEffectConfigForm)
+ {
+ groupSize->setTitle(QString());
+#if QT_CONFIG(whatsthis)
+ label->setWhatsThis(QCoreApplication::translate("KWin::ZoomEffectConfigForm", "On zoom-in and zoom-out change the zoom by the defined zoom-factor.", nullptr));
+#endif // QT_CONFIG(whatsthis)
+ label->setText(QCoreApplication::translate("KWin::ZoomEffectConfigForm", "Zoom Factor:", nullptr));
+#if QT_CONFIG(whatsthis)
+ kcfg_ZoomFactor->setWhatsThis(QCoreApplication::translate("KWin::ZoomEffectConfigForm", "On zoom-in and zoom-out change the zoom by the defined zoom-factor.", nullptr));
+#endif // QT_CONFIG(whatsthis)
+ kcfg_ZoomFactor->setSuffix(QString());
+#if QT_CONFIG(tooltip)
+ kcfg_EnableFocusTracking->setToolTip(QString());
+#endif // QT_CONFIG(tooltip)
+#if QT_CONFIG(whatsthis)
+ kcfg_EnableFocusTracking->setWhatsThis(QCoreApplication::translate("KWin::ZoomEffectConfigForm", "Enable tracking of the focused location. This needs QAccessible to be enabled per application (\"export QT_LINUX_ACCESSIBILITY_ALWAYS_ON=1\").", nullptr));
+#endif // QT_CONFIG(whatsthis)
+ kcfg_EnableFocusTracking->setText(QCoreApplication::translate("KWin::ZoomEffectConfigForm", "Enable Focus Tracking", nullptr));
+#if QT_CONFIG(whatsthis)
+ kcfg_EnableTextCaretTracking->setWhatsThis(QCoreApplication::translate("KWin::ZoomEffectConfigForm", "Enable tracking of the text cursor. This needs QAccessible to be enabled per application (\"export QT_LINUX_ACCESSIBILITY_ALWAYS_ON=1\").", nullptr));
+#endif // QT_CONFIG(whatsthis)
+ kcfg_EnableTextCaretTracking->setText(QCoreApplication::translate("KWin::ZoomEffectConfigForm", "Enable Text Cursor Tracking", nullptr));
+ label_2->setText(QCoreApplication::translate("KWin::ZoomEffectConfigForm", "Mouse Pointer:", nullptr));
+ kcfg_MousePointer->setItemText(0, QCoreApplication::translate("KWin::ZoomEffectConfigForm", "Scale", nullptr));
+ kcfg_MousePointer->setItemText(1, QCoreApplication::translate("KWin::ZoomEffectConfigForm", "Keep", nullptr));
+ kcfg_MousePointer->setItemText(2, QCoreApplication::translate("KWin::ZoomEffectConfigForm", "Hide", nullptr));
+
+#if QT_CONFIG(whatsthis)
+ kcfg_MousePointer->setWhatsThis(QCoreApplication::translate("KWin::ZoomEffectConfigForm", "Visibility of the mouse-pointer.", nullptr));
+#endif // QT_CONFIG(whatsthis)
+ kcfg_MouseTracking->setItemText(0, QCoreApplication::translate("KWin::ZoomEffectConfigForm", "Proportional", nullptr));
+ kcfg_MouseTracking->setItemText(1, QCoreApplication::translate("KWin::ZoomEffectConfigForm", "Centered", nullptr));
+ kcfg_MouseTracking->setItemText(2, QCoreApplication::translate("KWin::ZoomEffectConfigForm", "Push", nullptr));
+ kcfg_MouseTracking->setItemText(3, QCoreApplication::translate("KWin::ZoomEffectConfigForm", "Disabled", nullptr));
+
+#if QT_CONFIG(whatsthis)
+ kcfg_MouseTracking->setWhatsThis(QCoreApplication::translate("KWin::ZoomEffectConfigForm", "Track moving of the mouse.", nullptr));
+#endif // QT_CONFIG(whatsthis)
+ label_3->setText(QCoreApplication::translate("KWin::ZoomEffectConfigForm", "Mouse Tracking:", nullptr));
+ (void)KWin__ZoomEffectConfigForm;
+ } // retranslateUi
+
+};
+
+} // namespace KWin
+
+namespace KWin {
+namespace Ui {
+ class ZoomEffectConfigForm: public Ui_ZoomEffectConfigForm {};
+} // namespace Ui
+} // namespace KWin
+
+#endif // UI_ZOOM_CONFIG_H
diff --git a/src/plugins/zoom/zoom.cpp b/src/plugins/zoom/zoom.cpp
index a6eb528767..950583c15b 100644
--- a/src/plugins/zoom/zoom.cpp
+++ b/src/plugins/zoom/zoom.cpp
@@ -31,6 +31,12 @@
using namespace std::chrono_literals;
+static void ensureResources()
+{
+ // Must initialize resources manually because the effect is a static lib.
+ Q_INIT_RESOURCE(zoom);
+}
+
namespace KWin
{
@@ -48,6 +54,8 @@ ZoomEffect::ZoomEffect()
, moveFactor(20.0)
, lastPresentTime(std::chrono::milliseconds::zero())
{
+ ensureResources();
+
ZoomConfig::instance(effects->config());
QAction *a = nullptr;
a = KStandardAction::zoomIn(this, SLOT(zoomIn()), this);
@@ -214,6 +222,7 @@ void ZoomEffect::reconfigure(ReconfigureFlags)
ZoomConfig::self()->read();
// On zoom-in and zoom-out change the zoom by the defined zoom-factor.
zoomFactor = std::max(0.1, ZoomConfig::zoomFactor());
+ m_pixelGridZoom = ZoomConfig::pixelGridZoom();
// Visibility of the mouse-pointer.
mousePointer = MousePointerType(ZoomConfig::mousePointer());
// Track moving of the mouse.
@@ -271,12 +280,10 @@ void ZoomEffect::prePaintScreen(ScreenPrePaintData &data, std::chrono::milliseco
ZoomEffect::OffscreenData *ZoomEffect::ensureOffscreenData(const RenderTarget &renderTarget, const RenderViewport &viewport, Output *screen)
{
- const QRect rect = viewport.renderRect().toRect();
- const qreal devicePixelRatio = viewport.scale();
- const QSize nativeSize = (viewport.renderRect().size() * devicePixelRatio).toSize();
+ const QSize nativeSize = renderTarget.size();
OffscreenData &data = m_offscreenData[effects->waylandDisplay() ? screen : nullptr];
- data.viewport = rect;
+ data.viewport = viewport.renderRect();
data.color = renderTarget.colorDescription();
const GLenum textureFormat = renderTarget.colorDescription() == ColorDescription::sRGB ? GL_RGBA8 : GL_RGBA16F;
@@ -290,9 +297,22 @@ ZoomEffect::OffscreenData *ZoomEffect::ensureOffscreenData(const RenderTarget &r
data.framebuffer = std::make_unique<GLFramebuffer>(data.texture.get());
}
+ data.texture->setContentTransform(renderTarget.transform());
return &data;
}
+GLShader *ZoomEffect::shaderForZoom(double zoom)
+{
+ if (zoom < m_pixelGridZoom) {
+ return ShaderManager::instance()->shader(ShaderTrait::MapTexture | ShaderTrait::TransformColorspace);
+ } else {
+ if (!m_pixelGridShader) {
+ m_pixelGridShader = ShaderManager::instance()->generateShaderFromFile(ShaderTrait::MapTexture, QString(), QStringLiteral(":/effects/zoom/shaders/pixelgrid.frag"));
+ }
+ return m_pixelGridShader.get();
+ }
+}
+
void ZoomEffect::paintScreen(const RenderTarget &renderTarget, const RenderViewport &viewport, int mask, const QRegion &region, Output *screen)
{
OffscreenData *offscreenData = ensureOffscreenData(renderTarget, viewport, screen);
@@ -391,7 +411,8 @@ void ZoomEffect::paintScreen(const RenderTarget &renderTarget, const RenderViewp
glClearColor(0.0, 0.0, 0.0, 0.0);
glClear(GL_COLOR_BUFFER_BIT);
- auto shader = ShaderManager::instance()->pushShader(ShaderTrait::MapTexture | ShaderTrait::TransformColorspace);
+ GLShader *shader = shaderForZoom(zoom);
+ ShaderManager::instance()->pushShader(shader);
for (auto &[screen, offscreen] : m_offscreenData) {
QMatrix4x4 matrix;
matrix.translate(xTranslation * scale, yTranslation * scale);
@@ -399,6 +420,8 @@ void ZoomEffect::paintScreen(const RenderTarget &renderTarget, const RenderViewp
matrix.translate(offscreen.viewport.x() * scale, offscreen.viewport.y() * scale);
shader->setUniform(GLShader::Mat4Uniform::ModelViewProjectionMatrix, viewport.projectionMatrix() * matrix);
+ shader->setUniform(GLShader::IntUniform::TextureWidth, offscreen.texture->width());
+ shader->setUniform(GLShader::IntUniform::TextureHeight, offscreen.texture->height());
shader->setColorspaceUniforms(offscreen.color, renderTarget.colorDescription(), RenderingIntent::Perceptual);
offscreen.texture->render(offscreen.viewport.size() * scale);
diff --git a/src/plugins/zoom/zoom.h b/src/plugins/zoom/zoom.h
index 2a44395d74..1c9abb4b4e 100644
--- a/src/plugins/zoom/zoom.h
+++ b/src/plugins/zoom/zoom.h
@@ -26,6 +26,7 @@ class ZoomAccessibilityIntegration;
class GLFramebuffer;
class GLTexture;
class GLVertexBuffer;
+class GLShader;
class ZoomEffect
: public Effect
@@ -94,7 +95,7 @@ private:
{
std::unique_ptr<GLTexture> texture;
std::unique_ptr<GLFramebuffer> framebuffer;
- QRect viewport;
+ QRectF viewport;
ColorDescription color = ColorDescription::sRGB;
};
@@ -102,6 +103,8 @@ private:
OffscreenData *ensureOffscreenData(const RenderTarget &renderTarget, const RenderViewport &viewport, Output *screen);
void markCursorTextureDirty();
+ GLShader *shaderForZoom(double zoom);
+
#if HAVE_ACCESSIBILITY
ZoomAccessibilityIntegration *m_accessibilityIntegration = nullptr;
#endif
@@ -136,6 +139,8 @@ private:
double moveFactor;
std::chrono::milliseconds lastPresentTime;
std::map<Output *, OffscreenData> m_offscreenData;
+ std::unique_ptr<GLShader> m_pixelGridShader;
+ double m_pixelGridZoom;
};
} // namespace
diff --git a/src/plugins/zoom/zoom.kcfg b/src/plugins/zoom/zoom.kcfg
index 63887e32aa..ed43e225c8 100644
--- a/src/plugins/zoom/zoom.kcfg
+++ b/src/plugins/zoom/zoom.kcfg
@@ -29,5 +29,8 @@
<entry name="InitialZoom" type="Double">
<default>1.0</default>
</entry>
+ <entry name="PixelGridZoom" type="Double">
+ <default>15.0</default>
+ </entry>
</group>
</kcfg>
diff --git a/src/plugins/zoom/zoom.qrc b/src/plugins/zoom/zoom.qrc
new file mode 100644
index 0000000000..56e7369003
--- /dev/null
+++ b/src/plugins/zoom/zoom.qrc
@@ -0,0 +1,6 @@
+<!DOCTYPE RCC><RCC version="1.0">
+<qresource prefix="/effects/zoom/">
+ <file>shaders/pixelgrid.frag</file>
+ <file>shaders/pixelgrid_core.frag</file>
+</qresource>
+</RCC>
diff --git a/src/plugins/zoom/zoom_config.ui b/src/plugins/zoom/zoom_config.ui
index 454f9f9161..4b8a5c000e 100644
--- a/src/plugins/zoom/zoom_config.ui
+++ b/src/plugins/zoom/zoom_config.ui
@@ -6,159 +6,157 @@
<rect>
<x>0</x>
<y>0</y>
- <width>304</width>
- <height>288</height>
+ <width>595</width>
+ <height>551</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
- <widget class="QGroupBox" name="groupSize">
- <property name="title">
- <string/>
+ <layout class="QFormLayout" name="formLayout">
+ <property name="formAlignment">
+ <set>Qt::AlignHCenter|Qt::AlignTop</set>
</property>
- <layout class="QHBoxLayout" name="horizontalLayout">
- <item>
- <layout class="QGridLayout" name="gridLayout">
- <item row="0" column="0">
- <widget class="QLabel" name="label">
- <property name="whatsThis">
- <string>On zoom-in and zoom-out change the zoom by the defined zoom-factor.</string>
- </property>
- <property name="text">
- <string>Zoom Factor:</string>
- </property>
- <property name="alignment">
- <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
- </property>
- <property name="buddy">
- <cstring>kcfg_ZoomFactor</cstring>
- </property>
- </widget>
- </item>
- <item row="0" column="1">
- <widget class="QDoubleSpinBox" name="kcfg_ZoomFactor">
- <property name="whatsThis">
- <string>On zoom-in and zoom-out change the zoom by the defined zoom-factor.</string>
- </property>
- <property name="suffix">
- <string/>
- </property>
- <property name="decimals">
- <number>2</number>
- </property>
- <property name="maximum">
- <double>9999.000000000000000</double>
- </property>
- <property name="singleStep">
- <double>0.050000000000000</double>
- </property>
- <property name="value">
- <double>1.250000000000000</double>
- </property>
- </widget>
- </item>
- <item row="4" column="0" colspan="2">
- <widget class="QCheckBox" name="kcfg_EnableFocusTracking">
- <property name="toolTip">
- <string/>
- </property>
- <property name="whatsThis">
- <string>Enable tracking of the focused location. This needs QAccessible to be enabled per application (&quot;export QT_LINUX_ACCESSIBILITY_ALWAYS_ON=1&quot;).</string>
- </property>
- <property name="text">
- <string>Enable Focus Tracking</string>
- </property>
- </widget>
- </item>
- <item row="5" column="0" colspan="2">
- <widget class="QCheckBox" name="kcfg_EnableTextCaretTracking">
- <property name="whatsThis">
- <string>Enable tracking of the text cursor. This needs QAccessible to be enabled per application (&quot;export QT_LINUX_ACCESSIBILITY_ALWAYS_ON=1&quot;).</string>
- </property>
- <property name="text">
- <string>Enable Text Cursor Tracking</string>
- </property>
- </widget>
- </item>
- <item row="2" column="0">
- <widget class="QLabel" name="label_2">
- <property name="text">
- <string>Mouse Pointer:</string>
- </property>
- <property name="alignment">
- <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
- </property>
- <property name="buddy">
- <cstring>kcfg_MousePointer</cstring>
- </property>
- </widget>
- </item>
- <item row="2" column="1">
- <widget class="QComboBox" name="kcfg_MousePointer">
- <property name="whatsThis">
- <string>Visibility of the mouse-pointer.</string>
- </property>
- <item>
- <property name="text">
- <string>Scale</string>
- </property>
- </item>
- <item>
- <property name="text">
- <string>Keep</string>
- </property>
- </item>
- <item>
- <property name="text">
- <string>Hide</string>
- </property>
- </item>
- </widget>
- </item>
- <item row="3" column="1">
- <widget class="QComboBox" name="kcfg_MouseTracking">
- <property name="whatsThis">
- <string>Track moving of the mouse.</string>
- </property>
- <item>
- <property name="text">
- <string>Proportional</string>
- </property>
- </item>
- <item>
- <property name="text">
- <string>Centered</string>
- </property>
- </item>
- <item>
- <property name="text">
- <string>Push</string>
- </property>
- </item>
- <item>
- <property name="text">
- <string>Disabled</string>
- </property>
- </item>
- </widget>
- </item>
- <item row="3" column="0">
- <widget class="QLabel" name="label_3">
- <property name="text">
- <string>Mouse Tracking:</string>
- </property>
- <property name="alignment">
- <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
- </property>
- <property name="buddy">
- <cstring>kcfg_MouseTracking</cstring>
- </property>
- </widget>
- </item>
- </layout>
- </item>
- </layout>
- </widget>
+ <item row="0" column="0">
+ <widget class="QLabel" name="label">
+ <property name="whatsThis">
+ <string>On zoom-in and zoom-out change the zoom by the defined zoom-factor.</string>
+ </property>
+ <property name="text">
+ <string>Zoom factor:</string>
+ </property>
+ <property name="buddy">
+ <cstring>kcfg_ZoomFactor</cstring>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QDoubleSpinBox" name="kcfg_ZoomFactor">
+ <property name="whatsThis">
+ <string>On zoom-in and zoom-out change the zoom by the defined zoom-factor.</string>
+ </property>
+ <property name="suffix">
+ <string/>
+ </property>
+ <property name="decimals">
+ <number>2</number>
+ </property>
+ <property name="maximum">
+ <double>9999.000000000000000</double>
+ </property>
+ <property name="singleStep">
+ <double>0.050000000000000</double>
+ </property>
+ <property name="value">
+ <double>1.250000000000000</double>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0">
+ <widget class="QLabel" name="label_4">
+ <property name="text">
+ <string>Show pixel grid at zoom level:</string>
+ </property>
+ <property name="buddy">
+ <cstring>kcfg_PixelGridZoom</cstring>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <widget class="QDoubleSpinBox" name="kcfg_PixelGridZoom"/>
+ </item>
+ <item row="2" column="0">
+ <widget class="QLabel" name="label_2">
+ <property name="text">
+ <string>Mouse pointer:</string>
+ </property>
+ <property name="buddy">
+ <cstring>kcfg_MousePointer</cstring>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="1">
+ <widget class="QComboBox" name="kcfg_MousePointer">
+ <property name="whatsThis">
+ <string>Visibility of the mouse-pointer.</string>
+ </property>
+ <item>
+ <property name="text">
+ <string>Scale</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Keep</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Hide</string>
+ </property>
+ </item>
+ </widget>
+ </item>
+ <item row="3" column="0">
+ <widget class="QLabel" name="label_3">
+ <property name="text">
+ <string>Mouse tracking:</string>
+ </property>
+ <property name="buddy">
+ <cstring>kcfg_MouseTracking</cstring>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="1">
+ <widget class="QComboBox" name="kcfg_MouseTracking">
+ <property name="whatsThis">
+ <string>Track moving of the mouse.</string>
+ </property>
+ <item>
+ <property name="text">
+ <string>Proportional</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Centered</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Push</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Disabled</string>
+ </property>
+ </item>
+ </widget>
+ </item>
+ <item row="4" column="1">
+ <widget class="QCheckBox" name="kcfg_EnableFocusTracking">
+ <property name="toolTip">
+ <string/>
+ </property>
+ <property name="whatsThis">
+ <string>Enable tracking of the focused location. This needs QAccessible to be enabled per application (&quot;export QT_LINUX_ACCESSIBILITY_ALWAYS_ON=1&quot;).</string>
+ </property>
+ <property name="text">
+ <string>Enable focus tracking</string>
+ </property>
+ </widget>
+ </item>
+ <item row="5" column="1">
+ <widget class="QCheckBox" name="kcfg_EnableTextCaretTracking">
+ <property name="whatsThis">
+ <string>Enable tracking of the text cursor. This needs QAccessible to be enabled per application (&quot;export QT_LINUX_ACCESSIBILITY_ALWAYS_ON=1&quot;).</string>
+ </property>
+ <property name="text">
+ <string>Enable text cursor tracking</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
</item>
<item>
<widget class="KShortcutsEditor" name="editor">
@@ -183,13 +181,6 @@
<container>1</container>
</customwidget>
</customwidgets>
- <tabstops>
- <tabstop>kcfg_ZoomFactor</tabstop>
- <tabstop>kcfg_MousePointer</tabstop>
- <tabstop>kcfg_MouseTracking</tabstop>
- <tabstop>kcfg_EnableFocusTracking</tabstop>
- <tabstop>kcfg_EnableTextCaretTracking</tabstop>
- </tabstops>
<resources/>
<connections/>
</ui>

View file

@ -1,41 +0,0 @@
From 45a5d8844b36404334301f5da6e75f1a345e0c80 Mon Sep 17 00:00:00 2001
From: Xaver Hugl <xaver.hugl@gmail.com>
Date: Fri, 10 Jan 2025 13:45:30 +0000
Subject: [PATCH] plugins/screencast: call ItemRenderer::begin/endFrame
The OpenGL renderer references the explicit sync release points for client buffers
during rendering, and releases them in endFrame. If endFrame never gets called though
(for example because we're doing direct scanout) then the release points never get
signaled, and the client very quickly runs out of buffers to use and freezes.
BUG: 495287
(cherry picked from commit b1031ea63eaa8c9bf5c70157d1b6bf8eb0f5a74a)
Co-authored-by: Xaver Hugl <xaver.hugl@gmail.com>
---
src/plugins/screencast/windowscreencastsource.cpp | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/plugins/screencast/windowscreencastsource.cpp b/src/plugins/screencast/windowscreencastsource.cpp
index 24ef92aad7b..b396eed46f9 100644
--- a/src/plugins/screencast/windowscreencastsource.cpp
+++ b/src/plugins/screencast/windowscreencastsource.cpp
@@ -75,11 +75,11 @@ void WindowScreenCastSource::render(GLFramebuffer *target)
RenderTarget renderTarget(target);
RenderViewport viewport(m_window->clientGeometry(), 1, renderTarget);
- GLFramebuffer::pushFramebuffer(target);
+ Compositor::self()->scene()->renderer()->beginFrame(renderTarget, viewport);
glClearColor(0.0, 0.0, 0.0, 0.0);
glClear(GL_COLOR_BUFFER_BIT);
Compositor::self()->scene()->renderer()->renderItem(renderTarget, viewport, m_window->windowItem(), Scene::PAINT_WINDOW_TRANSFORMED, infiniteRegion(), WindowPaintData{});
- GLFramebuffer::popFramebuffer();
+ Compositor::self()->scene()->renderer()->endFrame();
}
std::chrono::nanoseconds WindowScreenCastSource::clock() const
--
GitLab

View file

@ -1,48 +0,0 @@
From 626a52ae30dcda0ca407d1de02e30fcf5c109862 Mon Sep 17 00:00:00 2001
From: Tem PQD <variable_valuables761@simplelogin.com>
Date: Sun, 27 Oct 2024 03:54:44 +0000
Subject: [PATCH] applets/taskmanager: Make group indicator icon follow accent
color
The green plus icon is pretty eye-catching, and normally green plus buttons indicate something is going to be added or created. Change this to follow the user-selected color scheme, also matching the focus/active indicator color.
---
src/desktoptheme/breeze/widgets/tasks.svg | Bin 78569 -> 78717 bytes
1 file changed, 0 insertions(+), 0 deletions(-)
diff --git a/src/desktoptheme/breeze/widgets/tasks.svg b/src/desktoptheme/breeze/widgets/tasks.svg
index b76d1a345fe59ca48cc3c52de53495202734d00f..c60707355f082749661ffc059744a0f2b0f94eee 100644
--- a/src/desktoptheme/breeze/widgets/tasks.svg
+++ b/src/desktoptheme/breeze/widgets/tasks.svg
@@ -952,25 +952,25 @@
</g>
<g id="group-expander-bottom" transform="matrix(1.2222222 0 0 1.2222222 -97 -16.777778)">
<path id="path4927" d="m211.5 89.000002a4.4999957 4.5000003 0 0 0-4.5 4.5 4.4999957 4.5000003 0 0 0 4.5 4.5 4.4999957 4.5000003 0 0 0 4.5-4.5 4.4999957 4.5000003 0 0 0-4.5-4.5z" opacity=".25"/>
- <path id="path4931" d="m211.50001 89.818184a3.6818164 3.6818182 0 0 0-3.68182 3.681818 3.6818164 3.6818182 0 0 0 3.68182 3.681818 3.6818164 3.6818182 0 0 0 3.68182-3.681818 3.6818164 3.6818182 0 0 0-3.68182-3.681818z" fill="#2ecc71"/>
+ <path id="path4931" class="ColorScheme-ButtonFocus" d="m211.50001 89.818184a3.6818164 3.6818182 0 0 0-3.68182 3.681818 3.6818164 3.6818182 0 0 0 3.68182 3.681818 3.6818164 3.6818182 0 0 0 3.68182-3.681818 3.6818164 3.6818182 0 0 0-3.68182-3.681818z" fill="currentColor"/>
<path id="path4930" d="m210.67561 91.045464.00097 1.630631-1.63462.01145.007 1.626089 1.63908.0091-.006 1.634313 1.62575-.005.007-1.631618 1.63976-.000027v-1.632877h-1.64636l.0204-1.642077z" opacity=".25"/>
<path id="path4949" d="m211.08482 91.454555.00097 1.630633-1.63474.01145.007.807899 1.6392.0091-.006 1.634319.8077-.005.007-1.631624 1.63951-.000027v-.814687h-1.64611l.0204-1.642079z" fill="#fff"/>
</g>
<g id="group-expander-left" transform="matrix(1.2222222 0 0 1.2222222 -74.999995 -16.777778)">
<path id="path4934" d="m211.5 89.000002a4.4999957 4.5000003 0 0 0-4.5 4.5 4.4999957 4.5000003 0 0 0 4.5 4.5 4.4999957 4.5000003 0 0 0 4.5-4.5 4.4999957 4.5000003 0 0 0-4.5-4.5z" opacity=".25"/>
- <path id="path4938" d="m211.50001 89.818184a3.6818164 3.6818182 0 0 0-3.68182 3.681818 3.6818164 3.6818182 0 0 0 3.68182 3.681818 3.6818164 3.6818182 0 0 0 3.68182-3.681818 3.6818164 3.6818182 0 0 0-3.68182-3.681818z" fill="#2ecc71"/>
+ <path id="path4938" class="ColorScheme-ButtonFocus" d="m211.50001 89.818184a3.6818164 3.6818182 0 0 0-3.68182 3.681818 3.6818164 3.6818182 0 0 0 3.68182 3.681818 3.6818164 3.6818182 0 0 0 3.68182-3.681818 3.6818164 3.6818182 0 0 0-3.68182-3.681818z" fill="currentColor"/>
<path id="path4940" d="m210.67561 91.045464.00097 1.630631-1.63462.01145.007 1.626089 1.63908.0091-.006 1.634313 1.62575-.005.007-1.631618 1.63976-.000027v-1.632877h-1.64636l.0204-1.642077z" opacity=".25"/>
<path id="path4942" d="m211.08482 91.454555.00097 1.630633-1.63474.01145.007.807899 1.6392.0091-.006 1.634319.8077-.005.007-1.631624 1.63951-.000027v-.814687h-1.64611l.0204-1.642079z" fill="#fff"/>
</g>
<g id="group-expander-top" transform="matrix(1.2222222 0 0 1.2222222 -97 1.222222)">
<path id="path4946" d="m211.5 89.000002a4.4999957 4.5000003 0 0 0-4.5 4.5 4.4999957 4.5000003 0 0 0 4.5 4.5 4.4999957 4.5000003 0 0 0 4.5-4.5 4.4999957 4.5000003 0 0 0-4.5-4.5z" opacity=".25"/>
- <path id="path4950" d="m211.50001 89.818184a3.6818164 3.6818182 0 0 0-3.68182 3.681818 3.6818164 3.6818182 0 0 0 3.68182 3.681818 3.6818164 3.6818182 0 0 0 3.68182-3.681818 3.6818164 3.6818182 0 0 0-3.68182-3.681818z" fill="#2ecc71"/>
+ <path id="path4950" class="ColorScheme-ButtonFocus" d="m211.50001 89.818184a3.6818164 3.6818182 0 0 0-3.68182 3.681818 3.6818164 3.6818182 0 0 0 3.68182 3.681818 3.6818164 3.6818182 0 0 0 3.68182-3.681818 3.6818164 3.6818182 0 0 0-3.68182-3.681818z" fill="currentColor"/>
<path id="path4952" d="m210.67561 91.045464.00097 1.630631-1.63462.01145.007 1.626089 1.63908.0091-.006 1.634313 1.62575-.005.007-1.631618 1.63976-.000027v-1.632877h-1.64636l.0204-1.642077z" opacity=".25"/>
<path id="path4954" d="m211.08482 91.454555.00097 1.630633-1.63474.01145.007.807899 1.6392.0091-.006 1.634319.8077-.005.007-1.631624 1.63951-.000027v-.814687h-1.64611l.0204-1.642079z" fill="#fff"/>
</g>
<g id="group-expander-right" transform="matrix(1.2222222 0 0 1.2222222 -74.999995 1.222222)">
<path id="path4958" d="m211.5 89.000002a4.4999957 4.5000003 0 0 0-4.5 4.5 4.4999957 4.5000003 0 0 0 4.5 4.5 4.4999957 4.5000003 0 0 0 4.5-4.5 4.4999957 4.5000003 0 0 0-4.5-4.5z" opacity=".25"/>
- <path id="path4962" d="m211.50001 89.818184a3.6818164 3.6818182 0 0 0-3.68182 3.681818 3.6818164 3.6818182 0 0 0 3.68182 3.681818 3.6818164 3.6818182 0 0 0 3.68182-3.681818 3.6818164 3.6818182 0 0 0-3.68182-3.681818z" fill="#2ecc71"/>
+ <path id="path4962" class="ColorScheme-ButtonFocus" d="m211.50001 89.818184a3.6818164 3.6818182 0 0 0-3.68182 3.681818 3.6818164 3.6818182 0 0 0 3.68182 3.681818 3.6818164 3.6818182 0 0 0 3.68182-3.681818 3.6818164 3.6818182 0 0 0-3.68182-3.681818z" fill="currentColor"/>
<path id="path4964" d="m210.67561 91.045464.00097 1.630631-1.63462.01145.007 1.626089 1.63908.0091-.006 1.634313 1.62575-.005.007-1.631618 1.63976-.000027v-1.632877h-1.64636l.0204-1.642077z" opacity=".25"/>
<path id="path4966" d="m211.08482 91.454555.00097 1.630633-1.63474.01145.007.807899 1.6392.0091-.006 1.634319.8077-.005.007-1.631624 1.63951-.000027v-.814687h-1.64611l.0204-1.642079z" fill="#fff"/>
</g>
--
GitLab

View file

@ -1,39 +0,0 @@
From 5301b211fc87f1b2253e87da61ff82618be9e899 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Niccol=C3=B2=20Venerandi?= <niccolo@venerandi.com>
Date: Thu, 5 Dec 2024 16:12:58 +0100
Subject: [PATCH] Only return valid task item size if task manager has been
resized to fit panel
Previously the task manager would briefly have null width and height, before
being resized to fit the panel. However, the "preferredMaxWidth" for tasks
would still be positive, as it adds margins to it, and tasks would then
assume a positive width. When the proper values are set, this casuses
an extra resize animation.
CCBUG:447476
---
.../taskmanager/package/contents/ui/code/layoutmetrics.js | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/applets/taskmanager/package/contents/ui/code/layoutmetrics.js b/applets/taskmanager/package/contents/ui/code/layoutmetrics.js
index c7f11c99a6..895cc725d2 100644
--- a/applets/taskmanager/package/contents/ui/code/layoutmetrics.js
+++ b/applets/taskmanager/package/contents/ui/code/layoutmetrics.js
@@ -63,8 +63,14 @@ function preferredMinWidth() {
function preferredMaxWidth() {
if (tasks.iconsOnly) {
if (tasks.vertical) {
+ if (tasks.width === 0) {
+ return 0
+ }
return tasks.width + verticalMargins();
} else {
+ if (tasks.height === 0) {
+ return 0
+ }
return tasks.height + horizontalMargins();
}
}
--
GitLab

View file

@ -1,40 +0,0 @@
From 11e7f5306fa013ec5c2b894a28457dabf5c42bad Mon Sep 17 00:00:00 2001
From: Nate Graham <nate@kde.org>
Date: Wed, 30 Oct 2024 21:55:49 -0600
Subject: [PATCH] Merge "Settings" menu category into "System"
This category is a bit of an odd duck. On a default install, it contains
only one item: System Settings. Random other apps you install will also
appear there, but all of these could just as logically live in the
"System" category. Let's put them there, so as to consolidate two
nebulous overlapping categories into one clear and obvious one.
---
menu/desktop/plasma-applications.menu | 8 +-------
1 file changed, 1 insertion(+), 7 deletions(-)
diff --git a/menu/desktop/plasma-applications.menu b/menu/desktop/plasma-applications.menu
index e153604252e..85f842d6a04 100644
--- a/menu/desktop/plasma-applications.menu
+++ b/menu/desktop/plasma-applications.menu
@@ -334,17 +334,11 @@
</Include>
</Menu>
</Menu>
- <Menu>
- <Name>Settingsmenu</Name>
- <Directory>kf5-settingsmenu.directory</Directory>
- <Include>
- <Category>Settings</Category>
- </Include>
- </Menu>
<Menu>
<Name>System</Name>
<Directory>kf5-system.directory</Directory>
<Include>
+ <Category>Settings</Category>
<And>
<Category>System</Category>
<Not><Category>X-KDE-More</Category></Not>
--
GitLab

View file

@ -1,915 +0,0 @@
diff --git a/appiumtests/applets/CMakeLists.txt b/appiumtests/applets/CMakeLists.txt
index 19229541aa..6715dd1f8f 100644
--- a/appiumtests/applets/CMakeLists.txt
+++ b/appiumtests/applets/CMakeLists.txt
@@ -59,7 +59,7 @@ add_test(
NAME notificationstest
COMMAND selenium-webdriver-at-spi-run ${CMAKE_CURRENT_SOURCE_DIR}/notificationstest.py --failfast
)
-set_tests_properties(notificationstest PROPERTIES TIMEOUT 120)
+set_tests_properties(notificationstest PROPERTIES TIMEOUT 120 ENVIRONMENT "KACTIVITYMANAGERD_PATH=${KDE_INSTALL_FULL_LIBEXECDIR}/kactivitymanagerd;USE_CUSTOM_BUS=1")
add_test(
NAME digitalclocktest
diff --git a/appiumtests/applets/kicker/favoritetest.py b/appiumtests/applets/kicker/favoritetest.py
new file mode 100755
index 0000000000..50613c3db1
--- /dev/null
+++ b/appiumtests/applets/kicker/favoritetest.py
@@ -0,0 +1,162 @@
+#!/usr/bin/env python3
+
+# SPDX-License-Identifier: BSD-3-Clause
+# SPDX-FileCopyrightText: 2022-2023 Harald Sitter <sitter@kde.org>
+
+import logging
+import os
+import shutil
+import subprocess
+import sys
+import tempfile
+import time
+import unittest
+from typing import Final
+
+from gi.repository import Gio, GLib
+
+sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir, os.pardir, "utils"))
+from GLibMainLoopThread import GLibMainLoopThread
+
+KDE_VERSION: Final = 6
+KACTIVITYMANAGERD_SERVICE_NAME: Final = "org.kde.ActivityManager"
+KACTIVITYMANAGERD_PATH: Final = os.getenv("KACTIVITYMANAGERD_PATH", "/usr/libexec/kactivitymanagerd")
+QMLTEST_EXEC: Final = os.getenv("QMLTEST_EXEC", "/usr/bin/qmltestrunner6")
+
+
+def name_has_owner(session_bus: Gio.DBusConnection, name: str) -> bool:
+ """
+ Whether the given name is available on session bus
+ """
+ message: Gio.DBusMessage = Gio.DBusMessage.new_method_call("org.freedesktop.DBus", "/", "org.freedesktop.DBus", "NameHasOwner")
+ message.set_body(GLib.Variant("(s)", [name]))
+ reply, _ = session_bus.send_message_with_reply_sync(message, Gio.DBusSendMessageFlags.NONE, 1000)
+ return reply and reply.get_signature() == 'b' and reply.get_body().get_child_value(0).get_boolean()
+
+
+def build_ksycoca() -> None:
+ subprocess.check_call([f"kbuildsycoca{KDE_VERSION}"], stdout=sys.stderr, stderr=sys.stderr, env=os.environ)
+
+
+def start_kactivitymanagerd() -> subprocess.Popen:
+ session_bus: Gio.DBusConnection = Gio.bus_get_sync(Gio.BusType.SESSION)
+ assert not name_has_owner(session_bus, KACTIVITYMANAGERD_SERVICE_NAME)
+
+ os.makedirs(os.path.join(GLib.get_user_config_dir(), "menus"))
+ shutil.copy(os.path.join(os.path.dirname(os.path.abspath(__file__)), "applications.menu"), os.path.join(GLib.get_user_config_dir(), "menus"))
+
+ kactivitymanagerd = subprocess.Popen([KACTIVITYMANAGERD_PATH], stdout=sys.stderr, stderr=sys.stderr, env=os.environ)
+ kactivitymanagerd_started: bool = False
+ for _ in range(10):
+ if name_has_owner(session_bus, KACTIVITYMANAGERD_SERVICE_NAME):
+ kactivitymanagerd_started = True
+ break
+ logging.info("waiting for kactivitymanagerd to appear on the DBus session")
+ time.sleep(1)
+ assert kactivitymanagerd_started
+
+ build_ksycoca()
+
+ return kactivitymanagerd
+
+
+class TestDBusInterface:
+ """
+ D-Bus interface for org.kde.kickertest
+ """
+
+ BUS_NAME: Final = "org.kde.kickertest"
+ OBJECT_PATH: Final = "/test"
+ INTERFACE_NAME: Final = "org.kde.kickertest"
+
+ connection: Gio.DBusConnection
+
+ def __init__(self) -> None:
+ self.reg_id: int = 0
+ self.owner_id: int = Gio.bus_own_name(Gio.BusType.SESSION, self.BUS_NAME, Gio.BusNameOwnerFlags.NONE, self.on_bus_acquired, None, None)
+ assert self.owner_id > 0
+
+ def on_bus_acquired(self, connection: Gio.DBusConnection, name: str, *args) -> None:
+ """
+ The interface is ready, now register objects.
+ """
+ self.connection = connection
+ introspection_data = Gio.DBusNodeInfo.new_for_xml("""
+<!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.kde.kickertest">
+ <method name="DeleteAndRebuildDatabase1"/>
+ <method name="DeleteAndRebuildDatabase2"/>
+ </interface>
+</node>
+""")
+ self.reg_id = connection.register_object(self.OBJECT_PATH, introspection_data.interfaces[0], self.handle_method_call, None, None)
+ assert self.reg_id > 0
+ logging.info("interface registered")
+
+ def handle_method_call(self, connection: Gio.DBusConnection, sender: str, object_path: str, interface_name: str, method_name: str, parameters: GLib.Variant, invocation: Gio.DBusMethodInvocation) -> None:
+ logging.info("method call %s", method_name)
+
+ if method_name == "DeleteAndRebuildDatabase1":
+ os.remove(KickerTest.desktop_entry_1)
+ build_ksycoca()
+ invocation.return_value(None)
+ elif method_name == "DeleteAndRebuildDatabase2":
+ os.remove(KickerTest.desktop_entry_2)
+ build_ksycoca()
+ invocation.return_value(None)
+
+
+class KickerTest(unittest.TestCase):
+ kactivitymanagerd: subprocess.Popen
+ loop_thread: GLibMainLoopThread
+ dbus_interface: TestDBusInterface
+
+ temp_dir: tempfile.TemporaryDirectory
+ desktop_entry_1: str
+ desktop_entry_2: str
+
+ @classmethod
+ def setUpClass(cls) -> None:
+ # Prepare desktop files
+ # 1
+ os.makedirs(os.path.join(GLib.get_user_data_dir(), "applications"))
+ shutil.copy(os.path.join(os.path.dirname(os.path.abspath(__file__)), "kickertest.desktop"), os.path.join(GLib.get_user_data_dir(), "applications"))
+ cls.desktop_entry_1 = os.path.join(GLib.get_user_data_dir(), "applications", "kickertest.desktop")
+ # 2
+ cls.temp_dir = tempfile.TemporaryDirectory()
+ os.makedirs(os.path.join(cls.temp_dir.name, "applications"))
+ shutil.copy(os.path.join(os.path.dirname(os.path.abspath(__file__)), "kickertest.desktop"), os.path.join(cls.temp_dir.name, "applications"))
+ cls.desktop_entry_2 = os.path.join(cls.temp_dir.name, "applications", "kickertest.desktop")
+
+ os.environ["LC_ALL"] = "en_US.UTF-8"
+ os.environ["QT_LOGGING_RULES"] = "org.kde.plasma.kicker.debug=true;kf.coreaddons.kdirwatch.debug=true"
+ os.environ["XDG_DATA_DIRS"] = os.environ["XDG_DATA_DIRS"] + ":" + cls.temp_dir.name
+
+ cls.kactivitymanagerd = start_kactivitymanagerd()
+
+ cls.loop_thread = GLibMainLoopThread()
+ cls.loop_thread.start()
+ cls.dbus_interface = TestDBusInterface()
+
+ @classmethod
+ def tearDownClass(cls) -> None:
+ cls.loop_thread.quit()
+ cls.kactivitymanagerd.kill()
+ cls.kactivitymanagerd.wait(10)
+
+ def test_qml(self) -> None:
+ """
+ 1. Add an entry to Favorites
+ 2. Remove the entry from Favorites
+ 3. Hide invalid entries automatically and don't crash when there are multiple entries with the same desktop name
+ """
+ with subprocess.Popen([QMLTEST_EXEC, "-input", os.path.join(os.path.dirname(os.path.abspath(__file__)), 'favoritetest.qml')], stdout=sys.stderr, stderr=sys.stderr) as process:
+ self.assertEqual(process.wait(60), 0)
+
+
+if __name__ == '__main__':
+ assert "USE_CUSTOM_BUS" in os.environ
+ logging.getLogger().setLevel(logging.INFO)
+ unittest.main()
diff --git a/appiumtests/applets/notificationstest.py b/appiumtests/applets/notificationstest.py
index 8917121a94..cd34a32905 100755
--- a/appiumtests/applets/notificationstest.py
+++ b/appiumtests/applets/notificationstest.py
@@ -5,6 +5,7 @@
import base64
import os
+import shutil
import subprocess
import tempfile
import time
@@ -15,6 +16,7 @@ import gi
from appium import webdriver
from appium.options.common.base import AppiumOptions
from appium.webdriver.common.appiumby import AppiumBy
+from selenium.common.exceptions import (NoSuchElementException, WebDriverException)
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
@@ -22,6 +24,8 @@ gi.require_version('Gdk', '4.0')
gi.require_version('GdkPixbuf', '2.0')
from gi.repository import Gdk, GdkPixbuf, Gio, GLib
+from kicker.favoritetest import start_kactivitymanagerd
+
WIDGET_ID: Final = "org.kde.plasma.notifications"
KDE_VERSION: Final = 6
@@ -48,17 +52,25 @@ class NotificationsTest(unittest.TestCase):
"""
driver: webdriver.Remote
+ kactivitymanagerd: subprocess.Popen
@classmethod
def setUpClass(cls) -> None:
"""
Opens the widget and initialize the webdriver
"""
+ # Make history work
+ cls.kactivitymanagerd = start_kactivitymanagerd()
+
+ os.makedirs(os.path.join(GLib.get_user_data_dir(), "knotifications6"))
+ shutil.copy(os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir, os.pardir, "libnotificationmanager", "libnotificationmanager.notifyrc"), os.path.join(GLib.get_user_data_dir(), "knotifications6"))
+
options = AppiumOptions()
options.set_capability("app", f"plasmawindowed -p org.kde.plasma.nano {WIDGET_ID}")
options.set_capability("timeouts", {'implicit': 10000})
options.set_capability("environ", {
"LC_ALL": "en_US.UTF-8",
+ "QT_LOGGING_RULES": "kf.notification*.debug=true;org.kde.plasma.notificationmanager.debug=true",
})
cls.driver = webdriver.Remote(command_executor='http://127.0.0.1:4723', options=options)
@@ -75,6 +87,8 @@ class NotificationsTest(unittest.TestCase):
Make sure to terminate the driver again, lest it dangles.
"""
subprocess.check_call([f"kquitapp{KDE_VERSION}", "plasmawindowed"])
+ cls.kactivitymanagerd.kill()
+ cls.kactivitymanagerd.wait(10)
for _ in range(10):
try:
subprocess.check_call(["pidof", "plasmawindowed"])
@@ -83,6 +97,15 @@ class NotificationsTest(unittest.TestCase):
time.sleep(1)
cls.driver.quit()
+ def close_notifications(self) -> None:
+ wait = WebDriverWait(self.driver, 5)
+ for button in self.driver.find_elements(AppiumBy.XPATH, "//button[@name='Close']"):
+ try:
+ button.click()
+ wait.until_not(lambda _: button.is_displayed())
+ except WebDriverException:
+ pass
+
def test_0_open(self) -> None:
"""
Tests the widget can be opened
@@ -106,6 +129,10 @@ class NotificationsTest(unittest.TestCase):
wait = WebDriverWait(self.driver, 5)
wait.until(EC.presence_of_element_located((AppiumBy.NAME, summary)))
+<<<<<<< HEAD
+=======
+ self.close_notifications()
+>>>>>>> 5145d877d8 (applets/notifications: suppress inhibited notifications after "Do not disturb" is off)
def take_screenshot(self) -> str:
with tempfile.TemporaryDirectory() as temp_dir:
@@ -123,6 +150,7 @@ class NotificationsTest(unittest.TestCase):
partial_pixbuf = GdkPixbuf.Pixbuf.new(GdkPixbuf.Colorspace.RGB, True, 8, 16, 16)
colors = (0xff0000ff, 0x00ff00ff, 0x0000ffff)
for color in colors:
+ logging.info(f"Testing color: {color}")
pixbuf.fill(color)
send_notification({
"app_name": "Appium Test",
@@ -145,7 +173,29 @@ class NotificationsTest(unittest.TestCase):
wait.until(EC.presence_of_element_located((AppiumBy.NAME, summary + str(color))))
partial_pixbuf.fill(color)
partial_image = base64.b64encode(Gdk.Texture.new_for_pixbuf(partial_pixbuf).save_to_png_bytes().get_data()).decode()
+<<<<<<< HEAD
self.driver.find_image_occurrence(self.take_screenshot(), partial_image)
+=======
+ try:
+ self.driver.find_image_occurrence(self.take_screenshot(), partial_image)
+ except WebDriverException: # Popup animation
+ self.driver.find_image_occurrence(self.take_screenshot(), partial_image)
+ self.close_notifications()
+
+ def test_2_notification_with_explicit_timeout(self) -> None:
+ """
+ Sends notifications with expire_timeout
+ """
+ summary = "expire_timeout"
+ send_notification({
+ "app_name": "Appium Test",
+ "summary": summary,
+ "body": "Will it disappear automatically?",
+ "timeout": 2000,
+ })
+ element = self.driver.find_element(AppiumBy.NAME, summary)
+ WebDriverWait(self.driver, 5).until_not(lambda _: element.is_displayed())
+>>>>>>> 5145d877d8 (applets/notifications: suppress inhibited notifications after "Do not disturb" is off)
def test_3_accessible_description_html_to_plaintext(self) -> None:
"""
@@ -157,6 +207,202 @@ class NotificationsTest(unittest.TestCase):
})
wait = WebDriverWait(self.driver, 5)
wait.until(EC.presence_of_element_located(("description", "biublinkwww.example.org from Appium Test")))
+<<<<<<< HEAD
+=======
+ self.close_notifications()
+
+ def test_4_actions(self) -> None:
+ """
+ When the "actions" key is set, a notification can provide actions.
+ """
+ loop = GLib.MainLoop()
+ activation_token = False
+ params_1: list[Any] = []
+ action_invoked = False
+ params_2: list[Any] = []
+ notification_closed = False
+ params_3: list[Any] = []
+
+ def notification_signal_handler(d_bus_proxy: Gio.DBusProxy, sender_name: str, signal_name: str, parameters: GLib.Variant) -> None:
+ nonlocal params_2, params_3, params_1, activation_token, action_invoked, notification_closed
+ logging.info(f"received signal {signal_name}")
+ match signal_name:
+ case "ActivationToken":
+ params_1 = parameters.unpack()
+ activation_token = True
+ case "ActionInvoked":
+ params_2 = parameters.unpack()
+ action_invoked = True
+ case "NotificationClosed":
+ params_3 = parameters.unpack()
+ notification_closed = True
+ loop.quit()
+
+ connection_id = self.notification_proxy.connect("g-signal", notification_signal_handler)
+ self.addCleanup(lambda: self.notification_proxy.disconnect(connection_id))
+
+ notification_id = send_notification({
+ "app_name": "Appium Test",
+ "body": "A notification with actions",
+ "actions": ["action1", "FooAction", "action2", "BarAction"],
+ })
+ self.driver.find_element(AppiumBy.NAME, "BarAction")
+ element = self.driver.find_element(AppiumBy.NAME, "FooAction")
+ element.click()
+ loop.run()
+ self.assertTrue(activation_token)
+ self.assertEqual(params_1[0], notification_id)
+ self.assertTrue(action_invoked)
+ self.assertEqual(params_2[0], notification_id)
+ self.assertEqual(params_2[1], "action1")
+ self.assertTrue(notification_closed)
+ self.assertEqual(params_3[0], notification_id)
+ self.assertEqual(params_3[1], 3) # reason: Revoked
+ self.assertFalse(element.is_displayed())
+
+ def test_5_inline_reply(self) -> None:
+ """
+ When the action list has "inline-reply", the notification popup will contain a text field and a reply button.
+ """
+ loop = GLib.MainLoop()
+ notification_replied = False
+ params: list[Any] = [] # id, text
+
+ def notification_signal_handler(d_bus_proxy: Gio.DBusProxy, sender_name: str, signal_name: str, parameters: GLib.Variant) -> None:
+ nonlocal params, notification_replied
+ logging.info(f"received signal {signal_name}")
+ if signal_name == "NotificationReplied":
+ params = parameters.unpack()
+ notification_replied = True
+ loop.quit()
+
+ connection_id = self.notification_proxy.connect("g-signal", notification_signal_handler)
+ self.addCleanup(lambda: self.notification_proxy.disconnect(connection_id))
+
+ # When there is only one action and it is a reply action, show text field right away
+ notification_id = send_notification({
+ "app_name": "Appium Test",
+ "body": "A notification with actions 1",
+ "actions": ["inline-reply", ""], # Use the default label
+ })
+ reply_text = "this is a reply"
+ self.driver.find_element(AppiumBy.NAME, "begin reply").click()
+ self.driver.find_element(AppiumBy.NAME, "Type a reply…").send_keys(reply_text)
+ element = self.driver.find_element(AppiumBy.NAME, "Send")
+ element.click()
+ loop.run()
+ self.assertTrue(notification_replied)
+ self.assertEqual(params[0], notification_id)
+ self.assertEqual(params[1], reply_text)
+ self.assertFalse(element.is_displayed())
+
+ notification_replied = False
+ notification_id = send_notification({
+ "app_name": "Appium Test",
+ "body": "A notification with actions 2",
+ "actions": ["inline-reply", ""],
+ "hints": {
+ "x-kde-reply-submit-button-text": GLib.Variant("s", "Reeply"), # Use a custom label
+ "x-kde-reply-placeholder-text": GLib.Variant("s", "A placeholder"), # Use a custom placeholder
+ },
+ })
+ reply_text = "this is another reply"
+ self.driver.find_element(AppiumBy.NAME, "begin reply").click()
+ self.driver.find_element(AppiumBy.NAME, "A placeholder").send_keys(reply_text)
+ element = self.driver.find_element(AppiumBy.NAME, "Reeply")
+ element.click()
+ loop.run()
+ self.assertTrue(notification_replied)
+ self.assertEqual(params[0], notification_id)
+ self.assertEqual(params[1], reply_text)
+ self.assertFalse(element.is_displayed())
+
+ notification_replied = False
+ notification_id = send_notification({
+ "app_name": "Appium Test",
+ "body": "A notification with actions 3",
+ "actions": ["inline-reply", "Replyy", "foo", "Foo", "bar", "Bar"], # Click to show the text field
+ })
+ self.driver.find_element(AppiumBy.NAME, "Foo")
+ self.driver.find_element(AppiumBy.NAME, "Bar")
+ element = self.driver.find_element(AppiumBy.NAME, "Replyy")
+ element.click()
+ reply_text = "Click Replyy to reply"
+ self.driver.find_element(AppiumBy.NAME, "Type a reply…").send_keys(reply_text)
+ self.assertFalse(element.is_displayed())
+ element = self.driver.find_element(AppiumBy.NAME, "Send")
+ element.click()
+ loop.run()
+ self.assertTrue(notification_replied)
+ self.assertEqual(params[0], notification_id)
+ self.assertEqual(params[1], reply_text)
+
+ def test_6_thumbnail(self) -> None:
+ """
+ When a notification has "x-kde-urls" hint, a thumbnail will be shown for the first url in the list
+ """
+ with tempfile.TemporaryDirectory() as temp_dir:
+ pixbuf = GdkPixbuf.Pixbuf.new(GdkPixbuf.Colorspace.RGB, True, 8, 256, 256)
+ colors = (0xff0000ff, 0x00ff00ff, 0x0000ffff)
+ for color in colors:
+ pixbuf.fill(color)
+ pixbuf.savev(os.path.join(temp_dir, f"{str(color)}.png"), "png")
+
+ url_list = [f"file://{os.path.join(temp_dir, path)}" for path in os.listdir(temp_dir)]
+ url_list.sort()
+ send_notification({
+ "app_name": "Appium Test",
+ "body": "Thumbnail",
+ "hints": {
+ "x-kde-urls": GLib.Variant("as", url_list),
+ },
+ "timeout": 10 * 1000,
+ })
+
+ self.driver.find_element(AppiumBy.NAME, "More Options…")
+
+ partial_pixbuf = GdkPixbuf.Pixbuf.new(GdkPixbuf.Colorspace.RGB, True, 8, 100, 100)
+ partial_pixbuf.fill(colors[1]) # Green is the first item
+ partial_image = base64.b64encode(Gdk.Texture.new_for_pixbuf(partial_pixbuf).save_to_png_bytes().get_data()).decode()
+
+ def match_image(driver) -> bool:
+ try:
+ self.driver.find_image_occurrence(self.take_screenshot(), partial_image)
+ return True
+ except WebDriverException:
+ return False
+
+ WebDriverWait(self.driver, 10).until(match_image)
+ self.close_notifications()
+
+ def test_7_do_not_disturb(self) -> None:
+ """
+ Suppress inhibited notifications after "Do not disturb" is turned off, and show a summary for unread inhibited notifications.
+ """
+ self.driver.find_element(AppiumBy.NAME, "Do not disturb").click()
+ dnd_button = self.driver.find_element(AppiumBy.XPATH, "//*[@name='Do not disturb' and contains(@states, 'checked')]")
+
+ summary = "Do not disturb me"
+ for i in range(2):
+ send_notification({
+ "app_name": "Appium Test",
+ "summary": summary + str(i),
+ "hints": {
+ "desktop-entry": GLib.Variant("s", "org.kde.plasmashell"),
+ },
+ "timeout": 60 * 1000,
+ })
+ title = self.driver.find_element(AppiumBy.XPATH, f"//heading[starts-with(@name, '{summary}') and contains(@accessibility-id, 'FullRepresentation')]")
+ self.assertRaises(NoSuchElementException, self.driver.find_element, AppiumBy.XPATH, f"//notification[starts-with(@name, '{summary}')]")
+
+ dnd_button.click()
+ self.driver.find_element(AppiumBy.XPATH, "//notification[@name='Unread Notifications' and @description='2 notifications were received while Do Not Disturb was active. from Notification Manager']")
+ self.driver.find_element(AppiumBy.XPATH, "//button[@name='Close' and contains(@accessibility-id, 'NotificationPopup')]").click()
+
+ # Notifications can only be cleared after they are expired, otherwise they will stay in the list
+ self.driver.find_element(AppiumBy.NAME, "Clear All Notifications").click()
+ WebDriverWait(self.driver, 5).until_not(lambda _: title.is_displayed())
+>>>>>>> 5145d877d8 (applets/notifications: suppress inhibited notifications after "Do not disturb" is off)
if __name__ == '__main__':
diff --git a/applets/notifications/package/contents/ui/FullRepresentation.qml b/applets/notifications/package/contents/ui/FullRepresentation.qml
index f949dad46a..33838599f6 100644
--- a/applets/notifications/package/contents/ui/FullRepresentation.qml
+++ b/applets/notifications/package/contents/ui/FullRepresentation.qml
@@ -71,6 +71,14 @@ PlasmaExtras.Representation {
checkable: true
checked: Globals.inhibited
+ Accessible.onPressAction: if (Globals.inhibited) {
+ Globals.revokeInhibitions();
+ } else {
+ let date = new Date();
+ date.setFullYear(date.getFullYear() + 1);
+ notificationSettings.notificationsInhibitedUntil = date;
+ notificationSettings.save();
+ }
KeyNavigation.down: list
KeyNavigation.tab: list
diff --git a/applets/notifications/package/contents/ui/global/Globals.qml b/applets/notifications/package/contents/ui/global/Globals.qml
index c46c32921a..2238653eff 100644
--- a/applets/notifications/package/contents/ui/global/Globals.qml
+++ b/applets/notifications/package/contents/ui/global/Globals.qml
@@ -34,6 +34,10 @@ QtObject {
property bool inhibited: false
onInhibitedChanged: {
+ if (!inhibited) {
+ popupNotificationsModel.showInhibitionSummary();
+ }
+
var pa = pulseAudio.item;
if (!pa) {
return;
@@ -405,6 +409,7 @@ QtObject {
limit: plasmoid ? (Math.ceil(globals.screenRect.height / (Kirigami.Units.gridUnit * 4))) : 0
showExpired: false
showDismissed: false
+ showAddedDuringInhibition: false
blacklistedDesktopEntries: notificationSettings.popupBlacklistedApplications
blacklistedNotifyRcNames: notificationSettings.popupBlacklistedServices
whitelistedDesktopEntries: globals.inhibited ? notificationSettings.doNotDisturbPopupWhitelistedApplications : []
@@ -613,9 +618,13 @@ QtObject {
onKillJobClicked: popupNotificationsModel.killJob(popupNotificationsModel.index(index, 0))
// popup width is fixed
- onHeightChanged: positionPopups()
+ onHeightChanged: globals.positionPopups()
Component.onCompleted: {
+ if (globals.inhibited) {
+ model.wasAddedDuringInhibition = false; // Don't count already shown notifications
+ }
+
if (model.type === NotificationManager.Notifications.NotificationType && model.desktopEntry) {
// Register apps that were seen spawning a popup so they can be configured later
// Apps with notifyrc can already be configured anyway
diff --git a/libnotificationmanager/CMakeLists.txt b/libnotificationmanager/CMakeLists.txt
index d04d8a4a24..5c48653ede 100644
--- a/libnotificationmanager/CMakeLists.txt
+++ b/libnotificationmanager/CMakeLists.txt
@@ -84,6 +84,7 @@ target_link_libraries(notificationmanager
KF6::I18n
KF6::WindowSystem
KF6::ItemModels # KDescendantsProxyModel
+ KF6::Notifications # Inhibition summary
KF6::KIOFileWidgets
Plasma::Plasma
KF6::Screen
@@ -135,3 +136,6 @@ install(EXPORT notificationmanagerLibraryTargets
install(FILES plasmanotifyrc
DESTINATION ${KDE_INSTALL_CONFDIR})
+
+install(FILES libnotificationmanager.notifyrc
+ DESTINATION ${KDE_INSTALL_KNOTIFYRCDIR})
diff --git a/libnotificationmanager/abstractnotificationsmodel.cpp b/libnotificationmanager/abstractnotificationsmodel.cpp
index 8bd55bc26c..8307a1e8f7 100644
--- a/libnotificationmanager/abstractnotificationsmodel.cpp
+++ b/libnotificationmanager/abstractnotificationsmodel.cpp
@@ -104,6 +104,7 @@ void AbstractNotificationsModel::Private::onNotificationReplaced(uint replacedId
newNotification.setExpired(oldNotification.expired());
newNotification.setDismissed(oldNotification.dismissed());
newNotification.setRead(oldNotification.read());
+ newNotification.setWasAddedDuringInhibition(Server::self().inhibited());
notifications[row] = newNotification;
const QModelIndex idx = q->index(row, 0);
@@ -378,6 +379,9 @@ QVariant AbstractNotificationsModel::data(const QModelIndex &index, int role) co
case Notifications::TransientRole:
return notification.transient();
+ case Notifications::WasAddedDuringInhibitionRole:
+ return notification.wasAddedDuringInhibition();
+
case Notifications::HasReplyActionRole:
return notification.hasReplyAction();
case Notifications::ReplyActionLabelRole:
@@ -416,6 +420,12 @@ bool AbstractNotificationsModel::setData(const QModelIndex &index, const QVarian
dirty = true;
}
break;
+ case Notifications::WasAddedDuringInhibitionRole:
+ if (bool v = value.toBool(); v != notification.wasAddedDuringInhibition()) {
+ notification.setWasAddedDuringInhibition(v);
+ dirty = true;
+ }
+ break;
}
if (dirty) {
@@ -471,7 +481,7 @@ void AbstractNotificationsModel::clear(Notifications::ClearFlags flags)
for (int i = 0; i < d->notifications.count(); ++i) {
const Notification &notification = d->notifications.at(i);
- if (flags.testFlag(Notifications::ClearExpired) && notification.expired()) {
+ if (flags.testFlag(Notifications::ClearExpired) && (notification.expired() || notification.wasAddedDuringInhibition())) {
close(notification.id());
}
}
diff --git a/libnotificationmanager/libnotificationmanager.notifyrc b/libnotificationmanager/libnotificationmanager.notifyrc
new file mode 100644
index 0000000000..79304e62bc
--- /dev/null
+++ b/libnotificationmanager/libnotificationmanager.notifyrc
@@ -0,0 +1,11 @@
+# SPDX-License-Identifier: CC0-1.0
+# SPDX-FileCopyrightText: None
+
+[Global]
+Name=Notification Manager
+IconName=preferences-desktop-notification-bell
+
+[Event/inhibitionSummary]
+Name=Summary for unread inhibited notifications
+Action=Popup
+Urgency=Low
diff --git a/libnotificationmanager/notification.cpp b/libnotificationmanager/notification.cpp
index 276611310c..320c837617 100644
--- a/libnotificationmanager/notification.cpp
+++ b/libnotificationmanager/notification.cpp
@@ -826,3 +826,13 @@ void Notification::processHints(const QVariantMap &hints)
{
d->processHints(hints);
}
+
+bool Notification::wasAddedDuringInhibition() const
+{
+ return d->wasAddedDuringInhibition;
+}
+
+void Notification::setWasAddedDuringInhibition(bool value)
+{
+ d->wasAddedDuringInhibition = value;
+}
diff --git a/libnotificationmanager/notification.h b/libnotificationmanager/notification.h
index 9f04a4e200..8613534fd2 100644
--- a/libnotificationmanager/notification.h
+++ b/libnotificationmanager/notification.h
@@ -131,6 +131,9 @@ public:
void processHints(const QVariantMap &hints);
+ bool wasAddedDuringInhibition() const;
+ void setWasAddedDuringInhibition(bool value);
+
private:
friend class NotificationsModel;
friend class AbstractNotificationsModel;
diff --git a/libnotificationmanager/notification_p.h b/libnotificationmanager/notification_p.h
index 6cae23c21e..923bde7882 100644
--- a/libnotificationmanager/notification_p.h
+++ b/libnotificationmanager/notification_p.h
@@ -96,6 +96,8 @@ public:
bool resident = false;
bool transient = false;
+
+ bool wasAddedDuringInhibition = false;
};
} // namespace NotificationManager
diff --git a/libnotificationmanager/notificationfilterproxymodel.cpp b/libnotificationmanager/notificationfilterproxymodel.cpp
index 98a32b6645..bb573713af 100644
--- a/libnotificationmanager/notificationfilterproxymodel.cpp
+++ b/libnotificationmanager/notificationfilterproxymodel.cpp
@@ -58,6 +58,20 @@ void NotificationFilterProxyModel::setShowDismissed(bool show)
}
}
+bool NotificationFilterProxyModel::showAddedDuringInhibition() const
+{
+ return m_showDismissed;
+}
+
+void NotificationFilterProxyModel::setShowAddedDuringInhibition(bool show)
+{
+ if (m_showAddedDuringInhibition != show) {
+ m_showAddedDuringInhibition = show;
+ invalidateFilter();
+ Q_EMIT showAddedDuringInhibitionChanged();
+ }
+}
+
QStringList NotificationFilterProxyModel::blacklistedDesktopEntries() const
{
return m_blacklistedDesktopEntries;
@@ -177,5 +191,9 @@ bool NotificationFilterProxyModel::filterAcceptsRow(int source_row, const QModel
}
}
+ if (!m_showAddedDuringInhibition && sourceIdx.data(Notifications::WasAddedDuringInhibitionRole).toBool()) {
+ return false;
+ }
+
return true;
}
diff --git a/libnotificationmanager/notificationfilterproxymodel_p.h b/libnotificationmanager/notificationfilterproxymodel_p.h
index 4029320e8e..af04a9fac1 100644
--- a/libnotificationmanager/notificationfilterproxymodel_p.h
+++ b/libnotificationmanager/notificationfilterproxymodel_p.h
@@ -30,6 +30,9 @@ public:
bool showDismissed() const;
void setShowDismissed(bool show);
+ bool showAddedDuringInhibition() const;
+ void setShowAddedDuringInhibition(bool show);
+
QStringList blacklistedDesktopEntries() const;
void setBlackListedDesktopEntries(const QStringList &blacklist);
@@ -46,6 +49,7 @@ Q_SIGNALS:
void urgenciesChanged();
void showExpiredChanged();
void showDismissedChanged();
+ void showAddedDuringInhibitionChanged();
void blacklistedDesktopEntriesChanged();
void blacklistedNotifyRcNamesChanged();
void whitelistedDesktopEntriesChanged();
@@ -58,6 +62,7 @@ private:
Notifications::Urgencies m_urgencies = Notifications::LowUrgency | Notifications::NormalUrgency | Notifications::CriticalUrgency;
bool m_showDismissed = false;
bool m_showExpired = false;
+ bool m_showAddedDuringInhibition = true;
QStringList m_blacklistedDesktopEntries;
QStringList m_blacklistedNotifyRcNames;
diff --git a/libnotificationmanager/notifications.cpp b/libnotificationmanager/notifications.cpp
index 9b3e18018b..3d5662f926 100644
--- a/libnotificationmanager/notifications.cpp
+++ b/libnotificationmanager/notifications.cpp
@@ -12,6 +12,8 @@
#include <memory>
#include <KDescendantsProxyModel>
+#include <KLocalizedString>
+#include <KNotification>
#include "limitedrowcountproxymodel_p.h"
#include "notificationfilterproxymodel_p.h"
@@ -30,6 +32,7 @@
#include "debug.h"
+using namespace Qt::StringLiterals;
using namespace NotificationManager;
class Q_DECL_HIDDEN Notifications::Private
@@ -166,6 +169,7 @@ void Notifications::Private::initProxyModels()
connect(filterModel, &NotificationFilterProxyModel::urgenciesChanged, q, &Notifications::urgenciesChanged);
connect(filterModel, &NotificationFilterProxyModel::showExpiredChanged, q, &Notifications::showExpiredChanged);
connect(filterModel, &NotificationFilterProxyModel::showDismissedChanged, q, &Notifications::showDismissedChanged);
+ connect(filterModel, &NotificationFilterProxyModel::showAddedDuringInhibitionChanged, q, &Notifications::showAddedDuringInhibitionChanged);
connect(filterModel, &NotificationFilterProxyModel::blacklistedDesktopEntriesChanged, q, &Notifications::blacklistedDesktopEntriesChanged);
connect(filterModel, &NotificationFilterProxyModel::blacklistedNotifyRcNamesChanged, q, &Notifications::blacklistedNotifyRcNamesChanged);
@@ -245,7 +249,7 @@ void Notifications::Private::updateCount()
for (int i = 0; i < filterModel->rowCount(); ++i) {
const QModelIndex idx = filterModel->index(i, 0);
- if (idx.data(Notifications::ExpiredRole).toBool()) {
+ if (idx.data(Notifications::ExpiredRole).toBool() || idx.data(Notifications::WasAddedDuringInhibitionRole).toBool()) {
++expired;
} else {
++active;
@@ -477,6 +481,16 @@ void Notifications::setShowDismissed(bool show)
d->filterModel->setShowDismissed(show);
}
+bool Notifications::showAddedDuringInhibition() const
+{
+ return d->filterModel->showAddedDuringInhibition();
+}
+
+void Notifications::setShowAddedDuringInhibition(bool show)
+{
+ d->filterModel->setShowAddedDuringInhibition(show);
+}
+
QStringList Notifications::blacklistedDesktopEntries() const
{
return d->filterModel->blacklistedDesktopEntries();
@@ -812,6 +826,28 @@ void Notifications::collapseAllGroups()
}
}
+void Notifications::showInhibitionSummary()
+{
+ int inhibited = 0;
+ for (int i = 0, count = d->notificationsAndJobsModel->rowCount(); i < count; ++i) {
+ const QModelIndex idx = d->notificationsAndJobsModel->index(i, 0);
+ if (!idx.data(Notifications::ReadRole).toBool() && idx.data(Notifications::WasAddedDuringInhibitionRole).toBool()) {
+ ++inhibited;
+ }
+ }
+
+ if (!inhibited) {
+ return;
+ }
+
+ KNotification::event(u"inhibitionSummary"_s,
+ i18nc("@title", "Unread Notifications"),
+ i18nc("@info", "%1 notifications were received while Do Not Disturb was active.", QString::number(inhibited)),
+ u"preferences-desktop-notification-bell"_s,
+ KNotification::CloseOnTimeout,
+ u"libnotificationmanager"_s);
+}
+
QVariant Notifications::data(const QModelIndex &index, int role) const
{
return QSortFilterProxyModel::data(index, role);
diff --git a/libnotificationmanager/notifications.h b/libnotificationmanager/notifications.h
index edb898988f..ef500b0c7b 100644
--- a/libnotificationmanager/notifications.h
+++ b/libnotificationmanager/notifications.h
@@ -61,6 +61,15 @@ class NOTIFICATIONMANAGER_EXPORT Notifications : public QSortFilterProxyModel, p
*/
Q_PROPERTY(bool showDismissed READ showDismissed WRITE setShowDismissed NOTIFY showDismissedChanged)
+ /**
+ * Whether to show notifications added during inhibition.
+ *
+ * If set to @c false, notifications are suppressed even after leaving "Do not disturb" mode.
+ *
+ * Default is @c true.
+ */
+ Q_PROPERTY(bool showAddedDuringInhibition READ showAddedDuringInhibition WRITE setShowAddedDuringInhibition NOTIFY showAddedDuringInhibitionChanged)
+
/**
* A list of desktop entries for which no notifications should be shown.
*
@@ -285,6 +294,8 @@ public:
///< notification in a certain way, or group notifications of similar types. @since 5.21
ResidentRole, ///< Whether the notification should keep its actions even when they were invoked. @since 5.22
TransientRole, ///< Whether the notification is transient and should not be kept in history. @since 5.22
+
+ WasAddedDuringInhibitionRole, ///< Whether the notification was added while inhibition was active. @since 6.3
};
Q_ENUM(Roles)
@@ -371,6 +382,9 @@ public:
bool showDismissed() const;
void setShowDismissed(bool show);
+ bool showAddedDuringInhibition() const;
+ void setShowAddedDuringInhibition(bool show);
+
QStringList blacklistedDesktopEntries() const;
void setBlacklistedDesktopEntries(const QStringList &blacklist);
@@ -529,6 +543,11 @@ public:
Q_INVOKABLE void collapseAllGroups();
+ /**
+ * Shows a notification to report the number of unread inhibited notifications.
+ */
+ Q_INVOKABLE void showInhibitionSummary();
+
QVariant data(const QModelIndex &index, int role) const override;
bool setData(const QModelIndex &index, const QVariant &value, int role) override;
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
@@ -541,6 +560,7 @@ Q_SIGNALS:
void limitChanged();
void showExpiredChanged();
void showDismissedChanged();
+ void showAddedDuringInhibitionChanged();
void blacklistedDesktopEntriesChanged();
void blacklistedNotifyRcNamesChanged();
void whitelistedDesktopEntriesChanged();
diff --git a/libnotificationmanager/server_p.cpp b/libnotificationmanager/server_p.cpp
index 84fe37afa9..66cd621033 100644
--- a/libnotificationmanager/server_p.cpp
+++ b/libnotificationmanager/server_p.cpp
@@ -164,6 +164,7 @@ uint ServerPrivate::Notify(const QString &app_name,
notification.setActions(actions);
notification.setTimeout(timeout);
+ notification.setWasAddedDuringInhibition(m_inhibited);
// might override some of the things we set above (like application name)
notification.d->processHints(hints);