From ae2a3492479873923d78735fd37d05db06ff89ed Mon Sep 17 00:00:00 2001 From: zach Date: Tue, 26 May 2026 22:52:54 +0200 Subject: [PATCH] optimize notification icon caching by copying image rather than item --- Daemons/NotifServer.qml | 112 +++++---- Drawers/Windows.qml | 1 + Modules/Dashboard/Wrapper.qml | 2 +- Modules/Dock/Wrapper.qml | 11 +- Modules/Drawing/Wrapper.qml | 2 + Modules/Notifications/Content.qml | 48 +--- Modules/Settings/Content.qml | 2 +- Plugins/ZShell/writefile.cpp | 396 ++++++++++++++++++++++++------ Plugins/ZShell/writefile.hpp | 38 +-- 9 files changed, 408 insertions(+), 204 deletions(-) diff --git a/Daemons/NotifServer.qml b/Daemons/NotifServer.qml index 4497370..e5a560f 100644 --- a/Daemons/NotifServer.qml +++ b/Daemons/NotifServer.qml @@ -179,6 +179,8 @@ Singleton { property string appIcon property string appName property string body + property string cachedImageSource: "" + property bool cachingImage: false property bool closed readonly property Connections conn: Connections { function onActionsChanged(): void { @@ -214,9 +216,9 @@ Singleton { } function onImageChanged(): void { - notif.image = notif.notification.image; - if (notif.notification?.image) - notif.dummyImageLoader.active = true; + notif.imageSource = notif.notification.image || ""; + notif.image = notif.imageSource; + notif.cacheImageIfNeeded(); } function onResidentChanged(): void { @@ -233,60 +235,12 @@ Singleton { target: notif.notification } - readonly property LazyLoader dummyImageLoader: LazyLoader { - active: false - - PanelWindow { - color: "transparent" - implicitHeight: Config.notifs.sizes.image - implicitWidth: Config.notifs.sizes.image - - mask: Region { - } - - Image { - function tryCache(): void { - if (status !== Image.Ready || width != Config.notifs.sizes.image || height != Config.notifs.sizes.image) - return; - - const cacheKey = notif.appName + notif.summary + notif.id; - let h1 = 0xdeadbeef, h2 = 0x41c6ce57, ch; - for (let i = 0; i < cacheKey.length; i++) { - ch = cacheKey.charCodeAt(i); - h1 = Math.imul(h1 ^ ch, 2654435761); - h2 = Math.imul(h2 ^ ch, 1597334677); - } - h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507); - h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909); - h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507); - h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909); - const hash = (h2 >>> 0).toString(16).padStart(8, 0) + (h1 >>> 0).toString(16).padStart(8, 0); - - const cache = `${Paths.notifimagecache}/${hash}.png`; - ZShellIo.saveItem(this, Qt.resolvedUrl(cache), () => { - notif.image = cache; - notif.dummyImageLoader.active = false; - }); - } - - anchors.fill: parent - asynchronous: true - cache: false - fillMode: Image.PreserveAspectCrop - opacity: 0 - source: Qt.resolvedUrl(notif.image) - - onHeightChanged: tryCache() - onStatusChanged: tryCache() - onWidthChanged: tryCache() - } - } - } property real expireTimeout: 5 property bool hasActionIcons - property string id property string image + property string imageSource property var locks: new Set() + property string notifId property Notification notification property bool popup property bool resident @@ -329,6 +283,35 @@ Singleton { } property int urgency: NotificationUrgency.Normal + function cacheImageIfNeeded(): void { + const source = imageSource; + + if (!source || cachingImage) + return; + + if (cachedImageSource === source) + return; + + if (source.startsWith("file:")) { + cachedImageSource = source; + image = source; + return; + } + + const hash = hashForString(source); + const cache = `${Paths.notifimagecache}/${hash}.png`; + const cacheUrl = Qt.resolvedUrl(cache); + + cachingImage = true; + ZShellIo.saveImage(source, cacheUrl, () => { + cachedImageSource = source; + image = cache; + cachingImage = false; + }, () => { + cachingImage = false; + }); + } + function close(): void { closed = true; if (locks.size === 0 && root.list.includes(this)) { @@ -338,6 +321,20 @@ Singleton { } } + function hashForString(s: string): string { + let h1 = 0xdeadbeef, h2 = 0x41c6ce57, ch; + for (let i = 0; i < s.length; i++) { + ch = s.charCodeAt(i); + h1 = Math.imul(h1 ^ ch, 2654435761); + h2 = Math.imul(h2 ^ ch, 1597334677); + } + h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507); + h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909); + h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507); + h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909); + return (h2 >>> 0).toString(16).padStart(8, "0") + (h1 >>> 0).toString(16).padStart(8, "0"); + } + function lock(item: Item): void { locks.add(item); } @@ -352,14 +349,13 @@ Singleton { if (!notification) return; - id = notification.id; + notifId = notification.id; summary = notification.summary; body = notification.body; appIcon = notification.appIcon; appName = notification.appName; - image = notification.image; - if (notification?.image) - dummyImageLoader.active = true; + imageSource = notification.image || ""; + image = imageSource; expireTimeout = notification.expireTimeout; urgency = notification.urgency; resident = notification.resident; @@ -369,6 +365,8 @@ Singleton { text: a.text, invoke: () => a.invoke() })); + + cacheImageIfNeeded(); } } } diff --git a/Drawers/Windows.qml b/Drawers/Windows.qml index ed91fda..a1c9df4 100644 --- a/Drawers/Windows.qml +++ b/Drawers/Windows.qml @@ -229,6 +229,7 @@ Variants { id: notifsBg panel: panels.notifications + radius: Appearance.rounding.normal } PanelBg { diff --git a/Modules/Dashboard/Wrapper.qml b/Modules/Dashboard/Wrapper.qml index 0e2fcf4..791a31c 100644 --- a/Modules/Dashboard/Wrapper.qml +++ b/Modules/Dashboard/Wrapper.qml @@ -20,7 +20,7 @@ Item { required property PersistentProperties visibilities implicitHeight: content.implicitHeight - implicitWidth: content.implicitWidth || 854 // Hard coded fallback for first open + implicitWidth: content.implicitWidth || 854 opacity: 1 - offsetScale visible: offsetScale < 1 diff --git a/Modules/Dock/Wrapper.qml b/Modules/Dock/Wrapper.qml index 26dbb15..d4d5aa3 100644 --- a/Modules/Dock/Wrapper.qml +++ b/Modules/Dock/Wrapper.qml @@ -9,18 +9,17 @@ Item { id: root property int contentHeight + property real offsetScale: shouldBeActive ? 0 : 1 required property var panels required property ShellScreen screen + readonly property bool shouldBeActive: visibilities.dock && Config.dock.enable required property PersistentProperties visibilities - readonly property bool shouldBeActive: visibilities.dock - property real offsetScale: shouldBeActive ? 0 : 1 - - visible: offsetScale < 1 anchors.bottomMargin: (-implicitHeight - 5) * offsetScale implicitHeight: content.implicitHeight implicitWidth: content.implicitWidth || 400 opacity: 1 - offsetScale + visible: offsetScale < 1 Behavior on offsetScale { Anim { @@ -32,10 +31,10 @@ Item { Loader { id: content + active: root.shouldBeActive || root.visible anchors.left: parent.left anchors.top: parent.top - - active: root.shouldBeActive || root.visible + asynchronous: true sourceComponent: Content { panels: root.panels diff --git a/Modules/Drawing/Wrapper.qml b/Modules/Drawing/Wrapper.qml index a97cce7..62a1705 100644 --- a/Modules/Drawing/Wrapper.qml +++ b/Modules/Drawing/Wrapper.qml @@ -47,6 +47,7 @@ Item { active: Qt.binding(() => root.shouldBeActive || root.visible) anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter + asynchronous: true height: content.contentItem.height opacity: root.expanded ? 0 : 1 @@ -65,6 +66,7 @@ Item { anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter + asynchronous: true opacity: root.expanded ? 1 : 0 Behavior on opacity { diff --git a/Modules/Notifications/Content.qml b/Modules/Notifications/Content.qml index cb4e4e7..ac53731 100644 --- a/Modules/Notifications/Content.qml +++ b/Modules/Notifications/Content.qml @@ -8,7 +8,7 @@ import QtQuick Item { id: root - readonly property int padding: 6 + readonly property int padding: Appearance.padding.smaller required property Item panels required property PersistentProperties visibilities @@ -54,7 +54,7 @@ Item { anchors.fill: parent anchors.margins: root.padding color: "transparent" - radius: Appearance.rounding.smallest / 2 + radius: Appearance.rounding.normal - root.padding CustomListView { id: list @@ -72,7 +72,7 @@ Item { required property NotifServer.Notif modelData readonly property alias nonAnimHeight: notif.nonAnimHeight - implicitHeight: notif.implicitHeight + (idx === 0 ? 0 : 8) + implicitHeight: notif.implicitHeight + (idx === 0 ? 0 : Appearance.spacing.small) implicitWidth: notif.implicitWidth ListView.onRemove: removeAnim.start() @@ -151,48 +151,6 @@ Item { property: "y" } } - - ExtraIndicator { - anchors.top: parent.top - extra: { - const count = list.count; - if (count === 0) - return 0; - - const scrollY = list.contentY; - - let height = 0; - for (let i = 0; i < count; i++) { - height += (list.itemAtIndex(i)?.nonAnimHeight ?? 0) + 8; - - if (height - 8 >= scrollY) - return i; - } - - return count; - } - } - - ExtraIndicator { - anchors.bottom: parent.bottom - extra: { - const count = list.count; - if (count === 0) - return 0; - - const scrollY = list.contentHeight - (list.contentY + list.height); - - let height = 0; - for (let i = count - 1; i >= 0; i--) { - height += (list.itemAtIndex(i)?.nonAnimHeight ?? 0) + 8; - - if (height - 8 >= scrollY) - return count - i - 1; - } - - return 0; - } - } } } diff --git a/Modules/Settings/Content.qml b/Modules/Settings/Content.qml index 8759357..81d33cf 100644 --- a/Modules/Settings/Content.qml +++ b/Modules/Settings/Content.qml @@ -134,7 +134,7 @@ Item { anchors.right: parent.right anchors.top: searchBar.bottom anchors.topMargin: Appearance.spacing.smaller - color: DynamicColors.tPalette.m3surfaceContainerLowest + color: DynamicColors.tPalette.m3surfaceContainer radius: Appearance.rounding.normal StackView { diff --git a/Plugins/ZShell/writefile.cpp b/Plugins/ZShell/writefile.cpp index a9d30b5..195edd4 100644 --- a/Plugins/ZShell/writefile.cpp +++ b/Plugins/ZShell/writefile.cpp @@ -1,131 +1,369 @@ #include "writefile.hpp" #include +#include #include #include + #include +#include #include #include +#include +#include #include namespace ZShell { +// ============================================================ +// saveItem +// ============================================================ + void ZShellIo::saveItem(QQuickItem* target, const QUrl& path) { - this->saveItem(target, path, QRect(), QJSValue(), QJSValue()); + this->saveItem(target, path, QRect(), QJSValue(), QJSValue()); } void ZShellIo::saveItem(QQuickItem* target, const QUrl& path, const QRect& rect) { - this->saveItem(target, path, rect, QJSValue(), QJSValue()); + this->saveItem(target, path, rect, QJSValue(), QJSValue()); } void ZShellIo::saveItem(QQuickItem* target, const QUrl& path, QJSValue onSaved) { - this->saveItem(target, path, QRect(), onSaved, QJSValue()); + this->saveItem(target, path, QRect(), onSaved, QJSValue()); } void ZShellIo::saveItem(QQuickItem* target, const QUrl& path, QJSValue onSaved, QJSValue onFailed) { - this->saveItem(target, path, QRect(), onSaved, onFailed); + this->saveItem(target, path, QRect(), onSaved, onFailed); } void ZShellIo::saveItem(QQuickItem* target, const QUrl& path, const QRect& rect, QJSValue onSaved) { - this->saveItem(target, path, rect, onSaved, QJSValue()); + this->saveItem(target, path, rect, onSaved, QJSValue()); } -void ZShellIo::saveItem(QQuickItem* target, const QUrl& path, const QRect& rect, QJSValue onSaved, QJSValue onFailed) { - if (!target) { - qWarning() << "ZShellIo::saveItem: a target is required"; - return; - } +void ZShellIo::saveItem( + QQuickItem* target, + const QUrl& path, + const QRect& rect, + QJSValue onSaved, + QJSValue onFailed + ) { + if (!target) { + qWarning() << "ZShellIo::saveItem: a target is required"; + return; + } - if (!path.isLocalFile()) { - qWarning() << "ZShellIo::saveItem:" << path << "is not a local file"; - return; - } + if (!path.isLocalFile()) { + qWarning() << "ZShellIo::saveItem:" << path << "is not a local file"; + return; + } - if (!target->window()) { - qWarning() << "ZShellIo::saveItem: unable to save target" << target << "without a window"; - return; - } + if (!target->window()) { + qWarning() << "ZShellIo::saveItem: unable to save target" + << target + << "without a window"; + return; + } - auto scaledRect = rect; - const qreal scale = target->window()->devicePixelRatio(); - if (rect.isValid() && !qFuzzyCompare(scale + 1.0, 2.0)) { - scaledRect = - QRectF(rect.left() * scale, rect.top() * scale, rect.width() * scale, rect.height() * scale).toRect(); - } + auto scaledRect = rect; - const QSharedPointer grabResult = target->grabToImage(); + const qreal scale = target->window()->devicePixelRatio(); - QObject::connect(grabResult.data(), &QQuickItemGrabResult::ready, this, - [grabResult, scaledRect, path, onSaved, onFailed, this]() { - const auto future = QtConcurrent::run([=]() { - QImage image = grabResult->image(); + if (rect.isValid() && !qFuzzyCompare(scale + 1.0, 2.0)) { + scaledRect = QRectF( + rect.left() * scale, + rect.top() * scale, + rect.width() * scale, + rect.height() * scale + ).toRect(); + } - if (scaledRect.isValid()) { - image = image.copy(scaledRect); - } + const QSharedPointer grabResult = + target->grabToImage(); - const QString file = path.toLocalFile(); - const QString parent = QFileInfo(file).absolutePath(); - return QDir().mkpath(parent) && image.save(file); - }); + QObject::connect( + grabResult.data(), + &QQuickItemGrabResult::ready, + this, + [grabResult, scaledRect, path, onSaved, onFailed, this]() { - auto* watcher = new QFutureWatcher(this); - auto* engine = qmlEngine(this); + QImage image = grabResult->image(); - QObject::connect(watcher, &QFutureWatcher::finished, this, [=]() { - if (watcher->result()) { - if (onSaved.isCallable()) { - onSaved.call( - { QJSValue(path.toLocalFile()), engine->toScriptValue(QVariant::fromValue(path)) }); - } - } else { - qWarning() << "ZShellIo::saveItem: failed to save" << path; - if (onFailed.isCallable()) { - onFailed.call({ engine->toScriptValue(QVariant::fromValue(path)) }); - } - } - watcher->deleteLater(); - }); - watcher->setFuture(future); - }); + if (scaledRect.isValid()) { + image = image.copy(scaledRect); + } + + const auto future = QtConcurrent::run([image, path]() { + + const QString file = path.toLocalFile(); + const QString parent = QFileInfo(file).absolutePath(); + + return QDir().mkpath(parent) && image.save(file); + }); + + auto* watcher = new QFutureWatcher(this); + auto* engine = qmlEngine(this); + + QObject::connect( + watcher, + &QFutureWatcher::finished, + this, + [=]() { + + if (watcher->result()) { + + if (onSaved.isCallable()) { + onSaved.call({ + QJSValue(path.toLocalFile()), + engine->toScriptValue(QVariant::fromValue(path)) + }); + } + + } else { + + qWarning() << "ZShellIo::saveItem: failed to save" + << path; + + if (onFailed.isCallable()) { + onFailed.call({ + engine->toScriptValue(QVariant::fromValue(path)) + }); + } + } + + watcher->deleteLater(); + } + ); + + watcher->setFuture(future); + } + ); } +// ============================================================ +// saveImage +// ============================================================ + +void ZShellIo::saveImage(const QUrl& source, const QUrl& target) { + this->saveImage(source, target, QJSValue(), QJSValue()); +} + +void ZShellIo::saveImage(const QUrl& source, const QUrl& target, QJSValue onSaved) { + this->saveImage(source, target, onSaved, QJSValue()); +} + +void ZShellIo::saveImage( + const QUrl& source, + const QUrl& target, + QJSValue onSaved, + QJSValue onFailed + ) { + auto* engine = qmlEngine(this); + + const auto future = QtConcurrent::run([this, source, target]() { + return this->saveImageInternal(source, target); + }); + + auto* watcher = new QFutureWatcher(this); + + QObject::connect( + watcher, + &QFutureWatcher::finished, + this, + [=]() { + + if (watcher->result()) { + + if (onSaved.isCallable()) { + onSaved.call({ + QJSValue(target.toLocalFile()), + engine->toScriptValue(QVariant::fromValue(target)) + }); + } + + } else { + + qWarning() << "ZShellIo::saveImage: failed to save" + << source + << "to" + << target; + + if (onFailed.isCallable()) { + onFailed.call({ + engine->toScriptValue(QVariant::fromValue(target)) + }); + } + } + + watcher->deleteLater(); + } + ); + + watcher->setFuture(future); +} + +bool ZShellIo::saveImageInternal(const QUrl& source, const QUrl& target) const { + + if (!target.isLocalFile()) { + qWarning() << "ZShellIo::saveImage: target" + << target + << "is not a local file"; + return false; + } + + const QString targetFile = target.toLocalFile(); + + if (!QDir().mkpath(QFileInfo(targetFile).absolutePath())) { + return false; + } + + // ======================================================== + // Local file path + // ======================================================== + + if (source.isLocalFile()) { + + QFile::remove(targetFile); + + return QFile::copy( + source.toLocalFile(), + targetFile + ); + } + + // ======================================================== + // image:// provider path + // ======================================================== + + if (source.scheme() == "image") { + + auto* engine = qmlEngine(const_cast(this)); + + if (!engine) { + qWarning() << "ZShellIo::saveImage: no QQmlEngine"; + return false; + } + + const QString providerId = source.host(); + + const QString imageId = + source.path().startsWith('/') + ? source.path().mid(1) + : source.path(); + + auto* providerBase = + engine->imageProvider(providerId); + + if (!providerBase) { + qWarning() << "ZShellIo::saveImage: provider not found" + << providerId; + return false; + } + + auto* provider = + dynamic_cast(providerBase); + + if (!provider) { + qWarning() << "ZShellIo::saveImage: provider is not a QQuickImageProvider" + << providerId; + return false; + } + + if (!provider) { + qWarning() << "ZShellIo::saveImage: provider not found" + << providerId; + return false; + } + + QSize size; + QImage image; + + switch (provider->imageType()) { + + case QQuickImageProvider::Image: + image = provider->requestImage( + imageId, + &size, + QSize() + ); + break; + + case QQuickImageProvider::Pixmap: + image = provider->requestPixmap( + imageId, + &size, + QSize() + ).toImage(); + break; + + default: + qWarning() << "ZShellIo::saveImage: unsupported provider type"; + return false; + } + + if (image.isNull()) { + qWarning() << "ZShellIo::saveImage: provider returned null image"; + return false; + } + + return image.save(targetFile); + } + + qWarning() << "ZShellIo::saveImage: unsupported source" + << source; + + return false; +} + +// ============================================================ +// File ops +// ============================================================ + bool ZShellIo::copyFile(const QUrl& source, const QUrl& target, bool overwrite) const { - if (!source.isLocalFile()) { - qWarning() << "ZShellIo::copyFile: source" << source << "is not a local file"; - return false; - } - if (!target.isLocalFile()) { - qWarning() << "ZShellIo::copyFile: target" << target << "is not a local file"; - return false; - } - if (overwrite) { - if (!QFile::remove(target.toLocalFile())) { - qWarning() << "ZShellIo::copyFile: overwrite was specified but failed to remove" << target.toLocalFile(); - return false; - } - } + if (!source.isLocalFile()) { + qWarning() << "ZShellIo::copyFile: source" + << source + << "is not a local file"; + return false; + } - return QFile::copy(source.toLocalFile(), target.toLocalFile()); + if (!target.isLocalFile()) { + qWarning() << "ZShellIo::copyFile: target" + << target + << "is not a local file"; + return false; + } + + if (overwrite) { + QFile::remove(target.toLocalFile()); + } + + return QFile::copy( + source.toLocalFile(), + target.toLocalFile() + ); } bool ZShellIo::deleteFile(const QUrl& path) const { - if (!path.isLocalFile()) { - qWarning() << "ZShellIo::deleteFile: path" << path << "is not a local file"; - return false; - } - return QFile::remove(path.toLocalFile()); + if (!path.isLocalFile()) { + qWarning() << "ZShellIo::deleteFile: path" + << path + << "is not a local file"; + return false; + } + + return QFile::remove(path.toLocalFile()); } QString ZShellIo::toLocalFile(const QUrl& url) const { - if (!url.isLocalFile()) { - qWarning() << "ZShellIo::toLocalFile: given url is not a local file" << url; - return QString(); - } - return url.toLocalFile(); + if (!url.isLocalFile()) { + qWarning() << "ZShellIo::toLocalFile: given url is not a local file" + << url; + return QString(); + } + + return url.toLocalFile(); } } // namespace ZShell diff --git a/Plugins/ZShell/writefile.hpp b/Plugins/ZShell/writefile.hpp index 62b5e03..f518da9 100644 --- a/Plugins/ZShell/writefile.hpp +++ b/Plugins/ZShell/writefile.hpp @@ -1,31 +1,39 @@ #pragma once #include +#include #include #include +#include namespace ZShell { class ZShellIo : public QObject { - Q_OBJECT - QML_ELEMENT - QML_SINGLETON +Q_OBJECT +QML_ELEMENT +QML_SINGLETON public: - // clang-format off - Q_INVOKABLE void saveItem(QQuickItem* target, const QUrl& path); - Q_INVOKABLE void saveItem(QQuickItem* target, const QUrl& path, const QRect& rect); - Q_INVOKABLE void saveItem(QQuickItem* target, const QUrl& path, QJSValue onSaved); - Q_INVOKABLE void saveItem(QQuickItem* target, const QUrl& path, QJSValue onSaved, QJSValue onFailed); - Q_INVOKABLE void saveItem(QQuickItem* target, const QUrl& path, const QRect& rect, QJSValue onSaved); - Q_INVOKABLE void saveItem(QQuickItem* target, const QUrl& path, const QRect& rect, QJSValue onSaved, QJSValue onFailed); - // clang-format on +// clang-format off +Q_INVOKABLE void saveItem(QQuickItem* target, const QUrl& path); +Q_INVOKABLE void saveItem(QQuickItem* target, const QUrl& path, const QRect& rect); +Q_INVOKABLE void saveItem(QQuickItem* target, const QUrl& path, QJSValue onSaved); +Q_INVOKABLE void saveItem(QQuickItem* target, const QUrl& path, QJSValue onSaved, QJSValue onFailed); +Q_INVOKABLE void saveItem(QQuickItem* target, const QUrl& path, const QRect& rect, QJSValue onSaved); +Q_INVOKABLE void saveItem(QQuickItem* target, const QUrl& path, const QRect& rect, QJSValue onSaved, QJSValue onFailed); - Q_INVOKABLE bool copyFile(const QUrl& source, const QUrl& target, bool overwrite = true) const; - Q_INVOKABLE bool deleteFile(const QUrl& path) const; - Q_INVOKABLE QString toLocalFile(const QUrl& url) const; +Q_INVOKABLE void saveImage(const QUrl& source, const QUrl& target); +Q_INVOKABLE void saveImage(const QUrl& source, const QUrl& target, QJSValue onSaved); +Q_INVOKABLE void saveImage(const QUrl& source, const QUrl& target, QJSValue onSaved, QJSValue onFailed); +// clang-format on + +Q_INVOKABLE bool copyFile(const QUrl& source, const QUrl& target, bool overwrite = true) const; +Q_INVOKABLE bool deleteFile(const QUrl& path) const; +Q_INVOKABLE QString toLocalFile(const QUrl& url) const; + +private: +bool saveImageInternal(const QUrl& source, const QUrl& target) const; }; - } // namespace ZShell