Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qohossystemtrayicon.cpp
Go to the documentation of this file.
1// Copyright (C) 2025 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
6#include "qohosenums.h"
8#include "qohosjsutils.h"
11#include <QtCore/qdatetime.h>
12#include <QtCore/qobject.h>
13#include <QtGui/private/qhighdpiscaling_p.h>
14#include <QtGui/qguiapplication.h>
15#include <QtGui/qicon.h>
16#include <QtGui/qpa/qplatformmenu.h>
17#include <QtGui/qscreen.h>
18#include <algorithm>
19#include <cmath>
20#include <qohosplugincore.h>
21
22QT_BEGIN_NAMESPACE
23
24namespace {
25
26constexpr int quickOperationHeight = 300;
27
28const std::string ohosSystemTrayItemTitle = "Qt Application";
29
30const auto trayIconGeometry = QRect(0, 0, 24, 24);
31
32const auto notificationIconSize = QSize(128,128);
33
34std::string applyWorkaroundForEmptyHoverTips(const std::string &hoverTips)
35{
36 return !hoverTips.empty() ? hoverTips : " ";
37}
38
40{
41 return [](QtOhos::JsState &jsState) {
42 return QNapi::Array::New(jsState.env());
43 };
44}
45
47{
49 if (!qUiAbilityPeer)
50 qOhosReportFatalErrorAndAbort("%s: Default QAbilityPeer is not a QUiAbilityPeer", Q_FUNC_INFO);
51
52 return qUiAbilityPeer->qAbility().eval<QNapi::Object>("context");
53}
54
56 QtOhos::JsState &jsState, std::function<void()> leftClickListener)
57{
58 return QtOhos::registerOnOffMethodsBasedEventHandler(
59 jsState.eval<QNapi::Object>("@kit.StatusBarExtensionKit.statusBarManager"),
60 "statusBarIconClick",
61 [leftClickListener = std::move(leftClickListener)](const QtOhos::CallbackInfo &cbInfo) {
62 auto eventData = cbInfo.getFirstArg<QNapi::Object>(Q_FUNC_INFO);
63 auto optIconClickType = QNapi::getOptionalPropOrEmpty<QNapi::String>(
64 QNapi::getOptionalPropOrEmpty<QNapi::Object>(eventData, "data"),
65 "iconClickType");
66
67 if (optIconClickType.IsEmpty()) {
68 qOhosPrintfDebug(
69 "%s: no 'iconClickType' in the event (%s), ignoring it",
70 Q_FUNC_INFO, QNapi::toJsonString(eventData).c_str());
71 return;
72 }
73
74 if (optIconClickType.Utf8Value() == "leftClick")
75 leftClickListener();
76 });
77}
78
79void addItemToOhosStatusBar(QtOhos::JsState &jsState, QNapi::Object statusBarItem)
80{
81 jsState.eval(
82 "@kit.StatusBarExtensionKit.statusBarManager.addToStatusBar(*)",
83 {getContextForStatusBarManager(jsState), statusBarItem});
84
85 qOhosPrintfDebug("%s: Successfully added icon to status bar [Statusbar] ", Q_FUNC_INFO);
86}
87
89{
90 jsState.eval(
91 "@kit.StatusBarExtensionKit.statusBarManager.removeFromStatusBar(*)",
92 {getContextForStatusBarManager(jsState)});
93
94 qOhosPrintfDebug("%s: Successfully removed icon from status bar", Q_FUNC_INFO);
95}
96
97void updateOhosStatusBarIcon(QtOhos::JsState &jsState, QNapi::Object iconData)
98{
99 qOhosPrintfDebug("%s", Q_FUNC_INFO);
100
101 if (iconData.IsEmpty()) {
102 qOhosPrintfError("%s: Icon data is empty", Q_FUNC_INFO);
103 return;
104 }
105
106 jsState.eval(
107 "@kit.StatusBarExtensionKit.statusBarManager.updateStatusBarIcon(*)",
108 {getContextForStatusBarManager(jsState), iconData});
109
110 qOhosPrintfDebug("%s: Successfully updated status bar icon", Q_FUNC_INFO);
111}
112
113void updateOhosStatusBarMenu(QtOhos::JsState &jsState, QNapi::Array statusBarGroupMenus)
114{
115 jsState.eval(
116 "@kit.StatusBarExtensionKit.statusBarManager.updateStatusBarMenu(*)",
117 {getContextForStatusBarManager(jsState), statusBarGroupMenus});
118
119 qOhosPrintfDebug("%s: Successfully updated status bar menu", Q_FUNC_INFO);
120}
121
123 QtOhos::JsState &jsState, QNapi::Object statusBarIcon, const std::string &title,
124 QNapi::Array statusBarGroupMenus, QOhosOptional<std::string> hoverTips)
125{
126 auto *env = jsState.env();
127
128 auto quickOperation = QNapi::makeObject(
129 env,
130 {
131 {"title", title},
132 {"height", quickOperationHeight},
133 {"abilityName", ""},
134 });
135
136 auto statusBarItem = QNapi::makeObject(
137 env,
138 {
139 {"icons", statusBarIcon},
140 {"quickOperation", quickOperation},
141 {"statusBarGroupMenu", statusBarGroupMenus},
142 });
143
144 if (hoverTips.has_value())
145 statusBarItem.set("hoverTips", applyWorkaroundForEmptyHoverTips(hoverTips.value()));
146
147 return statusBarItem;
148}
149
151{
152 constexpr auto fallbackDisplayDensity = 1.0;
153
154 auto primaryDisplay = jsState.eval<QNapi::Object>("@ohos.display.getPrimaryDisplaySync()");
155 auto displayInfo = QOhosDisplayInfo::makeFromOhosDisplayObject(jsState, primaryDisplay);
156
157 double density;
158 if (displayInfo.densityPixels > 0.0) {
159 density = std::lround(displayInfo.densityPixels);
160 } else {
161 qOhosPrintfDebug("%s: invalid display densityPixels: %.3f", Q_FUNC_INFO, displayInfo.densityPixels);
162 density = 0.0;
163 }
164
165 return density > 0.0 ? density : fallbackDisplayDensity;
166}
167
169{
170 qOhosPrintfDebug(
171 "%s: image dimensions: %dx%d, format: %d, bytes: %lld",
172 Q_FUNC_INFO, image.width(), image.height(), static_cast<int>(image.format()), image.sizeInBytes());
173
174 const int sourceWidth = image.width();
175 const int sourceHeight = image.height();
176
177 double density = getPrimaryDisplayPixelDensity(jsState);
178
179 const double widthVp = std::lround(sourceWidth / density);
180 const double heightVp = std::lround(sourceHeight / density);
181
182 QImage newImage = image.scaled(widthVp, heightVp, Qt::KeepAspectRatio, Qt::SmoothTransformation);
183
184 const int width = newImage.width();
185 const int height = newImage.height();
186 const double finalWidthVp = width / density;
187 const double finalHeightVp = height / density;
188
189 qOhosPrintfDebug(
190 "%s: density=%.3f, vp %.2f x %.2f -> %.2f x %.2f, px %dx%d -> %dx%d",
191 Q_FUNC_INFO, density, widthVp, heightVp, finalWidthVp, finalHeightVp, sourceWidth, sourceHeight, width, height);
192
193 return createNapiPixelMapFromQImage(jsState, newImage);
194}
195
197 const QIcon &icon, const QSize &imageSize, const QColor &fallbackContentColor)
198{
199 auto iconImage = icon.pixmap(imageSize).toImage();
200 if (!iconImage.isNull())
201 return iconImage;
202
203 auto fallbackImage = QImage(imageSize, QImage::Format_RGBA8888);
204 fallbackImage.fill(fallbackContentColor);
205 return fallbackImage;
206}
207
208QImage convertToMonochromeIcon(const QImage &sourceImage, bool useWhite)
209{
210 QImage monochromeImage = sourceImage.copy();
211 const int colorValue = useWhite ? 255 : 0;
212
213 for (int y = 0; y < monochromeImage.height(); ++y) {
214 for (int x = 0; x < monochromeImage.width(); ++x) {
215 const QRgb pixel = monochromeImage.pixel(x, y);
216 const int alpha = qAlpha(pixel);
217
218 if (alpha > 0)
219 monochromeImage.setPixel(x, y, qRgba(colorValue, colorValue, colorValue, alpha));
220 }
221 }
222
223 return monochromeImage;
224}
225
226QNapi::Object makeJsPixelMapFromIcon(QtOhos::JsState &jsState, const QIcon &icon, bool isWhiteIcon)
227{
228 const QSize iconSize(48, 48);
229
230 QImage iconImage = icon.pixmap(iconSize).toImage();
231 if (iconImage.isNull()) {
232 QImage defaultImage(iconSize, QImage::Format_RGBA8888);
233 defaultImage.fill(isWhiteIcon ? Qt::white : Qt::black);
234 return makeDisplayDensityScaledJsPixelMapFromQImage(jsState, defaultImage);
235 }
236
237 qOhosPrintfDebug(
238 "%s: original Icon dimensions: %dx%d, format: %d, bytes: %lld",
239 Q_FUNC_INFO, iconImage.width(), iconImage.height(),
240 static_cast<int>(iconImage.format()), iconImage.sizeInBytes());
241
242 return makeDisplayDensityScaledJsPixelMapFromQImage(
243 jsState, convertToMonochromeIcon(iconImage, isWhiteIcon));
244}
245
246QNapi::Object makeJsStatusBarIcon(QtOhos::JsState &jsState, const QIcon &icon)
247{
248 auto *env = jsState.env();
249
250 auto whitePixelMap = makeJsPixelMapFromIcon(jsState, icon, true);
251 auto blackPixelMap = makeJsPixelMapFromIcon(jsState, icon, false);
252
253 auto imageSize = whitePixelMap.eval<QNapi::Object>("getImageInfoSync().size");;
254
255 int imageWidth = imageSize.get<QNapi::Number>("width");
256 int imageHeight = imageSize.get<QNapi::Number>("height");
257 qOhosPrintfDebug("%s: ceated PixelMap size: %dx%d", Q_FUNC_INFO, imageWidth, imageHeight);
258
259 return QNapi::makeObject(
260 env,
261 {
262 {"white", whitePixelMap},
263 {"black", blackPixelMap},
264 });
265}
266
268 QtOhos::JsState &jsState, const std::string &title, const std::string &text)
269{
270 using ContentType = QtOhos::enums::ohos::notificationManager::ContentType;
271
272 return QNapi::makeObject(
273 jsState.env(),
274 {
275 {"notificationContentType", jsState.mapOhosEnumToJs(ContentType::NOTIFICATION_CONTENT_BASIC_TEXT)},
276 {
277 "normal",
278 QNapi::makeObject(
279 jsState.env(),
280 {
281 {"title", title},
282 {"text", text},
283 }),
284 },
285 });
286}
287
289 QtOhos::JsState &jsState, const std::string &title, const std::string &content,
290 const QIcon &icon, QOhosOptional<int> optAutoDeletedDelayMs)
291{
292 auto iconPixelMap = makeDisplayDensityScaledJsPixelMapFromQImage(
293 jsState, convertIconToScaledImage(icon, notificationIconSize, Qt::transparent));
294 auto notificationRequest = QNapi::makeObject(
295 jsState.env(),
296 {
297 {"content", makeJsNotificationContent(jsState, title, content)},
298 {"smallIcon", iconPixelMap},
299 });
300 if (optAutoDeletedDelayMs.has_value()) {
301 notificationRequest.set(
302 "autoDeletedTime",
303 QDateTime::currentMSecsSinceEpoch() + optAutoDeletedDelayMs.value());
304 }
305
306 return notificationRequest;
307}
308
309class QOhosSystemTrayIcon final : public QPlatformSystemTrayIcon
310{
311public:
314
317 void updateIcon(const QIcon &icon) override;
318 void updateToolTip(const QString &tooltip) override;
319 void updateMenu(QPlatformMenu *menu) override;
320 QRect geometry() const override;
322 const QString &title, const QString &msg, const QIcon &icon,
323 MessageIcon iconType, int msecs) override;
324
327
328 QPlatformMenu *createMenu() const override;
329
330private:
331 bool m_initialized = false;
332 QIcon m_icon;
333 QOhosOptional<QString> m_optToolTip;
334 QPlatformMenu *m_menu = nullptr;
335
336 struct JsScopeData
337 {
338 std::shared_ptr<void> m_iconLeftClickListenerHandle;
339 };
340
341 std::shared_ptr<JsScopeData> m_jsScopeData;
342};
343
344QOhosSystemTrayIcon::QOhosSystemTrayIcon() = default;
345
346QOhosSystemTrayIcon::~QOhosSystemTrayIcon()
347{
348 cleanup();
349}
350
351void QOhosSystemTrayIcon::init()
352{
353 if (m_initialized)
354 return;
355
356 auto jsStatusBarGroupMenusFactory =
357 m_menu != nullptr
358 ? static_cast<QOhosStatusBarMenu *>(m_menu)->makeJsStatusBarGroupMenusFactory()
359 : makeEmptyJsArrayFactory();
360
361 m_jsScopeData = QtOhos::makeProxyWithJsThreadDeleter(std::make_shared<JsScopeData>());
362
363 auto selfRef = QtOhos::makeQThreadSafeRef(this);
365 [&](QtOhos::JsState &jsState) {
366 auto jsStatusBarIcon = makeJsStatusBarIcon(jsState, m_icon);
367 auto jsStatusBarGroupMenus = jsStatusBarGroupMenusFactory(jsState);
368 auto optHoverTipsString = qTransform(
369 m_optToolTip,
370 [](const auto &toolTip) {
371 return toolTip.toStdString();
372 });
373 auto jsStatusBarItem = makeJsStatusBarItem(
374 jsState, jsStatusBarIcon, ohosSystemTrayItemTitle, jsStatusBarGroupMenus, optHoverTipsString);
375 addItemToOhosStatusBar(jsState, jsStatusBarItem);
376 m_jsScopeData->m_iconLeftClickListenerHandle = registerOhosIconLeftClickListener(
377 jsState,
378 [selfRef]() {
379 selfRef.visitInQtThreadIfAlive(
380 [](auto &self) {
381 Q_EMIT self.activated(QPlatformSystemTrayIcon::Trigger);
382 });
383 });
384 },
385 Q_FUNC_INFO);
386
387 m_initialized = true;
388
389 qOhosPrintfDebug("%s: System tray icon initialized", Q_FUNC_INFO);
390}
391
392void QOhosSystemTrayIcon::cleanup()
393{
394 if (!m_initialized)
395 return;
396
398
399 m_jsScopeData.reset();
400
401 m_initialized = false;
402}
403
404void QOhosSystemTrayIcon::updateIcon(const QIcon &icon)
405{
406 if (!m_initialized)
407 return;
408
409 m_icon = icon;
410
412 [&](QtOhos::JsState &jsState) {
413 updateOhosStatusBarIcon(jsState, makeJsStatusBarIcon(jsState, icon));
414 },
415 Q_FUNC_INFO);
416}
417
418void QOhosSystemTrayIcon::updateToolTip(const QString &tooltip)
419{
420 m_optToolTip = tooltip;
421
422 if (m_initialized) {
424 [&](QtOhos::JsState &jsState, QOhosTaskPromise<> taskPromise) {
425 jsState.evalToPromiseOrRejectOnThrow(
426 "@kit.StatusBarExtensionKit.statusBarManager.updateStatusBarHoverTips(*)",
427 {getContextForStatusBarManager(jsState), applyWorkaroundForEmptyHoverTips(tooltip.toStdString())})
428 .onCatch(QtOhos::makeErrorLoggingJsCallback("updateStatusBarHoverTips()"))
429 .onFinally(std::move(taskPromise).makeChained(Q_FUNC_INFO));
430 },
431 Q_FUNC_INFO);
432 }
433}
434
435QRect QOhosSystemTrayIcon::geometry() const
436{
437 return trayIconGeometry;
438}
439
440void QOhosSystemTrayIcon::showMessage(
441 const QString &title, const QString &msg, const QIcon &icon, MessageIcon iconType, int msecs)
442{
443 Q_UNUSED(iconType);
445 [&](QtOhos::JsState &jsState, QOhosTaskPromise<> taskPromise) {
446 auto notificationRequest = makeJsNotificationRequest(
447 jsState, title.toStdString(), msg.toStdString(), icon,
448 msecs > 0 ? makeQOhosOptional(msecs) : makeEmptyQOhosOptional());
449 jsState.evalToPromiseOrRejectOnThrow("@ohos.notificationManager.publish(*)", {notificationRequest})
450 .onCatch(QtOhos::makeErrorLoggingJsCallback("publish()"))
451 .onFinally(std::move(taskPromise).makeChained(Q_FUNC_INFO));
452 },
453 Q_FUNC_INFO);
454}
455
456bool QOhosSystemTrayIcon::isSystemTrayAvailable() const
457{
458 return true;
459}
460
461bool QOhosSystemTrayIcon::supportsMessages() const
462{
463 return true;
464}
465
466QPlatformMenu *QOhosSystemTrayIcon::createMenu() const
467{
468 if (m_menu == nullptr)
469 return makeQOhosStatusBarMenu().release();
470 return m_menu;
471}
472
473void QOhosSystemTrayIcon::updateMenu(QPlatformMenu *menu)
474{
475 qOhosPrintfDebug("%s: Updating status bar menu", Q_FUNC_INFO);
476
477 m_menu = menu;
478
479 if (!m_initialized) {
480 qOhosPrintfDebug("%s: System tray not initialized yet", Q_FUNC_INFO);
481 return;
482 }
483
484 auto jsStatusBarGroupMenusFactory =
485 m_menu != nullptr
486 ? static_cast<QOhosStatusBarMenu *>(m_menu)->makeJsStatusBarGroupMenusFactory()
487 : makeEmptyJsArrayFactory();
488
490 [&](QtOhos::JsState &jsState) {
491 updateOhosStatusBarMenu(jsState, jsStatusBarGroupMenusFactory(jsState));
492 },
493 Q_FUNC_INFO);
494}
495
496}
497
499{
500 return std::make_unique<QOhosSystemTrayIcon>();
501}
502
503QT_END_NAMESPACE
void init() override
This method is called to initialize the platform dependent implementation.
void cleanup() override
This method is called to cleanup the platform dependent implementation.
void showMessage(const QString &title, const QString &msg, const QIcon &icon, MessageIcon iconType, int msecs) override
Shows a balloon message for the entry with the given title, message msg and icon for the time specifi...
void updateMenu(QPlatformMenu *menu) override
This method is called when the system tray menu did change.
QPlatformMenu * createMenu() const override
This method allows platforms to use a different QPlatformMenu for system tray menus than what would n...
QRect geometry() const override
This method returns the geometry of the platform dependent system tray icon on the screen.
bool supportsMessages() const override
Returns true if the system tray supports messages on the platform.
bool isSystemTrayAvailable() const override
Returns true if the system tray is available on the platform.
void updateToolTip(const QString &tooltip) override
This method is called when the tooltip text did change.
void updateIcon(const QIcon &icon) override
This method is called when the icon did change.
virtual std::shared_ptr< QAbilityPeer > defaultQAbilityPeer()=0
static std::shared_ptr< QUiAbilityPeer > tryCastFromQAbilityPeerOrNull(std::shared_ptr< QAbilityPeer > qAbilityPeer)
QImage convertToMonochromeIcon(const QImage &sourceImage, bool useWhite)
QNapi::Object makeJsStatusBarIcon(QtOhos::JsState &jsState, const QIcon &icon)
QImage convertIconToScaledImage(const QIcon &icon, const QSize &imageSize, const QColor &fallbackContentColor)
QNapi::Object makeJsStatusBarItem(QtOhos::JsState &jsState, QNapi::Object statusBarIcon, const std::string &title, QNapi::Array statusBarGroupMenus, QOhosOptional< std::string > hoverTips)
void addItemToOhosStatusBar(QtOhos::JsState &jsState, QNapi::Object statusBarItem)
QNapi::Object getContextForStatusBarManager(QtOhos::JsState &jsState)
QNapi::Object makeDisplayDensityScaledJsPixelMapFromQImage(QtOhos::JsState &jsState, const QImage &image)
QNapi::Object makeJsNotificationContent(QtOhos::JsState &jsState, const std::string &title, const std::string &text)
std::string applyWorkaroundForEmptyHoverTips(const std::string &hoverTips)
std::function< QNapi::Array(QtOhos::JsState &)> makeEmptyJsArrayFactory()
void removeIconFromOhosStatusBar(QtOhos::JsState &jsState)
QNapi::Object makeJsNotificationRequest(QtOhos::JsState &jsState, const std::string &title, const std::string &content, const QIcon &icon, QOhosOptional< int > optAutoDeletedDelayMs)
void updateOhosStatusBarIcon(QtOhos::JsState &jsState, QNapi::Object iconData)
constexpr int quickOperationHeight
std::shared_ptr< void > registerOhosIconLeftClickListener(QtOhos::JsState &jsState, std::function< void()> leftClickListener)
const std::string ohosSystemTrayItemTitle
QNapi::Object makeJsPixelMapFromIcon(QtOhos::JsState &jsState, const QIcon &icon, bool isWhiteIcon)
double getPrimaryDisplayPixelDensity(QtOhos::JsState &jsState)
void updateOhosStatusBarMenu(QtOhos::JsState &jsState, QNapi::Array statusBarGroupMenus)
void runInJsThreadAndWait(const std::function< void(JsState &)> &task, std::string callerContextName={})
void invokeInJsThreadAndWaitForContinue(std::function< void(JsState &, QOhosTaskPromise<>)> &&task, std::string callerContextName={})
QT_BEGIN_NAMESPACE std::unique_ptr< QPlatformSystemTrayIcon > makeQOhosSystemTrayIcon()