2022-03-23 15:59:53 +00:00
|
|
|
/*
|
|
|
|
This file is part of Telegram Desktop,
|
|
|
|
the official desktop application for the Telegram messaging service.
|
|
|
|
|
|
|
|
For license and copyright information please follow this link:
|
|
|
|
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|
|
|
*/
|
|
|
|
#include "platform/linux/tray_linux.h"
|
|
|
|
|
2022-04-20 22:08:53 +00:00
|
|
|
#include "base/invoke_queued.h"
|
|
|
|
#include "base/qt_signal_producer.h"
|
|
|
|
#include "core/application.h"
|
|
|
|
#include "core/sandbox.h"
|
2022-09-19 02:58:27 +00:00
|
|
|
#include "platform/platform_specific.h"
|
2022-04-20 22:08:53 +00:00
|
|
|
#include "ui/ui_utility.h"
|
|
|
|
#include "ui/widgets/popup_menu.h"
|
|
|
|
#include "window/window_controller.h"
|
|
|
|
#include "styles/style_window.h"
|
|
|
|
|
|
|
|
#include <QtCore/QCoreApplication>
|
|
|
|
#include <QtWidgets/QMenu>
|
|
|
|
#include <QtWidgets/QSystemTrayIcon>
|
|
|
|
|
2022-03-23 15:59:53 +00:00
|
|
|
namespace Platform {
|
|
|
|
|
2022-04-21 14:24:27 +00:00
|
|
|
class IconGraphic final {
|
|
|
|
public:
|
|
|
|
explicit IconGraphic();
|
|
|
|
~IconGraphic();
|
|
|
|
|
|
|
|
[[nodiscard]] bool isRefreshNeeded(
|
2022-12-09 10:50:20 +00:00
|
|
|
const QIcon &systemIcon,
|
|
|
|
const QString &iconThemeName,
|
2022-04-21 14:24:27 +00:00
|
|
|
int counter,
|
2022-12-09 10:50:20 +00:00
|
|
|
bool muted) const;
|
|
|
|
[[nodiscard]] QIcon systemIcon(
|
|
|
|
const QString &iconThemeName,
|
|
|
|
int counter,
|
|
|
|
bool muted) const;
|
|
|
|
[[nodiscard]] QIcon trayIcon(
|
|
|
|
const QIcon &systemIcon,
|
|
|
|
const QString &iconThemeName,
|
|
|
|
int counter,
|
|
|
|
bool muted);
|
2022-04-21 14:24:27 +00:00
|
|
|
|
|
|
|
private:
|
|
|
|
[[nodiscard]] QString panelIconName(int counter, bool muted) const;
|
|
|
|
[[nodiscard]] int counterSlice(int counter) const;
|
|
|
|
void updateIconRegenerationNeeded(
|
|
|
|
const QIcon &icon,
|
2022-12-09 10:50:20 +00:00
|
|
|
const QIcon &systemIcon,
|
|
|
|
const QString &iconThemeName,
|
2022-04-21 14:24:27 +00:00
|
|
|
int counter,
|
2022-12-09 10:50:20 +00:00
|
|
|
bool muted);
|
2022-04-21 14:24:27 +00:00
|
|
|
[[nodiscard]] QSize dprSize(const QImage &image) const;
|
|
|
|
|
|
|
|
const QString _panelTrayIconName;
|
|
|
|
const QString _mutePanelTrayIconName;
|
|
|
|
const QString _attentionPanelTrayIconName;
|
|
|
|
|
2022-12-06 08:55:09 +00:00
|
|
|
const int _iconSizes[5];
|
2022-04-20 22:08:53 +00:00
|
|
|
|
2022-04-21 14:24:27 +00:00
|
|
|
bool _muted = true;
|
|
|
|
int32 _count = 0;
|
|
|
|
base::flat_map<int, QImage> _imageBack;
|
|
|
|
QIcon _trayIcon;
|
2022-12-09 10:50:20 +00:00
|
|
|
QIcon _systemIcon;
|
2022-04-21 14:24:27 +00:00
|
|
|
QString _themeName;
|
|
|
|
|
|
|
|
};
|
2022-04-20 22:08:53 +00:00
|
|
|
|
2022-04-21 14:24:27 +00:00
|
|
|
IconGraphic::IconGraphic()
|
|
|
|
: _panelTrayIconName("telegram-panel")
|
|
|
|
, _mutePanelTrayIconName("telegram-mute-panel")
|
|
|
|
, _attentionPanelTrayIconName("telegram-attention-panel")
|
2022-12-06 08:55:09 +00:00
|
|
|
, _iconSizes{ 16, 22, 24, 32, 48 } {
|
2022-04-21 14:24:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
IconGraphic::~IconGraphic() = default;
|
|
|
|
|
|
|
|
QString IconGraphic::panelIconName(int counter, bool muted) const {
|
2022-04-20 22:08:53 +00:00
|
|
|
return (counter > 0)
|
|
|
|
? (muted
|
2022-04-21 14:24:27 +00:00
|
|
|
? _mutePanelTrayIconName
|
|
|
|
: _attentionPanelTrayIconName)
|
|
|
|
: _panelTrayIconName;
|
2022-04-20 22:08:53 +00:00
|
|
|
}
|
|
|
|
|
2022-12-09 10:50:20 +00:00
|
|
|
QIcon IconGraphic::systemIcon(
|
|
|
|
const QString &iconThemeName,
|
|
|
|
int counter,
|
|
|
|
bool muted) const {
|
|
|
|
if (iconThemeName == _themeName
|
2023-03-08 23:06:11 +00:00
|
|
|
&& (counter > 0) == (_count > 0)
|
2022-12-09 10:50:20 +00:00
|
|
|
&& muted == _muted) {
|
|
|
|
return _systemIcon;
|
|
|
|
}
|
|
|
|
|
|
|
|
const auto candidates = {
|
|
|
|
panelIconName(counter, muted),
|
|
|
|
base::IconName(),
|
|
|
|
};
|
2022-04-20 22:08:53 +00:00
|
|
|
|
2022-12-16 05:00:00 +00:00
|
|
|
for (const auto &candidate : candidates) {
|
2022-12-09 10:50:20 +00:00
|
|
|
const auto icon = QIcon::fromTheme(candidate);
|
|
|
|
if (icon.name() == candidate) {
|
|
|
|
return icon;
|
|
|
|
}
|
2022-04-20 22:08:53 +00:00
|
|
|
}
|
|
|
|
|
2022-12-09 10:50:20 +00:00
|
|
|
return QIcon();
|
2022-04-20 22:08:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2022-04-21 14:24:27 +00:00
|
|
|
int IconGraphic::counterSlice(int counter) const {
|
2022-04-20 22:08:53 +00:00
|
|
|
return (counter >= 1000)
|
|
|
|
? (1000 + (counter % 100))
|
|
|
|
: counter;
|
|
|
|
}
|
|
|
|
|
2022-04-21 14:24:27 +00:00
|
|
|
bool IconGraphic::isRefreshNeeded(
|
2022-12-09 10:50:20 +00:00
|
|
|
const QIcon &systemIcon,
|
|
|
|
const QString &iconThemeName,
|
2022-04-20 22:08:53 +00:00
|
|
|
int counter,
|
2022-12-09 10:50:20 +00:00
|
|
|
bool muted) const {
|
2022-04-21 14:24:27 +00:00
|
|
|
return _trayIcon.isNull()
|
|
|
|
|| iconThemeName != _themeName
|
2022-12-09 10:50:20 +00:00
|
|
|
|| systemIcon.name() != _systemIcon.name()
|
2022-04-21 14:24:27 +00:00
|
|
|
|| muted != _muted
|
|
|
|
|| counterSlice(counter) != _count;
|
2022-04-20 22:08:53 +00:00
|
|
|
}
|
|
|
|
|
2022-04-21 14:24:27 +00:00
|
|
|
void IconGraphic::updateIconRegenerationNeeded(
|
2022-04-20 22:08:53 +00:00
|
|
|
const QIcon &icon,
|
2022-12-09 10:50:20 +00:00
|
|
|
const QIcon &systemIcon,
|
|
|
|
const QString &iconThemeName,
|
2022-04-20 22:08:53 +00:00
|
|
|
int counter,
|
2022-12-09 10:50:20 +00:00
|
|
|
bool muted) {
|
2022-04-21 14:24:27 +00:00
|
|
|
_trayIcon = icon;
|
2022-12-09 10:50:20 +00:00
|
|
|
_systemIcon = systemIcon;
|
2022-04-21 14:24:27 +00:00
|
|
|
_themeName = iconThemeName;
|
2022-12-09 10:50:20 +00:00
|
|
|
_count = counterSlice(counter);
|
|
|
|
_muted = muted;
|
2022-04-21 14:24:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
QSize IconGraphic::dprSize(const QImage &image) const {
|
|
|
|
return image.size() / image.devicePixelRatio();
|
2022-04-20 22:08:53 +00:00
|
|
|
}
|
|
|
|
|
2022-12-09 10:50:20 +00:00
|
|
|
QIcon IconGraphic::trayIcon(
|
|
|
|
const QIcon &systemIcon,
|
|
|
|
const QString &iconThemeName,
|
|
|
|
int counter,
|
|
|
|
bool muted) {
|
|
|
|
if (!isRefreshNeeded(systemIcon, iconThemeName, counter, muted)) {
|
2022-04-21 14:24:27 +00:00
|
|
|
return _trayIcon;
|
2022-04-20 22:08:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2022-12-09 10:50:20 +00:00
|
|
|
if (systemIcon.name() == panelIconName(counter, muted)) {
|
|
|
|
updateIconRegenerationNeeded(
|
|
|
|
systemIcon,
|
|
|
|
systemIcon,
|
|
|
|
iconThemeName,
|
|
|
|
counter,
|
|
|
|
muted);
|
|
|
|
|
|
|
|
return systemIcon;
|
2022-04-20 22:08:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
QIcon result;
|
|
|
|
|
2022-04-21 14:24:27 +00:00
|
|
|
for (const auto iconSize : _iconSizes) {
|
|
|
|
auto ¤tImageBack = _imageBack[iconSize];
|
2022-04-20 22:08:53 +00:00
|
|
|
const auto desiredSize = QSize(iconSize, iconSize);
|
|
|
|
|
|
|
|
if (currentImageBack.isNull()
|
2022-04-21 14:24:27 +00:00
|
|
|
|| iconThemeName != _themeName
|
2022-12-09 10:50:20 +00:00
|
|
|
|| systemIcon.name() != _systemIcon.name()) {
|
|
|
|
if (!systemIcon.isNull()) {
|
2022-04-20 22:08:53 +00:00
|
|
|
// We can't use QIcon::actualSize here
|
|
|
|
// since it works incorrectly with svg icon themes
|
|
|
|
currentImageBack = systemIcon
|
|
|
|
.pixmap(desiredSize)
|
|
|
|
.toImage();
|
|
|
|
|
|
|
|
const auto firstAttemptSize = dprSize(currentImageBack);
|
|
|
|
|
|
|
|
// if current icon theme is not a svg one, Qt can return
|
|
|
|
// a pixmap that less in size even if there are a bigger one
|
|
|
|
if (firstAttemptSize.width() < desiredSize.width()) {
|
|
|
|
const auto availableSizes = systemIcon.availableSizes();
|
|
|
|
|
|
|
|
const auto biggestSize = ranges::max_element(
|
|
|
|
availableSizes,
|
|
|
|
std::less<>(),
|
|
|
|
&QSize::width);
|
|
|
|
|
|
|
|
if (biggestSize->width() > firstAttemptSize.width()) {
|
|
|
|
currentImageBack = systemIcon
|
|
|
|
.pixmap(*biggestSize)
|
|
|
|
.toImage();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
currentImageBack = Window::Logo();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (dprSize(currentImageBack) != desiredSize) {
|
|
|
|
currentImageBack = currentImageBack.scaled(
|
|
|
|
desiredSize * currentImageBack.devicePixelRatio(),
|
|
|
|
Qt::IgnoreAspectRatio,
|
|
|
|
Qt::SmoothTransformation);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
auto iconImage = currentImageBack;
|
|
|
|
|
|
|
|
if (counter > 0) {
|
|
|
|
const auto &bg = muted
|
|
|
|
? st::trayCounterBgMute
|
|
|
|
: st::trayCounterBg;
|
|
|
|
const auto &fg = st::trayCounterFg;
|
|
|
|
if (iconSize >= 22) {
|
2022-12-09 10:50:44 +00:00
|
|
|
const auto imageSize = dprSize(iconImage);
|
2022-12-06 08:55:09 +00:00
|
|
|
const auto layerSize = (iconSize >= 48)
|
|
|
|
? 32
|
|
|
|
: (iconSize >= 36)
|
|
|
|
? 24
|
|
|
|
: (iconSize >= 32)
|
|
|
|
? 20
|
|
|
|
: 16;
|
2022-04-20 22:08:53 +00:00
|
|
|
const auto layer = Window::GenerateCounterLayer({
|
|
|
|
.size = layerSize,
|
2023-04-04 05:14:39 +00:00
|
|
|
.devicePixelRatio = iconImage.devicePixelRatio(),
|
2022-04-20 22:08:53 +00:00
|
|
|
.count = counter,
|
|
|
|
.bg = bg,
|
|
|
|
.fg = fg,
|
|
|
|
});
|
|
|
|
|
|
|
|
QPainter p(&iconImage);
|
|
|
|
p.drawImage(
|
2022-12-09 10:50:44 +00:00
|
|
|
imageSize.width() - layer.width() - 1,
|
|
|
|
imageSize.height() - layer.height() - 1,
|
2022-04-20 22:08:53 +00:00
|
|
|
layer);
|
|
|
|
} else {
|
|
|
|
iconImage = Window::WithSmallCounter(std::move(iconImage), {
|
|
|
|
.size = 16,
|
|
|
|
.count = counter,
|
|
|
|
.bg = bg,
|
|
|
|
.fg = fg,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
result.addPixmap(Ui::PixmapFromImage(std::move(iconImage)));
|
|
|
|
}
|
|
|
|
|
2022-12-09 10:50:20 +00:00
|
|
|
updateIconRegenerationNeeded(
|
|
|
|
result,
|
|
|
|
systemIcon,
|
|
|
|
iconThemeName,
|
|
|
|
counter,
|
|
|
|
muted);
|
2022-04-20 22:08:53 +00:00
|
|
|
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
class TrayEventFilter final : public QObject {
|
|
|
|
public:
|
|
|
|
TrayEventFilter(not_null<QObject*> parent);
|
|
|
|
|
|
|
|
[[nodiscard]] rpl::producer<> contextMenuFilters() const;
|
|
|
|
|
|
|
|
protected:
|
|
|
|
bool eventFilter(QObject *watched, QEvent *event) override;
|
|
|
|
|
|
|
|
private:
|
|
|
|
const QString _iconObjectName;
|
|
|
|
rpl::event_stream<> _contextMenuFilters;
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
TrayEventFilter::TrayEventFilter(not_null<QObject*> parent)
|
|
|
|
: QObject(parent)
|
|
|
|
, _iconObjectName("QSystemTrayIconSys") {
|
|
|
|
parent->installEventFilter(this);
|
|
|
|
}
|
|
|
|
|
|
|
|
bool TrayEventFilter::eventFilter(QObject *obj, QEvent *event) {
|
|
|
|
if (event->type() == QEvent::MouseButtonPress
|
|
|
|
&& obj->objectName() == _iconObjectName) {
|
|
|
|
const auto m = static_cast<QMouseEvent*>(event);
|
|
|
|
if (m->button() == Qt::RightButton) {
|
|
|
|
Core::Sandbox::Instance().customEnterFromEventLoop([&] {
|
|
|
|
_contextMenuFilters.fire({});
|
|
|
|
});
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
rpl::producer<> TrayEventFilter::contextMenuFilters() const {
|
|
|
|
return _contextMenuFilters.events();
|
|
|
|
}
|
|
|
|
|
2022-03-23 15:59:53 +00:00
|
|
|
Tray::Tray() {
|
2022-05-24 06:25:38 +00:00
|
|
|
LOG(("System tray available: %1").arg(Logs::b(TrayIconSupported())));
|
2022-03-23 15:59:53 +00:00
|
|
|
}
|
|
|
|
|
2022-04-20 22:08:53 +00:00
|
|
|
void Tray::createIcon() {
|
|
|
|
if (!_icon) {
|
2022-04-21 14:24:27 +00:00
|
|
|
if (!_iconGraphic) {
|
|
|
|
_iconGraphic = std::make_unique<IconGraphic>();
|
|
|
|
}
|
|
|
|
|
2022-04-20 22:08:53 +00:00
|
|
|
const auto showXEmbed = [=] {
|
|
|
|
_aboutToShowRequests.fire({});
|
|
|
|
InvokeQueued(_menuXEmbed.get(), [=] {
|
|
|
|
_menuXEmbed->popup(QCursor::pos());
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2022-12-09 10:50:20 +00:00
|
|
|
const auto iconThemeName = QIcon::themeName();
|
|
|
|
const auto counter = Core::App().unreadBadge();
|
|
|
|
const auto muted = Core::App().unreadBadgeMuted();
|
|
|
|
|
2023-01-18 08:11:18 +00:00
|
|
|
_icon = base::make_unique_q<QSystemTrayIcon>(nullptr);
|
2022-04-21 14:24:27 +00:00
|
|
|
_icon->setIcon(_iconGraphic->trayIcon(
|
2022-12-09 10:50:20 +00:00
|
|
|
_iconGraphic->systemIcon(
|
|
|
|
iconThemeName,
|
|
|
|
counter,
|
|
|
|
muted),
|
|
|
|
iconThemeName,
|
|
|
|
counter,
|
|
|
|
muted));
|
2022-04-20 22:08:53 +00:00
|
|
|
_icon->setToolTip(AppName.utf16());
|
|
|
|
|
|
|
|
using Reason = QSystemTrayIcon::ActivationReason;
|
|
|
|
base::qt_signal_producer(
|
|
|
|
_icon.get(),
|
|
|
|
&QSystemTrayIcon::activated
|
|
|
|
) | rpl::start_with_next([=](Reason reason) {
|
|
|
|
if (reason == QSystemTrayIcon::Context) {
|
|
|
|
showXEmbed();
|
|
|
|
} else {
|
|
|
|
_iconClicks.fire({});
|
|
|
|
}
|
|
|
|
}, _lifetime);
|
|
|
|
|
|
|
|
_icon->setContextMenu(_menu.get());
|
|
|
|
|
|
|
|
if (!_eventFilter) {
|
|
|
|
_eventFilter = base::make_unique_q<TrayEventFilter>(
|
|
|
|
QCoreApplication::instance());
|
|
|
|
_eventFilter->contextMenuFilters(
|
|
|
|
) | rpl::start_with_next([=] {
|
|
|
|
showXEmbed();
|
|
|
|
}, _lifetime);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
updateIcon();
|
|
|
|
|
|
|
|
_icon->show();
|
|
|
|
}
|
|
|
|
|
|
|
|
void Tray::destroyIcon() {
|
|
|
|
_icon = nullptr;
|
|
|
|
}
|
|
|
|
|
|
|
|
void Tray::updateIcon() {
|
2022-04-21 14:24:27 +00:00
|
|
|
if (!_icon || !_iconGraphic) {
|
2022-04-20 22:08:53 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
const auto counter = Core::App().unreadBadge();
|
|
|
|
const auto muted = Core::App().unreadBadgeMuted();
|
2022-12-09 10:50:20 +00:00
|
|
|
const auto iconThemeName = QIcon::themeName();
|
|
|
|
const auto systemIcon = _iconGraphic->systemIcon(
|
|
|
|
iconThemeName,
|
|
|
|
counter,
|
|
|
|
muted);
|
|
|
|
|
|
|
|
if (_iconGraphic->isRefreshNeeded(
|
|
|
|
systemIcon,
|
|
|
|
iconThemeName,
|
|
|
|
counter,
|
|
|
|
muted)) {
|
|
|
|
_icon->setIcon(_iconGraphic->trayIcon(
|
|
|
|
systemIcon,
|
|
|
|
iconThemeName,
|
|
|
|
counter,
|
|
|
|
muted));
|
2022-04-20 22:08:53 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void Tray::createMenu() {
|
|
|
|
if (!_menu) {
|
2023-01-18 08:11:18 +00:00
|
|
|
_menu = base::make_unique_q<QMenu>(nullptr);
|
2022-04-20 22:08:53 +00:00
|
|
|
}
|
|
|
|
if (!_menuXEmbed) {
|
|
|
|
_menuXEmbed = base::make_unique_q<Ui::PopupMenu>(nullptr);
|
|
|
|
_menuXEmbed->deleteOnHide(false);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void Tray::destroyMenu() {
|
|
|
|
_menuXEmbed = nullptr;
|
|
|
|
if (_menu) {
|
|
|
|
_menu->clear();
|
|
|
|
}
|
|
|
|
_actionsLifetime.destroy();
|
|
|
|
}
|
|
|
|
|
|
|
|
void Tray::addAction(rpl::producer<QString> text, Fn<void()> &&callback) {
|
|
|
|
if (_menuXEmbed) {
|
|
|
|
const auto XEAction = _menuXEmbed->addAction(QString(), callback);
|
|
|
|
rpl::duplicate(
|
|
|
|
text
|
|
|
|
) | rpl::start_with_next([=](const QString &text) {
|
|
|
|
XEAction->setText(text);
|
|
|
|
}, _actionsLifetime);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (_menu) {
|
|
|
|
const auto action = _menu->addAction(QString(), std::move(callback));
|
|
|
|
std::move(
|
|
|
|
text
|
|
|
|
) | rpl::start_with_next([=](const QString &text) {
|
|
|
|
action->setText(text);
|
|
|
|
}, _actionsLifetime);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void Tray::showTrayMessage() const {
|
|
|
|
}
|
|
|
|
|
|
|
|
bool Tray::hasTrayMessageSupport() const {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
rpl::producer<> Tray::aboutToShowRequests() const {
|
|
|
|
return rpl::merge(
|
|
|
|
_aboutToShowRequests.events(),
|
|
|
|
_menu
|
|
|
|
? base::qt_signal_producer(_menu.get(), &QMenu::aboutToShow)
|
|
|
|
: rpl::never<>() | rpl::type_erased());
|
|
|
|
}
|
|
|
|
|
|
|
|
rpl::producer<> Tray::showFromTrayRequests() const {
|
|
|
|
return rpl::never<>();
|
|
|
|
}
|
|
|
|
|
|
|
|
rpl::producer<> Tray::hideToTrayRequests() const {
|
|
|
|
return rpl::never<>();
|
|
|
|
}
|
|
|
|
|
|
|
|
rpl::producer<> Tray::iconClicks() const {
|
|
|
|
return _iconClicks.events();
|
|
|
|
}
|
|
|
|
|
2022-06-23 05:29:11 +00:00
|
|
|
bool Tray::hasIcon() const {
|
|
|
|
return _icon;
|
|
|
|
}
|
|
|
|
|
2022-04-20 22:08:53 +00:00
|
|
|
rpl::lifetime &Tray::lifetime() {
|
|
|
|
return _lifetime;
|
|
|
|
}
|
|
|
|
|
2022-04-21 14:24:27 +00:00
|
|
|
Tray::~Tray() = default;
|
|
|
|
|
2022-03-23 15:59:53 +00:00
|
|
|
} // namespace Platform
|