feat: expire progress bar on popup notifs
This commit is contained in:
@@ -16,9 +16,11 @@ CustomRect {
|
|||||||
required property NotificationGroup group
|
required property NotificationGroup group
|
||||||
required property Flickable container
|
required property Flickable container
|
||||||
required property bool expanded
|
required property bool expanded
|
||||||
|
|
||||||
property bool expandable: true
|
property bool expandable: true
|
||||||
property int baseSurface: 0
|
property int baseSurface: 0
|
||||||
property int baseLayer: baseSurface
|
property int baseLayer: baseSurface
|
||||||
|
property int bottomPadding: 0
|
||||||
|
|
||||||
readonly property list<NotificationRow> notifs: [...group.activeNotifs]
|
readonly property list<NotificationRow> notifs: [...group.activeNotifs]
|
||||||
readonly property bool single: notifs.length === 1
|
readonly property bool single: notifs.length === 1
|
||||||
@@ -27,7 +29,7 @@ CustomRect {
|
|||||||
readonly property int nonAnimHeight: {
|
readonly property int nonAnimHeight: {
|
||||||
const headerHeight = header.implicitHeight + (expanded ? Tokens.spacing.extraSmall : 0);
|
const headerHeight = header.implicitHeight + (expanded ? Tokens.spacing.extraSmall : 0);
|
||||||
const columnHeight = headerHeight + notifList.layoutHeight;
|
const columnHeight = headerHeight + notifList.layoutHeight;
|
||||||
return Math.round(Math.max(Config.notifs.sizes.image, columnHeight) + Tokens.padding.medium * 2);
|
return Math.round(Math.max(Config.notifs.sizes.image, columnHeight) + Tokens.padding.medium * 2 + bottomPadding);
|
||||||
}
|
}
|
||||||
|
|
||||||
signal requestExpand(expand: bool)
|
signal requestExpand(expand: bool)
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import qs.Components
|
|||||||
import qs.Components.Notifs
|
import qs.Components.Notifs
|
||||||
import qs.Daemons
|
import qs.Daemons
|
||||||
import qs.Helpers
|
import qs.Helpers
|
||||||
|
import qs.Services
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
@@ -109,10 +110,6 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
component Anim: NumberAnimation {
|
|
||||||
duration: Tokens.anim.durations.expressiveDefaultSpatial
|
|
||||||
easing: Tokens.anim.expressiveDefaultSpatial
|
|
||||||
}
|
|
||||||
component NotifWrapper: Item {
|
component NotifWrapper: Item {
|
||||||
id: wrapper
|
id: wrapper
|
||||||
|
|
||||||
@@ -125,8 +122,8 @@ Item {
|
|||||||
fn(n);
|
fn(n);
|
||||||
}
|
}
|
||||||
|
|
||||||
function holdPopups(): void {
|
function holdGroupExpiry(): void {
|
||||||
forEachPopup(n => n.holdPopup());
|
wrapper.modelData.holdExpiry();
|
||||||
}
|
}
|
||||||
|
|
||||||
LazyListView.trackViewport: !expanded && card.nonAnimHeight < card.implicitHeight
|
LazyListView.trackViewport: !expanded && card.nonAnimHeight < card.implicitHeight
|
||||||
@@ -148,15 +145,33 @@ Item {
|
|||||||
|
|
||||||
onHoldingChanged: {
|
onHoldingChanged: {
|
||||||
if (holding)
|
if (holding)
|
||||||
holdPopups();
|
holdGroupExpiry();
|
||||||
else
|
else
|
||||||
forEachPopup(n => n.releasePopup());
|
wrapper.modelData.releaseExpiry();
|
||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
function onNotifsChanged(): void {
|
function onNotifsChanged(): void {
|
||||||
if (wrapper.holding)
|
if (wrapper.holding)
|
||||||
wrapper.holdPopups();
|
wrapper.holdGroupExpiry();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onExpireTimerRestarted(): void {
|
||||||
|
if (expireProgress.running)
|
||||||
|
expireProgress.stop();
|
||||||
|
expireProgressReset.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onExpirePausedChanged(): void {
|
||||||
|
if (wrapper.modelData.expirePaused) {
|
||||||
|
if (expireProgress.running)
|
||||||
|
expireProgress.pause();
|
||||||
|
} else {
|
||||||
|
if (expireProgress.running)
|
||||||
|
expireProgress.resume();
|
||||||
|
else
|
||||||
|
expireProgress.start();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
target: wrapper.modelData
|
target: wrapper.modelData
|
||||||
@@ -171,6 +186,39 @@ Item {
|
|||||||
easing: Tokens.anim.emphasized
|
easing: Tokens.anim.emphasized
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Anim {
|
||||||
|
id: expireProgressReset
|
||||||
|
|
||||||
|
readonly property bool nPaused: wrapper.modelData.expirePaused
|
||||||
|
|
||||||
|
target: progress
|
||||||
|
property: "implicitWidth"
|
||||||
|
type: Anim.FastEffects
|
||||||
|
to: nPaused ? area.width : area.width * (1 - duration / expireProgress.duration)
|
||||||
|
|
||||||
|
onFinished: {
|
||||||
|
if (nPaused)
|
||||||
|
expireProgress.duration = wrapper.modelData.expireTimeout;
|
||||||
|
else
|
||||||
|
expireProgress.duration = wrapper.modelData.expireTimeout - expireProgressReset.duration;
|
||||||
|
expireProgress.from = to;
|
||||||
|
if (!nPaused)
|
||||||
|
expireProgress.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Anim {
|
||||||
|
id: expireProgress
|
||||||
|
|
||||||
|
running: wrapper.modelData.expireActive
|
||||||
|
target: progress
|
||||||
|
property: "implicitWidth"
|
||||||
|
duration: wrapper.modelData.expireTimeout
|
||||||
|
easing.type: Easing.Linear
|
||||||
|
from: area.width
|
||||||
|
to: 0
|
||||||
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
|
|
||||||
@@ -208,6 +256,25 @@ Item {
|
|||||||
onRequestExpand: expand => wrapper.expanded = expand
|
onRequestExpand: expand => wrapper.expanded = expand
|
||||||
onSwiped: notif => notif.dismissPopup()
|
onSwiped: notif => notif.dismissPopup()
|
||||||
onLinkActivated: notif => notif.dismissPopup()
|
onLinkActivated: notif => notif.dismissPopup()
|
||||||
|
bottomPadding: Config.notifs.expire ? progress.implicitHeight : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomClippingRect {
|
||||||
|
anchors.fill: parent
|
||||||
|
|
||||||
|
radius: Tokens.rounding.large
|
||||||
|
|
||||||
|
visible: Config.notifs.expire
|
||||||
|
|
||||||
|
CustomRect {
|
||||||
|
id: progress
|
||||||
|
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.right: parent.right
|
||||||
|
implicitWidth: parent.width
|
||||||
|
implicitHeight: 6
|
||||||
|
color: Colors.palette.m3outlineVariant
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -296,7 +296,7 @@ Item {
|
|||||||
CustomText {
|
CustomText {
|
||||||
animate: true
|
animate: true
|
||||||
text: fabMenuItem.modelData.name
|
text: fabMenuItem.modelData.name
|
||||||
font: Tokens.font.body.builders.small.capitalization(Font.Capitalize).build
|
font: Tokens.font.body.builders.small.capitalization(Font.Capitalize).build()
|
||||||
color: Colors.palette.m3onPrimaryContainer
|
color: Colors.palette.m3onPrimaryContainer
|
||||||
Layout.preferredWidth: implicitWidth
|
Layout.preferredWidth: implicitWidth
|
||||||
|
|
||||||
|
|||||||
@@ -38,21 +38,14 @@ namespace {
|
|||||||
constexpr int MAX_ROWS = 500;
|
constexpr int MAX_ROWS = 500;
|
||||||
constexpr int MAX_PER_GROUP = 50;
|
constexpr int MAX_PER_GROUP = 50;
|
||||||
constexpr qint64 MAX_AGE_MS = 30LL * 24 * 60 * 60 * 1000;
|
constexpr qint64 MAX_AGE_MS = 30LL * 24 * 60 * 60 * 1000;
|
||||||
constexpr int TICK_MS = 250;
|
constexpr int TICK_MS = 100;
|
||||||
constexpr int MAX_IMAGE_DIM = 512;
|
constexpr int MAX_IMAGE_DIM = 512;
|
||||||
// Memoized icon theme file lookups (hits and misses) per theme configuration.
|
|
||||||
constexpr int MAX_ICON_PATHS = 256;
|
constexpr int MAX_ICON_PATHS = 256;
|
||||||
constexpr qint64 PRUNE_INTERVAL_MS = 4LL * 60 * 60 * 1000;
|
constexpr qint64 PRUNE_INTERVAL_MS = 4LL * 60 * 60 * 1000;
|
||||||
constexpr qint64 TIME_STR_REFRESH_MS = 2000;
|
constexpr qint64 TIME_STR_REFRESH_MS = 2000;
|
||||||
// Coalesces content updates (live replaces_id edits, popup state) into one
|
constexpr qint64 MAX_EXPIRE_MS = 5LL * 60 * 1000;
|
||||||
// transaction instead of one UPDATE per changed field.
|
|
||||||
constexpr int FLUSH_MS = 300;
|
constexpr int FLUSH_MS = 300;
|
||||||
|
|
||||||
// Quickshell serves inline notification pictures through image://qsimage/
|
|
||||||
// (and qspixmap) handles that belong to the live notification, not to the
|
|
||||||
// shell: once that notification is replaced or dismissed, the handle is gone
|
|
||||||
// and the url fails to resolve. Such a source is only ever useful as the raw
|
|
||||||
// url long enough to read the bytes, so the UI must never see it.
|
|
||||||
bool transientImageSource(const QString& source) {
|
bool transientImageSource(const QString& source) {
|
||||||
if (source.isEmpty()) {
|
if (source.isEmpty()) {
|
||||||
return false;
|
return false;
|
||||||
@@ -64,8 +57,6 @@ bool transientImageSource(const QString& source) {
|
|||||||
url.host() == QLatin1String("qspixmap"));
|
url.host() == QLatin1String("qspixmap"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// The url the UI should use: the cached blob when there is one, and the raw
|
|
||||||
// source only while that source keeps resolving.
|
|
||||||
QString displayImage(const QString& source, const QString& digest) {
|
QString displayImage(const QString& source, const QString& digest) {
|
||||||
if (!digest.isEmpty()) {
|
if (!digest.isEmpty()) {
|
||||||
return QStringLiteral("image://notifimg/") + digest;
|
return QStringLiteral("image://notifimg/") + digest;
|
||||||
@@ -74,14 +65,9 @@ QString displayImage(const QString& source, const QString& digest) {
|
|||||||
return transientImageSource(source) ? QString() : source;
|
return transientImageSource(source) ? QString() : source;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Icon theme state, captured on the GUI thread. `QIcon::themeName()`,
|
|
||||||
// `fallbackThemeName()` and `themeSearchPaths()` touch GUI-thread only state,
|
|
||||||
// but the strings they return are safe to hand to a worker.
|
|
||||||
struct IconTheme {
|
struct IconTheme {
|
||||||
QStringList searchPaths;
|
QStringList searchPaths;
|
||||||
QStringList themes;
|
QStringList themes;
|
||||||
// Changes when the theme or the search paths change, so the path cache
|
|
||||||
// below can be dropped after a theme switch.
|
|
||||||
QString signature;
|
QString signature;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -104,21 +90,11 @@ IconTheme iconTheme() {
|
|||||||
return theme;
|
return theme;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve an icon name to a file the theme actually ships, so notification
|
|
||||||
// images that only name an icon can still be cached. KF6's KIconLoader is not
|
|
||||||
// thread-safe (it crashed the shell when the icon provider was called from a
|
|
||||||
// worker) and QIcon painting is GUI-thread only, so this only builds candidate
|
|
||||||
// paths and stats them - which makes it worker-thread safe when the theme
|
|
||||||
// strings come from iconTheme(). Hits and misses are both memoized: a sender
|
|
||||||
// repeats the same icon name, and a name the theme lacks costs thousands of
|
|
||||||
// stat calls to discover.
|
|
||||||
QString iconFileForName(const QString& name, const IconTheme& theme) {
|
QString iconFileForName(const QString& name, const IconTheme& theme) {
|
||||||
if (name.isEmpty()) {
|
if (name.isEmpty()) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stat results only change when a theme is installed or removed, which the
|
|
||||||
// signature covers; the cap just keeps a stream of odd names bounded.
|
|
||||||
static std::mutex cacheMutex;
|
static std::mutex cacheMutex;
|
||||||
static QHash<QString, QString> pathCache;
|
static QHash<QString, QString> pathCache;
|
||||||
static QString cacheSignature;
|
static QString cacheSignature;
|
||||||
@@ -171,7 +147,6 @@ QString iconFileForName(const QString& name, const IconTheme& theme) {
|
|||||||
const QString suffix =
|
const QString suffix =
|
||||||
candidateName + "." + extension;
|
candidateName + "." + extension;
|
||||||
|
|
||||||
// hicolor: <size>/<context>/<ext>/<name>.<ext>
|
|
||||||
const QString canonical = themeDir.filePath(
|
const QString canonical = themeDir.filePath(
|
||||||
size + "/" + context + "/" + extension +
|
size + "/" + context + "/" + extension +
|
||||||
"/" + suffix);
|
"/" + suffix);
|
||||||
@@ -179,15 +154,12 @@ QString iconFileForName(const QString& name, const IconTheme& theme) {
|
|||||||
return canonical;
|
return canonical;
|
||||||
}
|
}
|
||||||
|
|
||||||
// hicolor and Papirus both also ship
|
|
||||||
// <size>/<context>/<name>.<ext>
|
|
||||||
const QString sized = themeDir.filePath(
|
const QString sized = themeDir.filePath(
|
||||||
size + "/" + context + "/" + suffix);
|
size + "/" + context + "/" + suffix);
|
||||||
if (QFileInfo::exists(sized)) {
|
if (QFileInfo::exists(sized)) {
|
||||||
return sized;
|
return sized;
|
||||||
}
|
}
|
||||||
|
|
||||||
// themes ordering size under context
|
|
||||||
const QString nested = themeDir.filePath(
|
const QString nested = themeDir.filePath(
|
||||||
context + "/" + size + "/" + suffix);
|
context + "/" + size + "/" + suffix);
|
||||||
if (QFileInfo::exists(nested)) {
|
if (QFileInfo::exists(nested)) {
|
||||||
@@ -197,7 +169,6 @@ QString iconFileForName(const QString& name, const IconTheme& theme) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// theme without size directories: <context>/<name>.<ext>
|
|
||||||
for (const auto& context : contexts) {
|
for (const auto& context : contexts) {
|
||||||
for (const auto& extension : extensions) {
|
for (const auto& extension : extensions) {
|
||||||
const QString plain = themeDir.filePath(
|
const QString plain = themeDir.filePath(
|
||||||
@@ -420,10 +391,6 @@ void NotificationRow::setPopup(bool popup) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
m_popup = popup;
|
m_popup = popup;
|
||||||
m_popupPaused = false;
|
|
||||||
m_deadline =
|
|
||||||
popup ? QDateTime::currentMSecsSinceEpoch() + m_store->popupTimeout()
|
|
||||||
: 0;
|
|
||||||
emit popupChanged();
|
emit popupChanged();
|
||||||
|
|
||||||
if (m_store != nullptr) {
|
if (m_store != nullptr) {
|
||||||
@@ -473,9 +440,6 @@ void NotificationRow::lock(QObject* item) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
m_locks.insert(item);
|
m_locks.insert(item);
|
||||||
// Goes through unlock() so releasing the last lock on an already-closed
|
|
||||||
// row finishes the removal the close started. The destroyed signal
|
|
||||||
// carries the dying object, which is the lock holder.
|
|
||||||
QObject::connect(item, &QObject::destroyed, this, [this](QObject* obj) {
|
QObject::connect(item, &QObject::destroyed, this, [this](QObject* obj) {
|
||||||
unlock(obj);
|
unlock(obj);
|
||||||
});
|
});
|
||||||
@@ -500,15 +464,17 @@ void NotificationRow::unlock(QObject* item) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void NotificationRow::pausePopup() {
|
void NotificationRow::pausePopup() {
|
||||||
m_popupPaused = true;
|
if (m_popupGroup != nullptr) {
|
||||||
|
m_popupGroup->holdExpiry();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void NotificationRow::resumePopup() {
|
void NotificationRow::resumePopup() {
|
||||||
m_popupPaused = false;
|
if (m_popupGroup != nullptr) {
|
||||||
|
m_popupGroup->releaseExpiry();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The v2 notification UI uses caelestia's naming; these are thin aliases of the
|
|
||||||
// existing behaviour so both spellings work.
|
|
||||||
void NotificationRow::dismiss() {
|
void NotificationRow::dismiss() {
|
||||||
close();
|
close();
|
||||||
}
|
}
|
||||||
@@ -535,8 +501,6 @@ void NotificationRow::unref(QObject* item) {
|
|||||||
unlock(item);
|
unlock(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
// True when path is this row's image, in either its raw or cached form, so the
|
|
||||||
// UI can tell "the same picture changed url" from "a different picture".
|
|
||||||
bool NotificationRow::isSameImage(const QString& path) const {
|
bool NotificationRow::isSameImage(const QString& path) const {
|
||||||
return path == m_image || path == m_imageSource;
|
return path == m_image || path == m_imageSource;
|
||||||
}
|
}
|
||||||
@@ -545,11 +509,6 @@ bool NotificationRow::isSameAppIcon(const QString& path) const {
|
|||||||
return path == m_appIcon;
|
return path == m_appIcon;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool NotificationRow::expired(qint64 now) const {
|
|
||||||
return m_popup && !m_popupPaused && !m_closed && m_deadline > 0 &&
|
|
||||||
now >= m_deadline;
|
|
||||||
}
|
|
||||||
|
|
||||||
void NotificationRow::refreshTimeStr() {
|
void NotificationRow::refreshTimeStr() {
|
||||||
const qint64 diff = QDateTime::currentMSecsSinceEpoch() - m_timeMs;
|
const qint64 diff = QDateTime::currentMSecsSinceEpoch() - m_timeMs;
|
||||||
const qint64 m = diff / 60000;
|
const qint64 m = diff / 60000;
|
||||||
@@ -595,6 +554,26 @@ int NotificationGroup::count() const {
|
|||||||
return m_count;
|
return m_count;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool NotificationGroup::expireActive() const {
|
||||||
|
return m_expireActive;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool NotificationGroup::expirePaused() const {
|
||||||
|
return m_expirePaused;
|
||||||
|
}
|
||||||
|
|
||||||
|
qreal NotificationGroup::expireProgress() const {
|
||||||
|
return m_expireProgress;
|
||||||
|
}
|
||||||
|
|
||||||
|
int NotificationGroup::expireRemaining() const {
|
||||||
|
return m_expireRemaining;
|
||||||
|
}
|
||||||
|
|
||||||
|
int NotificationGroup::expireTimeout() const {
|
||||||
|
return m_expireTimeout;
|
||||||
|
}
|
||||||
|
|
||||||
QString NotificationGroup::firstSummary() const {
|
QString NotificationGroup::firstSummary() const {
|
||||||
return m_firstSummary;
|
return m_firstSummary;
|
||||||
}
|
}
|
||||||
@@ -636,11 +615,6 @@ void NotificationGroup::removeRow(NotificationRow* row) {
|
|||||||
|
|
||||||
QObject::disconnect(row, nullptr, this, nullptr);
|
QObject::disconnect(row, nullptr, this, nullptr);
|
||||||
emit notifsChanged();
|
emit notifsChanged();
|
||||||
// Must not drop the row from m_active here: refreshActive() only emits
|
|
||||||
// activeNotifsChanged when the list it rebuilds differs from the stored
|
|
||||||
// one, and a silently pre-edited m_active is equal to the rebuild. The
|
|
||||||
// popup stack reads activeNotifs, so skipping the emit left a closed row
|
|
||||||
// occupying its slot - an invisible gap other entries did not move into.
|
|
||||||
refreshActive();
|
refreshActive();
|
||||||
sync();
|
sync();
|
||||||
}
|
}
|
||||||
@@ -708,8 +682,109 @@ void NotificationGroup::sync() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delegates compare their own shown url against the group's current source row,
|
void NotificationGroup::resetExpiry(int timeout) {
|
||||||
// so a row swapping raw source <-> cached blob does not re-trigger the fade.
|
const qint64 now = QDateTime::currentMSecsSinceEpoch();
|
||||||
|
|
||||||
|
if (timeout != m_expireTimeout) {
|
||||||
|
m_expireTimeout = timeout;
|
||||||
|
emit expireTimeoutChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timeout <= 0) {
|
||||||
|
m_expireDeadline = 0;
|
||||||
|
m_expireHeld = 0;
|
||||||
|
|
||||||
|
if (m_expirePaused) {
|
||||||
|
m_expirePaused = false;
|
||||||
|
emit expirePausedChanged();
|
||||||
|
}
|
||||||
|
} else if (m_expirePaused) {
|
||||||
|
m_expireHeld = timeout;
|
||||||
|
} else {
|
||||||
|
m_expireDeadline = now + timeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshExpiry(now);
|
||||||
|
emit expireTimerRestarted();
|
||||||
|
}
|
||||||
|
|
||||||
|
void NotificationGroup::holdExpiry() {
|
||||||
|
if (m_expirePaused || m_expireDeadline <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const qint64 now = QDateTime::currentMSecsSinceEpoch();
|
||||||
|
m_expireHeld = (std::max)(qint64(0), m_expireDeadline - now);
|
||||||
|
m_expireDeadline = 0;
|
||||||
|
m_expirePaused = true;
|
||||||
|
|
||||||
|
emit expirePausedChanged();
|
||||||
|
refreshExpiry(now);
|
||||||
|
}
|
||||||
|
|
||||||
|
void NotificationGroup::releaseExpiry() {
|
||||||
|
if (!m_expirePaused) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const qint64 now = QDateTime::currentMSecsSinceEpoch();
|
||||||
|
m_expireDeadline = now + m_expireHeld;
|
||||||
|
m_expireHeld = 0;
|
||||||
|
m_expirePaused = false;
|
||||||
|
|
||||||
|
emit expirePausedChanged();
|
||||||
|
refreshExpiry(now);
|
||||||
|
}
|
||||||
|
|
||||||
|
void NotificationGroup::clearExpiry() {
|
||||||
|
m_expireDeadline = 0;
|
||||||
|
m_expireHeld = 0;
|
||||||
|
|
||||||
|
if (m_expirePaused) {
|
||||||
|
m_expirePaused = false;
|
||||||
|
emit expirePausedChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshExpiry(QDateTime::currentMSecsSinceEpoch());
|
||||||
|
}
|
||||||
|
|
||||||
|
bool NotificationGroup::expired(qint64 now) const {
|
||||||
|
return m_expireDeadline > 0 && !m_expirePaused && now >= m_expireDeadline;
|
||||||
|
}
|
||||||
|
|
||||||
|
void NotificationGroup::refreshExpiry(qint64 now) {
|
||||||
|
qint64 remaining = 0;
|
||||||
|
if (m_expirePaused) {
|
||||||
|
remaining = m_expireHeld;
|
||||||
|
} else if (m_expireDeadline > 0) {
|
||||||
|
remaining = (std::max)(qint64(0), m_expireDeadline - now);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remaining != m_expireRemaining) {
|
||||||
|
m_expireRemaining = static_cast<int>(remaining);
|
||||||
|
emit expireRemainingChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
const qreal progress =
|
||||||
|
m_expireTimeout > 0
|
||||||
|
? (std::min)(1.0, static_cast<qreal>(remaining) / m_expireTimeout)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
if (!qFuzzyCompare(progress, m_expireProgress)) {
|
||||||
|
m_expireProgress = progress;
|
||||||
|
emit expireProgressChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
setExpireActive(m_expireDeadline > 0 || m_expirePaused);
|
||||||
|
}
|
||||||
|
|
||||||
|
void NotificationGroup::setExpireActive(bool active) {
|
||||||
|
if (active != m_expireActive) {
|
||||||
|
m_expireActive = active;
|
||||||
|
emit expireActiveChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bool NotificationGroup::isSameImage(const QString& path) const {
|
bool NotificationGroup::isSameImage(const QString& path) const {
|
||||||
return m_imageSource != nullptr && m_imageSource->isSameImage(path);
|
return m_imageSource != nullptr && m_imageSource->isSameImage(path);
|
||||||
}
|
}
|
||||||
@@ -721,8 +796,6 @@ bool NotificationGroup::isSameAppIcon(const QString& path) const {
|
|||||||
void NotificationGroup::lock(QObject* item) {
|
void NotificationGroup::lock(QObject* item) {
|
||||||
if (item != nullptr && !m_locks.contains(item)) {
|
if (item != nullptr && !m_locks.contains(item)) {
|
||||||
m_locks.insert(item);
|
m_locks.insert(item);
|
||||||
// No Qt::UniqueConnection here: it requires a member-function pointer,
|
|
||||||
// and the contains() check above already keeps this one-shot per item.
|
|
||||||
QObject::connect(
|
QObject::connect(
|
||||||
item, &QObject::destroyed, this, [this, item](QObject*) {
|
item, &QObject::destroyed, this, [this, item](QObject*) {
|
||||||
unlock(item);
|
unlock(item);
|
||||||
@@ -737,8 +810,6 @@ void NotificationGroup::unlock(QObject* item) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Called by the store once the group has no rows left. A delegate may still be
|
|
||||||
// animating it out, so the delete waits for its last unlock.
|
|
||||||
void NotificationGroup::release() {
|
void NotificationGroup::release() {
|
||||||
m_releasing = true;
|
m_releasing = true;
|
||||||
if (m_locks.isEmpty()) {
|
if (m_locks.isEmpty()) {
|
||||||
@@ -758,9 +829,6 @@ NotificationStore::NotificationStore(QObject* parent)
|
|||||||
m_flush->setInterval(FLUSH_MS);
|
m_flush->setInterval(FLUSH_MS);
|
||||||
QObject::connect(m_flush, &QTimer::timeout, this, &NotificationStore::flush);
|
QObject::connect(m_flush, &QTimer::timeout, this, &NotificationStore::flush);
|
||||||
|
|
||||||
// Config-backed settings, same pattern as TickingService: read the
|
|
||||||
// config node directly and update live on its change signals. QML may
|
|
||||||
// still write these properties; the next config change re-applies.
|
|
||||||
auto* notifs = config::ConfigSingleton::instance()->notifs();
|
auto* notifs = config::ConfigSingleton::instance()->notifs();
|
||||||
applyNotifConfig(notifs);
|
applyNotifConfig(notifs);
|
||||||
QObject::connect(
|
QObject::connect(
|
||||||
@@ -818,6 +886,12 @@ void NotificationStore::setPopupTimeout(int timeout) {
|
|||||||
|
|
||||||
m_popupTimeout = timeout;
|
m_popupTimeout = timeout;
|
||||||
emit popupTimeoutChanged();
|
emit popupTimeoutChanged();
|
||||||
|
|
||||||
|
if (m_autoExpire) {
|
||||||
|
for (auto* group : std::as_const(m_popupGroups)) {
|
||||||
|
group->resetExpiry(groupExpireTimeoutMs(group));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool NotificationStore::autoExpire() const {
|
bool NotificationStore::autoExpire() const {
|
||||||
@@ -831,6 +905,14 @@ void NotificationStore::setAutoExpire(bool autoExpire) {
|
|||||||
|
|
||||||
m_autoExpire = autoExpire;
|
m_autoExpire = autoExpire;
|
||||||
emit autoExpireChanged();
|
emit autoExpireChanged();
|
||||||
|
|
||||||
|
for (auto* group : std::as_const(m_popupGroups)) {
|
||||||
|
if (autoExpire) {
|
||||||
|
group->resetExpiry(groupExpireTimeoutMs(group));
|
||||||
|
} else {
|
||||||
|
group->clearExpiry();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void NotificationStore::applyNotifConfig(const config::Notifs* notifs) {
|
void NotificationStore::applyNotifConfig(const config::Notifs* notifs) {
|
||||||
@@ -881,9 +963,6 @@ NotificationRow* NotificationStore::insert(
|
|||||||
attachNotification(row, wrapped, notification);
|
attachNotification(row, wrapped, notification);
|
||||||
|
|
||||||
row->m_popup = popup;
|
row->m_popup = popup;
|
||||||
if (popup) {
|
|
||||||
row->m_deadline = QDateTime::currentMSecsSinceEpoch() + m_popupTimeout;
|
|
||||||
}
|
|
||||||
|
|
||||||
insertDbRow(row);
|
insertDbRow(row);
|
||||||
startImageCache(row);
|
startImageCache(row);
|
||||||
@@ -929,8 +1008,6 @@ void NotificationStore::clear() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The bulk delete above already removed every DB row; the per-row
|
|
||||||
// cleanup would just re-delete no-ops, so batch it instead.
|
|
||||||
m_clearing = true;
|
m_clearing = true;
|
||||||
const auto rows = m_rows;
|
const auto rows = m_rows;
|
||||||
for (auto* row : rows) {
|
for (auto* row : rows) {
|
||||||
@@ -953,6 +1030,11 @@ void NotificationStore::copyFields(NotificationRow* row, QObject* notification)
|
|||||||
row->m_resident = notification->property("resident").toBool();
|
row->m_resident = notification->property("resident").toBool();
|
||||||
row->m_hasActionIcons = notification->property("hasActionIcons").toBool();
|
row->m_hasActionIcons = notification->property("hasActionIcons").toBool();
|
||||||
row->m_urgency = notification->property("urgency").toInt();
|
row->m_urgency = notification->property("urgency").toInt();
|
||||||
|
|
||||||
|
const auto expireTimeout = notification->property("expireTimeout");
|
||||||
|
row->m_expireTimeoutMs = expireTimeout.isValid()
|
||||||
|
? static_cast<qint64>(expireTimeout.toDouble())
|
||||||
|
: -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
void NotificationStore::attachNotification(
|
void NotificationStore::attachNotification(
|
||||||
@@ -980,10 +1062,6 @@ void NotificationStore::attachNotification(
|
|||||||
rowGuard->m_store->handleClosed(rowGuard);
|
rowGuard->m_store->handleClosed(rowGuard);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Quickshell applies replaces_id updates to the object we already
|
|
||||||
// hold; mirror those changes onto the row (in UI and in the DB).
|
|
||||||
// Connected straight to the slot (not through an intermediate
|
|
||||||
// signal) so sender() in handleLiveUpdate is the Notification.
|
|
||||||
static const QMetaMethod slot = [] {
|
static const QMetaMethod slot = [] {
|
||||||
const QMetaObject& storeMeta = NotificationStore::staticMetaObject;
|
const QMetaObject& storeMeta = NotificationStore::staticMetaObject;
|
||||||
return storeMeta.method(
|
return storeMeta.method(
|
||||||
@@ -1000,6 +1078,7 @@ void NotificationStore::attachNotification(
|
|||||||
"hasActionIconsChanged",
|
"hasActionIconsChanged",
|
||||||
"actionsChanged",
|
"actionsChanged",
|
||||||
"hintsChanged",
|
"hintsChanged",
|
||||||
|
"expireTimeoutChanged",
|
||||||
};
|
};
|
||||||
|
|
||||||
const auto* meta = notification->metaObject();
|
const auto* meta = notification->metaObject();
|
||||||
@@ -1040,6 +1119,11 @@ void NotificationStore::handleLiveUpdate() {
|
|||||||
const bool hasActionIcons = it.key()->property("hasActionIcons").toBool();
|
const bool hasActionIcons = it.key()->property("hasActionIcons").toBool();
|
||||||
const int urgency = it.key()->property("urgency").toInt();
|
const int urgency = it.key()->property("urgency").toInt();
|
||||||
|
|
||||||
|
const auto expireTimeout = it.key()->property("expireTimeout");
|
||||||
|
row->m_expireTimeoutMs = expireTimeout.isValid()
|
||||||
|
? static_cast<qint64>(expireTimeout.toDouble())
|
||||||
|
: -1;
|
||||||
|
|
||||||
const bool imageChanged = imageSource != row->m_imageSource;
|
const bool imageChanged = imageSource != row->m_imageSource;
|
||||||
|
|
||||||
if (summary != row->m_summary) {
|
if (summary != row->m_summary) {
|
||||||
@@ -1095,12 +1179,6 @@ void NotificationStore::handleLiveUpdate() {
|
|||||||
startImageCache(row);
|
startImageCache(row);
|
||||||
}
|
}
|
||||||
|
|
||||||
// New content deserves a full look: restart the popup countdown.
|
|
||||||
if (row->m_popup) {
|
|
||||||
row->m_deadline = QDateTime::currentMSecsSinceEpoch() + m_popupTimeout;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The row may have moved to another app name (replaces_id can change it).
|
|
||||||
if (row->m_group != nullptr && row->m_group->m_appName != row->m_appName) {
|
if (row->m_group != nullptr && row->m_group->m_appName != row->m_appName) {
|
||||||
removeGroupRow(row);
|
removeGroupRow(row);
|
||||||
addGroup(row);
|
addGroup(row);
|
||||||
@@ -1111,6 +1189,10 @@ void NotificationStore::handleLiveUpdate() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (row->m_popupGroup != nullptr) {
|
||||||
|
touchExpiry(row);
|
||||||
|
}
|
||||||
|
|
||||||
markDirty(row);
|
markDirty(row);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1199,7 +1281,6 @@ NotificationRow* NotificationStore::rowForNotificationId(int id) const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void NotificationStore::handleClosed(NotificationRow* row) {
|
void NotificationStore::handleClosed(NotificationRow* row) {
|
||||||
// During clear() the bulk DELETE already removed every row.
|
|
||||||
if (m_clearing) {
|
if (m_clearing) {
|
||||||
removeRow(row);
|
removeRow(row);
|
||||||
return;
|
return;
|
||||||
@@ -1221,9 +1302,6 @@ void NotificationStore::handleClosed(NotificationRow* row) {
|
|||||||
row->m_group->sync();
|
row->m_group->sync();
|
||||||
}
|
}
|
||||||
|
|
||||||
// A closed row leaves the store immediately, so its group and the sidebar
|
|
||||||
// order update at once; a delegate that still holds it only delays the
|
|
||||||
// delete, never the removal.
|
|
||||||
removeRow(row);
|
removeRow(row);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1236,14 +1314,10 @@ void NotificationStore::removeRow(NotificationRow* row) {
|
|||||||
removeGroupRow(row);
|
removeGroupRow(row);
|
||||||
m_dirty.remove(row);
|
m_dirty.remove(row);
|
||||||
|
|
||||||
// The row is gone; drop any live-notification mapping to it so
|
|
||||||
// handleLiveUpdate can never resolve a freed pointer.
|
|
||||||
erase_if(m_liveRows, [row](std::pair<const QObject*, NotificationRow*> e) {
|
erase_if(m_liveRows, [row](std::pair<const QObject*, NotificationRow*> e) {
|
||||||
return e.second == row;
|
return e.second == row;
|
||||||
});
|
});
|
||||||
|
|
||||||
// A delegate animating this row out still holds it; unlock() deletes once
|
|
||||||
// the last lock is gone.
|
|
||||||
if (row->m_locks.isEmpty()) {
|
if (row->m_locks.isEmpty()) {
|
||||||
row->deleteLater();
|
row->deleteLater();
|
||||||
} else {
|
} else {
|
||||||
@@ -1302,7 +1376,6 @@ void NotificationStore::addGroup(NotificationRow* row) {
|
|||||||
m_groups.prepend(group);
|
m_groups.prepend(group);
|
||||||
emit groupsChanged();
|
emit groupsChanged();
|
||||||
} else {
|
} else {
|
||||||
// Most-recently-active group first, matching the sidebar order.
|
|
||||||
const qsizetype index = m_groups.indexOf(group);
|
const qsizetype index = m_groups.indexOf(group);
|
||||||
if (index > 0) {
|
if (index > 0) {
|
||||||
m_groups.move(index, 0);
|
m_groups.move(index, 0);
|
||||||
@@ -1333,8 +1406,6 @@ void NotificationStore::removeGroupRow(NotificationRow* row) {
|
|||||||
emit groupsChanged();
|
emit groupsChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Popup groups mirror m_groups over the rows currently showing as popups, so
|
|
||||||
// the popup stack can render one card per app instead of one per notification.
|
|
||||||
void NotificationStore::addPopupGroup(NotificationRow* row) {
|
void NotificationStore::addPopupGroup(NotificationRow* row) {
|
||||||
if (row->m_popupGroup != nullptr) {
|
if (row->m_popupGroup != nullptr) {
|
||||||
return;
|
return;
|
||||||
@@ -1356,9 +1427,19 @@ void NotificationStore::addPopupGroup(NotificationRow* row) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
row->m_popupGroup = group;
|
row->m_popupGroup = group;
|
||||||
|
|
||||||
|
touchExpiry(row);
|
||||||
|
|
||||||
group->addRow(row);
|
group->addRow(row);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void NotificationStore::expireGroup(NotificationGroup* group) {
|
||||||
|
const auto rows = group->m_notifs; // setPopup() mutates the group's list
|
||||||
|
for (auto* row : rows) {
|
||||||
|
row->setPopup(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void NotificationStore::removePopupGroupRow(NotificationRow* row) {
|
void NotificationStore::removePopupGroupRow(NotificationRow* row) {
|
||||||
auto* group = row->m_popupGroup;
|
auto* group = row->m_popupGroup;
|
||||||
if (group == nullptr) {
|
if (group == nullptr) {
|
||||||
@@ -1378,6 +1459,32 @@ void NotificationStore::removePopupGroupRow(NotificationRow* row) {
|
|||||||
emit popupGroupsChanged();
|
emit popupGroupsChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int NotificationStore::expireTimeoutMs(const NotificationRow* row) const {
|
||||||
|
if (row->m_expireTimeoutMs == 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (row->m_expireTimeoutMs < 0) {
|
||||||
|
return m_popupTimeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
return static_cast<int>((std::min)(row->m_expireTimeoutMs, MAX_EXPIRE_MS));
|
||||||
|
}
|
||||||
|
|
||||||
|
int NotificationStore::groupExpireTimeoutMs(
|
||||||
|
const NotificationGroup* group) const {
|
||||||
|
const auto* newest = group->m_notifs.value(0);
|
||||||
|
return newest != nullptr ? expireTimeoutMs(newest) : m_popupTimeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
void NotificationStore::touchExpiry(NotificationRow* row) {
|
||||||
|
if (!m_autoExpire || row->m_popupGroup == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
row->m_popupGroup->resetExpiry(expireTimeoutMs(row));
|
||||||
|
}
|
||||||
|
|
||||||
void NotificationStore::updatePopupGroup(NotificationRow* row) {
|
void NotificationStore::updatePopupGroup(NotificationRow* row) {
|
||||||
if (row->m_popup && !row->m_closed) {
|
if (row->m_popup && !row->m_closed) {
|
||||||
addPopupGroup(row);
|
addPopupGroup(row);
|
||||||
@@ -1450,10 +1557,6 @@ bool NotificationStore::openDb() {
|
|||||||
|
|
||||||
QSqlQuery query(db);
|
QSqlQuery query(db);
|
||||||
const QStringList statements = {
|
const QStringList statements = {
|
||||||
// WAL + NORMAL: commits don't fsync per insert (the fsync is what
|
|
||||||
// made every notification insert 5-8ms). Safe with WAL: an
|
|
||||||
// application crash still recovers the WAL; only an OS/power
|
|
||||||
// failure can lose the most recent commits.
|
|
||||||
"PRAGMA journal_mode=WAL",
|
"PRAGMA journal_mode=WAL",
|
||||||
"PRAGMA synchronous=NORMAL",
|
"PRAGMA synchronous=NORMAL",
|
||||||
"CREATE TABLE IF NOT EXISTS notifications("
|
"CREATE TABLE IF NOT EXISTS notifications("
|
||||||
@@ -1479,8 +1582,6 @@ bool NotificationStore::openDb() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Databases created before progress was stored lack the column; ALTER adds
|
|
||||||
// it instead of dropping history (caelestia drops and recreates).
|
|
||||||
if (!db.record("notifications").contains("progress")) {
|
if (!db.record("notifications").contains("progress")) {
|
||||||
QSqlQuery alter(db);
|
QSqlQuery alter(db);
|
||||||
if (!alter.exec(
|
if (!alter.exec(
|
||||||
@@ -1574,10 +1675,6 @@ void NotificationStore::loadRows() {
|
|||||||
row->m_urgency = query.value(7).toInt();
|
row->m_urgency = query.value(7).toInt();
|
||||||
row->m_resident = query.value(8).toInt() != 0;
|
row->m_resident = query.value(8).toInt() != 0;
|
||||||
row->m_hasActionIcons = query.value(9).toInt() != 0;
|
row->m_hasActionIcons = query.value(9).toInt() != 0;
|
||||||
// Popup state is deliberately not restored: showing stored
|
|
||||||
// notifications as toasts again after a shell restart would
|
|
||||||
// replay an old burst, and with autoExpire off the flag never
|
|
||||||
// clears itself. The rows stay in the sidebar instead.
|
|
||||||
row->m_popup = false;
|
row->m_popup = false;
|
||||||
row->m_imageSource = query.value(12).toString();
|
row->m_imageSource = query.value(12).toString();
|
||||||
row->m_digest = query.value(13).toString();
|
row->m_digest = query.value(13).toString();
|
||||||
@@ -1649,7 +1746,6 @@ void NotificationStore::migrateLegacyJson() {
|
|||||||
row->m_urgency = object.value("urgency").toInt(1);
|
row->m_urgency = object.value("urgency").toInt(1);
|
||||||
row->m_resident = object.value("resident").toBool();
|
row->m_resident = object.value("resident").toBool();
|
||||||
row->m_hasActionIcons = object.value("hasActionIcons").toBool();
|
row->m_hasActionIcons = object.value("hasActionIcons").toBool();
|
||||||
// Same reasoning as loadRows(): never re-toast migrated rows.
|
|
||||||
row->m_popup = false;
|
row->m_popup = false;
|
||||||
|
|
||||||
const auto time = object.value("time");
|
const auto time = object.value("time");
|
||||||
@@ -1708,8 +1804,6 @@ void NotificationStore::prune() {
|
|||||||
<< query.lastError();
|
<< query.lastError();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Total cap, preferring higher urgency so a chatty app cannot push out
|
|
||||||
// an important notification.
|
|
||||||
if (!query.exec(
|
if (!query.exec(
|
||||||
"DELETE FROM notifications WHERE id NOT IN"
|
"DELETE FROM notifications WHERE id NOT IN"
|
||||||
" (SELECT id FROM notifications ORDER BY urgency DESC, time DESC"
|
" (SELECT id FROM notifications ORDER BY urgency DESC, time DESC"
|
||||||
@@ -1720,7 +1814,6 @@ void NotificationStore::prune() {
|
|||||||
<< query.lastError();
|
<< query.lastError();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Per-app cap: one app flooding must not crowd out the other groups.
|
|
||||||
QStringList appNames;
|
QStringList appNames;
|
||||||
if (query.exec("SELECT DISTINCT app_name FROM notifications")) {
|
if (query.exec("SELECT DISTINCT app_name FROM notifications")) {
|
||||||
while (query.next()) {
|
while (query.next()) {
|
||||||
@@ -1753,8 +1846,6 @@ void NotificationStore::trimRows() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only closed, unlocked rows are droppable (open rows may still be
|
|
||||||
// acted on by the sender). Weakest first: lowest urgency, then oldest.
|
|
||||||
const auto db = QSqlDatabase::database(m_uuid, false);
|
const auto db = QSqlDatabase::database(m_uuid, false);
|
||||||
|
|
||||||
while (m_rows.size() > MAX_ROWS) {
|
while (m_rows.size() > MAX_ROWS) {
|
||||||
@@ -1802,11 +1893,6 @@ void NotificationStore::startImageCache(NotificationRow* row) {
|
|||||||
|
|
||||||
const QUrl url(row->m_imageSource);
|
const QUrl url(row->m_imageSource);
|
||||||
|
|
||||||
// Resolve *what* to read on the main thread (engine provider lookups and
|
|
||||||
// QIcon's theme state belong here), but do no file scanning and no
|
|
||||||
// decoding here: providers are designed to serve requestImage from non-GUI
|
|
||||||
// threads, and SVG rasterization was blocking the shell 3-9ms per
|
|
||||||
// notification.
|
|
||||||
QString localPath;
|
QString localPath;
|
||||||
QQuickImageProvider* provider = nullptr;
|
QQuickImageProvider* provider = nullptr;
|
||||||
QString providerId;
|
QString providerId;
|
||||||
@@ -1825,20 +1911,9 @@ void NotificationStore::startImageCache(NotificationRow* row) {
|
|||||||
|
|
||||||
if (host != QLatin1String("qsimage") &&
|
if (host != QLatin1String("qsimage") &&
|
||||||
host != QLatin1String("qspixmap")) {
|
host != QLatin1String("qspixmap")) {
|
||||||
// Provider urls such as image://icon/... are served by KF6's
|
|
||||||
// KIconLoader, which is not thread-safe (calling it from a
|
|
||||||
// worker crashed the shell). Only QIcon's theme state is read
|
|
||||||
// here, because the GUI thread owns it; the candidate scanning it
|
|
||||||
// feeds is pure filesystem work and runs in the worker below. A
|
|
||||||
// name the theme does not have means thousands of stat calls, and
|
|
||||||
// doing those here stalled inserts for milliseconds. If nothing
|
|
||||||
// matches, the row keeps rendering the url directly.
|
|
||||||
iconName = id;
|
iconName = id;
|
||||||
theme = iconTheme();
|
theme = iconTheme();
|
||||||
} else {
|
} else {
|
||||||
// Transient inline notification images (image://qsimage/...).
|
|
||||||
// Provider objects are engine-owned and outlive this task;
|
|
||||||
// ids owned by a destroyed notification resolve to nothing.
|
|
||||||
auto* engine = qmlEngine(this);
|
auto* engine = qmlEngine(this);
|
||||||
if (engine == nullptr) {
|
if (engine == nullptr) {
|
||||||
return;
|
return;
|
||||||
@@ -1864,10 +1939,6 @@ void NotificationStore::startImageCache(NotificationRow* row) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy the pixels here instead of in the worker: the handle
|
|
||||||
// belongs to the sender's notification, and it can be gone by the
|
|
||||||
// time the task runs, which silently dropped the picture. This is
|
|
||||||
// a memory copy of an already decoded image, not a rasterization.
|
|
||||||
inlineImage =
|
inlineImage =
|
||||||
usePixmap
|
usePixmap
|
||||||
? provider->requestPixmap(providerId, nullptr, QSize())
|
? provider->requestPixmap(providerId, nullptr, QSize())
|
||||||
@@ -1938,8 +2009,6 @@ void NotificationStore::startImageCache(NotificationRow* row) {
|
|||||||
|
|
||||||
normalized = normalized.convertToFormat(QImage::Format_RGBA8888);
|
normalized = normalized.convertToFormat(QImage::Format_RGBA8888);
|
||||||
|
|
||||||
// Cache the avatar content already cut to a circle, so the view
|
|
||||||
// does not have to mask it with a layer effect every frame.
|
|
||||||
const int side =
|
const int side =
|
||||||
(std::min)(normalized.width(), normalized.height());
|
(std::min)(normalized.width(), normalized.height());
|
||||||
const QRect square(
|
const QRect square(
|
||||||
@@ -2020,8 +2089,6 @@ void NotificationStore::storeImageBlob(
|
|||||||
|
|
||||||
row->m_digest = digest;
|
row->m_digest = digest;
|
||||||
row->m_image = QStringLiteral("image://notifimg/") + digest;
|
row->m_image = QStringLiteral("image://notifimg/") + digest;
|
||||||
// The group reads its image through the source row and listens to
|
|
||||||
// imageChanged, so this emit is enough to keep the card in sync.
|
|
||||||
emit row->imageChanged();
|
emit row->imageChanged();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2029,9 +2096,15 @@ void NotificationStore::storeImageBlob(
|
|||||||
void NotificationStore::tick() {
|
void NotificationStore::tick() {
|
||||||
const qint64 now = QDateTime::currentMSecsSinceEpoch();
|
const qint64 now = QDateTime::currentMSecsSinceEpoch();
|
||||||
|
|
||||||
for (auto* row : std::as_const(m_rows)) {
|
if (m_autoExpire) {
|
||||||
if (m_autoExpire && row->expired(now)) {
|
const auto groups = m_popupGroups;
|
||||||
row->setPopup(false);
|
|
||||||
|
for (auto* group : groups) {
|
||||||
|
if (group->expired(now)) {
|
||||||
|
expireGroup(group);
|
||||||
|
} else {
|
||||||
|
group->refreshExpiry(now);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -134,7 +134,6 @@ class NotificationRow : public QObject {
|
|||||||
friend class NotificationGroup;
|
friend class NotificationGroup;
|
||||||
friend NotificationStore;
|
friend NotificationStore;
|
||||||
|
|
||||||
[[nodiscard]] bool expired(qint64 now) const;
|
|
||||||
void refreshTimeStr();
|
void refreshTimeStr();
|
||||||
|
|
||||||
QList<NotificationActionRow*> m_actions;
|
QList<NotificationActionRow*> m_actions;
|
||||||
@@ -151,7 +150,7 @@ class NotificationRow : public QObject {
|
|||||||
QString m_summary;
|
QString m_summary;
|
||||||
QString m_timeStr;
|
QString m_timeStr;
|
||||||
|
|
||||||
qint64 m_deadline = 0;
|
qint64 m_expireTimeoutMs = -1;
|
||||||
qint64 m_rowId = -1;
|
qint64 m_rowId = -1;
|
||||||
qint64 m_timeMs = 0;
|
qint64 m_timeMs = 0;
|
||||||
|
|
||||||
@@ -159,15 +158,12 @@ class NotificationRow : public QObject {
|
|||||||
QSet<QObject*> m_locks;
|
QSet<QObject*> m_locks;
|
||||||
QJSValue m_jsNotification;
|
QJSValue m_jsNotification;
|
||||||
|
|
||||||
// The store has already dropped this row from its lists, but a delegate is
|
|
||||||
// still animating it out; the delete waits for the last unlock.
|
|
||||||
bool m_deleteAfterUnlock = false;
|
bool m_deleteAfterUnlock = false;
|
||||||
|
|
||||||
bool m_closed = false;
|
bool m_closed = false;
|
||||||
bool m_hasActionIcons = false;
|
bool m_hasActionIcons = false;
|
||||||
bool m_liveConnected = false;
|
bool m_liveConnected = false;
|
||||||
bool m_popup = false;
|
bool m_popup = false;
|
||||||
bool m_popupPaused = false;
|
|
||||||
bool m_resident = false;
|
bool m_resident = false;
|
||||||
|
|
||||||
int m_notificationId = 0;
|
int m_notificationId = 0;
|
||||||
@@ -186,6 +182,13 @@ class NotificationGroup : public QObject {
|
|||||||
Q_PROPERTY(QString appIcon READ appIcon NOTIFY appIconChanged)
|
Q_PROPERTY(QString appIcon READ appIcon NOTIFY appIconChanged)
|
||||||
Q_PROPERTY(QString appName READ appName CONSTANT)
|
Q_PROPERTY(QString appName READ appName CONSTANT)
|
||||||
Q_PROPERTY(int count READ count NOTIFY countChanged)
|
Q_PROPERTY(int count READ count NOTIFY countChanged)
|
||||||
|
Q_PROPERTY(bool expireActive READ expireActive NOTIFY expireActiveChanged)
|
||||||
|
Q_PROPERTY(bool expirePaused READ expirePaused NOTIFY expirePausedChanged)
|
||||||
|
Q_PROPERTY(
|
||||||
|
qreal expireProgress READ expireProgress NOTIFY expireProgressChanged)
|
||||||
|
Q_PROPERTY(
|
||||||
|
int expireRemaining READ expireRemaining NOTIFY expireRemainingChanged)
|
||||||
|
Q_PROPERTY(int expireTimeout READ expireTimeout NOTIFY expireTimeoutChanged)
|
||||||
Q_PROPERTY(QString firstSummary READ firstSummary NOTIFY firstSummaryChanged)
|
Q_PROPERTY(QString firstSummary READ firstSummary NOTIFY firstSummaryChanged)
|
||||||
Q_PROPERTY(QString image READ image NOTIFY imageChanged)
|
Q_PROPERTY(QString image READ image NOTIFY imageChanged)
|
||||||
Q_PROPERTY(
|
Q_PROPERTY(
|
||||||
@@ -201,21 +204,34 @@ class NotificationGroup : public QObject {
|
|||||||
[[nodiscard]] QString appIcon() const;
|
[[nodiscard]] QString appIcon() const;
|
||||||
[[nodiscard]] QString appName() const;
|
[[nodiscard]] QString appName() const;
|
||||||
[[nodiscard]] int count() const;
|
[[nodiscard]] int count() const;
|
||||||
|
[[nodiscard]] bool expireActive() const;
|
||||||
|
[[nodiscard]] bool expirePaused() const;
|
||||||
|
[[nodiscard]] qreal expireProgress() const;
|
||||||
|
[[nodiscard]] int expireRemaining() const;
|
||||||
|
[[nodiscard]] int expireTimeout() const;
|
||||||
[[nodiscard]] QString firstSummary() const;
|
[[nodiscard]] QString firstSummary() const;
|
||||||
[[nodiscard]] QString image() const;
|
[[nodiscard]] QString image() const;
|
||||||
[[nodiscard]] QQmlListProperty<NotificationRow> notifs();
|
[[nodiscard]] QQmlListProperty<NotificationRow> notifs();
|
||||||
[[nodiscard]] QString timeStr() const;
|
[[nodiscard]] QString timeStr() const;
|
||||||
[[nodiscard]] int urgency() const;
|
[[nodiscard]] int urgency() const;
|
||||||
|
|
||||||
|
Q_INVOKABLE void holdExpiry();
|
||||||
Q_INVOKABLE bool isSameAppIcon(const QString& path) const;
|
Q_INVOKABLE bool isSameAppIcon(const QString& path) const;
|
||||||
Q_INVOKABLE bool isSameImage(const QString& path) const;
|
Q_INVOKABLE bool isSameImage(const QString& path) const;
|
||||||
Q_INVOKABLE void lock(QObject* item);
|
Q_INVOKABLE void lock(QObject* item);
|
||||||
|
Q_INVOKABLE void releaseExpiry();
|
||||||
Q_INVOKABLE void unlock(QObject* item);
|
Q_INVOKABLE void unlock(QObject* item);
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void activeNotifsChanged();
|
void activeNotifsChanged();
|
||||||
void appIconChanged();
|
void appIconChanged();
|
||||||
void countChanged();
|
void countChanged();
|
||||||
|
void expireActiveChanged();
|
||||||
|
void expirePausedChanged();
|
||||||
|
void expireProgressChanged();
|
||||||
|
void expireRemainingChanged();
|
||||||
|
void expireTimerRestarted();
|
||||||
|
void expireTimeoutChanged();
|
||||||
void firstSummaryChanged();
|
void firstSummaryChanged();
|
||||||
void imageChanged();
|
void imageChanged();
|
||||||
void notifsChanged();
|
void notifsChanged();
|
||||||
@@ -226,17 +242,19 @@ class NotificationGroup : public QObject {
|
|||||||
friend NotificationStore;
|
friend NotificationStore;
|
||||||
|
|
||||||
void addRow(NotificationRow* row);
|
void addRow(NotificationRow* row);
|
||||||
|
void clearExpiry();
|
||||||
|
bool expired(qint64 now) const;
|
||||||
void refreshActive();
|
void refreshActive();
|
||||||
|
void refreshExpiry(qint64 now);
|
||||||
void release();
|
void release();
|
||||||
void removeRow(NotificationRow* row);
|
void removeRow(NotificationRow* row);
|
||||||
|
void resetExpiry(int timeout);
|
||||||
|
void setExpireActive(bool active);
|
||||||
void sync();
|
void sync();
|
||||||
|
|
||||||
QList<NotificationRow*> m_active;
|
QList<NotificationRow*> m_active;
|
||||||
QList<NotificationRow*> m_notifs;
|
QList<NotificationRow*> m_notifs;
|
||||||
|
|
||||||
// The group reads image/appIcon through the row it took them from, so a
|
|
||||||
// row swapping between its raw source and the cached blob keeps the card
|
|
||||||
// in sync instead of holding a stale copy.
|
|
||||||
QPointer<NotificationRow> m_iconSource;
|
QPointer<NotificationRow> m_iconSource;
|
||||||
QPointer<NotificationRow> m_imageSource;
|
QPointer<NotificationRow> m_imageSource;
|
||||||
QSet<QObject*> m_locks;
|
QSet<QObject*> m_locks;
|
||||||
@@ -245,6 +263,15 @@ class NotificationGroup : public QObject {
|
|||||||
QString m_firstSummary;
|
QString m_firstSummary;
|
||||||
QString m_timeStr;
|
QString m_timeStr;
|
||||||
|
|
||||||
|
qint64 m_expireDeadline = 0;
|
||||||
|
qint64 m_expireHeld = 0;
|
||||||
|
qreal m_expireProgress = 0;
|
||||||
|
int m_expireRemaining = 0;
|
||||||
|
int m_expireTimeout = 0;
|
||||||
|
|
||||||
|
bool m_expireActive = false;
|
||||||
|
bool m_expirePaused = false;
|
||||||
|
|
||||||
int m_count = 0;
|
int m_count = 0;
|
||||||
int m_urgency = 1;
|
int m_urgency = 1;
|
||||||
bool m_releasing = false;
|
bool m_releasing = false;
|
||||||
@@ -312,6 +339,8 @@ class NotificationStore : public QObject {
|
|||||||
void applyNotifConfig(const config::Notifs* notifs);
|
void applyNotifConfig(const config::Notifs* notifs);
|
||||||
void closeDb();
|
void closeDb();
|
||||||
void copyFields(NotificationRow* row, QObject* notification);
|
void copyFields(NotificationRow* row, QObject* notification);
|
||||||
|
[[nodiscard]] int expireTimeoutMs(const NotificationRow* row) const;
|
||||||
|
void expireGroup(NotificationGroup* group);
|
||||||
void flush();
|
void flush();
|
||||||
void handleClosed(NotificationRow* row);
|
void handleClosed(NotificationRow* row);
|
||||||
void insertDbRow(NotificationRow* row);
|
void insertDbRow(NotificationRow* row);
|
||||||
@@ -323,12 +352,12 @@ class NotificationStore : public QObject {
|
|||||||
void prune();
|
void prune();
|
||||||
void addPopupGroup(NotificationRow* row);
|
void addPopupGroup(NotificationRow* row);
|
||||||
void removePopupGroupRow(NotificationRow* row);
|
void removePopupGroupRow(NotificationRow* row);
|
||||||
|
void touchExpiry(NotificationRow* row);
|
||||||
|
[[nodiscard]] int groupExpireTimeoutMs(const NotificationGroup* group) const;
|
||||||
void updatePopupGroup(NotificationRow* row);
|
void updatePopupGroup(NotificationRow* row);
|
||||||
void registerProvider();
|
void registerProvider();
|
||||||
void removeGroupRow(NotificationRow* row);
|
void removeGroupRow(NotificationRow* row);
|
||||||
void removeRow(NotificationRow* row);
|
void removeRow(NotificationRow* row);
|
||||||
// Coalesced rowsChanged: a burst of inserts/removes triggers one QML
|
|
||||||
// rebind on the next event loop turn instead of one per notification.
|
|
||||||
void scheduleRowsChanged();
|
void scheduleRowsChanged();
|
||||||
[[nodiscard]] NotificationRow* rowForNotificationId(int id) const;
|
[[nodiscard]] NotificationRow* rowForNotificationId(int id) const;
|
||||||
void startImageCache(NotificationRow* row);
|
void startImageCache(NotificationRow* row);
|
||||||
|
|||||||
Reference in New Issue
Block a user