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
5#include "qohosjsutils.h"
9#include <QtCore/private/qcore_ohos_p.h>
10#include <QtCore/qdatetime.h>
11#include <QtCore/qobject.h>
12#include <QtGui/private/qhighdpiscaling_p.h>
13#include <QtGui/private/qohosimageconversions_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 <chrono>
20#include <cstdint>
21#include <optional>
22#include <qohosjsscopedataowner.h>
23
25
26namespace {
27
28constexpr int quickOperationHeight = 300;
29
30const std::string ohosSystemTrayItemTitle = "Qt Application";
31
32const auto trayIconGeometry = QRect(0, 0, 24, 24);
33
34const auto notificationIconSize = QSize(128,128);
35
36QNapi::Promise makeRejectedPromise(napi_env env, QNapi::Value rejectValue)
37{
38 auto deferred = Napi::Promise::Deferred::New(env);
39 deferred.Reject(rejectValue);
40 return deferred.Promise();
41}
42
43QNapi::Promise makePromiseResolvedAfter(QOhosJsState &jsState, std::chrono::milliseconds delay)
44{
45 auto deferred = Napi::Promise::Deferred::New(jsState.env());
47 jsState,
48 [deferred](const QOhosCallbackInfo &cbInfo) {
49 deferred.Resolve(cbInfo.Env().Undefined());
50 },
51 delay);
52
53 return deferred.Promise();
54}
55
57 QOhosJsState &jsState, const std::function<QNapi::Promise(QOhosJsState &)> &func)
58{
59 try {
60 return func(jsState);
61 } catch (const Napi::Error &error) {
62 return makeRejectedPromise(jsState.env(), error.Value());
63 }
64}
65
67 QOhosJsState &jsState, std::function<QNapi::Promise(QOhosJsState &)> call)
68{
69 auto callAttemptPromise = callToPromiseOrRejectOnThrow(jsState, call);
70
71 return callAttemptPromise.onCatch(
72 [call = std::move(call)](const QOhosCallbackInfo &cbInfo) mutable {
73 auto rejectedValue = cbInfo.getFirstArg<QNapi::Value>(Q_FUNC_INFO);
74
75 constexpr std::int64_t tooFrequentApiCallsErrorCode = 1010710003;
76 constexpr auto throttlingWindow = std::chrono::milliseconds(20);
77
78 if (QtOhos::tryExtractErrorInfoFromJsError(rejectedValue).optCode != tooFrequentApiCallsErrorCode)
79 return makeRejectedPromise(cbInfo.Env(), rejectedValue);
80
81 return makePromiseResolvedAfter(cbInfo.jsState(), throttlingWindow)
82 .onThen(
83 [call = std::move(call)](const QOhosCallbackInfo &cbInfo) {
84 return callToPromiseOrRejectOnThrow(cbInfo.jsState(), call);
85 });
86 });
87}
88
89std::string applyWorkaroundForEmptyHoverTips(const std::string &hoverTips)
90{
91 return !hoverTips.empty() ? hoverTips : " ";
92}
93
95{
96 return [](QOhosJsState &jsState) {
97 return QNapi::Array::New(jsState.env());
98 };
99}
100
102{
103 auto optQAbility = jsState.defaultQAbility();
104 if (!optQAbility) {
105 qOhosPrintfWarning("%s: no default Ability instance", Q_FUNC_INFO);
106 return std::nullopt;
107 }
108
109 return optQAbility->eval<QNapi::Object>("context");
110}
111
113{
114 auto optContext = tryGetContextForStatusBarManager(jsState);
115 if (!optContext)
116 qOhosReportFatalErrorAndAbort("%s: no context for the status bar manager", Q_FUNC_INFO);
117
118 return *optContext;
119}
120
122 QOhosJsState &jsState, std::function<void()> leftClickListener)
123{
124 return registerQOhosOnOffMethodsBasedEventHandler(
125 jsState.eval<QNapi::Object>("@kit.StatusBarExtensionKit.statusBarManager"),
126 "statusBarIconClick",
127 [leftClickListener = std::move(leftClickListener)](const QOhosCallbackInfo &cbInfo) {
128 auto eventData = cbInfo.getFirstArg<QNapi::Object>(Q_FUNC_INFO);
129 auto optIconClickType = QNapi::getOptionalPropOrEmpty<QNapi::String>(
130 QNapi::getOptionalPropOrEmpty<QNapi::Object>(eventData, "data"),
131 "iconClickType");
132
133 if (optIconClickType.IsEmpty()) {
134 qOhosPrintfDebug(
135 "%s: no 'iconClickType' in the event (%s), ignoring it",
136 Q_FUNC_INFO, QNapi::toJsonString(eventData).c_str());
137 return;
138 }
139
140 if (optIconClickType.Utf8Value() == "leftClick")
141 leftClickListener();
142 });
143}
144
146{
147 auto optContext = tryGetContextForStatusBarManager(jsState);
148 if (!optContext) {
149 qOhosPrintfWarning(
150 "%s: no context for the status bar manager, skipping the removal of the icon",
151 Q_FUNC_INFO);
152 return QtOhos::makeResolvedPromise(jsState.env());
153 }
154
155 jsState.eval(
156 "@kit.StatusBarExtensionKit.statusBarManager.removeFromStatusBar(*)",
157 {*optContext});
158
159 qOhosPrintfDebug("%s: Successfully removed icon from status bar", Q_FUNC_INFO);
160
161 return QtOhos::makeResolvedPromise(jsState.env());
162}
163
164QNapi::Promise addItemToOhosStatusBar(QOhosJsState &jsState, QNapi::Object statusBarItem)
165{
166 jsState.eval(
167 "@kit.StatusBarExtensionKit.statusBarManager.addToStatusBar(*)",
168 {getContextForStatusBarManager(jsState), statusBarItem});
169
170 qOhosPrintfDebug("%s: Successfully added icon to status bar [Statusbar] ", Q_FUNC_INFO);
171
172 return QtOhos::makeResolvedPromise(jsState.env());
173}
174
175QNapi::Promise updateOhosStatusBarIcon(QOhosJsState &jsState, QNapi::Object iconData)
176{
177 qOhosPrintfDebug("%s", Q_FUNC_INFO);
178
179 if (iconData.IsEmpty()) {
180 qOhosPrintfError("%s: Icon data is empty", Q_FUNC_INFO);
181 return QtOhos::makeResolvedPromise(jsState.env());
182 }
183
184 jsState.eval(
185 "@kit.StatusBarExtensionKit.statusBarManager.updateStatusBarIcon(*)",
186 {getContextForStatusBarManager(jsState), iconData});
187
188 qOhosPrintfDebug("%s: Successfully updated status bar icon", Q_FUNC_INFO);
189
190 return QtOhos::makeResolvedPromise(jsState.env());
191}
192
193QNapi::Promise updateOhosStatusBarHoverTips(QOhosJsState &jsState, const std::string &hoverTips)
194{
195 return jsState.evalToPromiseOrRejectOnThrow(
196 "@kit.StatusBarExtensionKit.statusBarManager.updateStatusBarHoverTips(*)",
197 {getContextForStatusBarManager(jsState), applyWorkaroundForEmptyHoverTips(hoverTips)});
198}
199
200QNapi::Promise updateOhosStatusBarMenu(QOhosJsState &jsState, QNapi::Array statusBarGroupMenus)
201{
202 jsState.eval(
203 "@kit.StatusBarExtensionKit.statusBarManager.updateStatusBarMenu(*)",
204 {getContextForStatusBarManager(jsState), statusBarGroupMenus});
205
206 qOhosPrintfDebug("%s: Successfully updated status bar menu", Q_FUNC_INFO);
207
208 return QtOhos::makeResolvedPromise(jsState.env());
209}
210
212 QOhosJsState &jsState, QNapi::Object statusBarIcon, const std::string &title,
213 QNapi::Array statusBarGroupMenus, std::optional<std::string> hoverTips)
214{
215 auto *env = jsState.env();
216
217 auto quickOperation = QNapi::makeObject(
218 env,
219 {
220 {"title", title},
221 {"height", quickOperationHeight},
222 {"abilityName", ""},
223 });
224
225 auto statusBarItem = QNapi::makeObject(
226 env,
227 {
228 {"icons", statusBarIcon},
229 {"quickOperation", quickOperation},
230 {"statusBarGroupMenu", statusBarGroupMenus},
231 });
232
233 if (hoverTips.has_value())
234 statusBarItem.set("hoverTips", applyWorkaroundForEmptyHoverTips(hoverTips.value()));
235
236 return statusBarItem;
237}
238
240 const QIcon &icon, const QSize &imageSize, const QColor &fallbackContentColor)
241{
242 auto iconImage = icon.pixmap(imageSize).toImage();
243 if (!iconImage.isNull())
244 return iconImage;
245
246 auto fallbackImage = QImage(imageSize, QImage::Format_RGBA8888);
247 fallbackImage.fill(fallbackContentColor);
248 return fallbackImage;
249}
250
251QNapi::Object makeJsPixelMapFromIcon(QOhosJsState &jsState, const QIcon &icon)
252{
253 const QSize iconSize(48, 48);
254 return makeDisplayDensityScaledJsPixelMapFromQImage(
255 jsState, convertIconToScaledImage(icon, iconSize, Qt::transparent));
256}
257
258QNapi::Object makeJsStatusBarIcon(QOhosJsState &jsState, const QIcon &icon)
259{
260 auto *env = jsState.env();
261
262 auto trayIcon = makeJsPixelMapFromIcon(jsState, icon);
263
264 auto imageSize = trayIcon.eval<QNapi::Object>("getImageInfoSync().size");
265
266 int imageWidth = imageSize.get<QNapi::Number>("width");
267 int imageHeight = imageSize.get<QNapi::Number>("height");
268 qOhosPrintfDebug("%s: ceated PixelMap size: %dx%d", Q_FUNC_INFO, imageWidth, imageHeight);
269
270 return QNapi::makeObject(
271 env,
272 {
273 {"white", trayIcon},
274 {"black", trayIcon},
275 });
276}
277
279 QOhosJsState &jsState, const std::string &title, const std::string &text)
280{
281 using ContentType = QtOhosQpa::enums::ohos::notificationManager::ContentType;
282
283 return QNapi::makeObject(
284 jsState.env(),
285 {
286 {"notificationContentType", jsState.mapOhosEnumToJs(ContentType::NOTIFICATION_CONTENT_BASIC_TEXT)},
287 {
288 "normal",
289 QNapi::makeObject(
290 jsState.env(),
291 {
292 {"title", title},
293 {"text", text},
294 }),
295 },
296 });
297}
298
300 QOhosJsState &jsState, const std::string &title, const std::string &content,
301 const QIcon &icon, std::optional<int> optAutoDeletedDelayMs)
302{
303 auto iconPixelMap = makeDisplayDensityScaledJsPixelMapFromQImage(
304 jsState, convertIconToScaledImage(icon, notificationIconSize, Qt::transparent));
305 auto notificationRequest = QNapi::makeObject(
306 jsState.env(),
307 {
308 {"content", makeJsNotificationContent(jsState, title, content)},
309 {"smallIcon", iconPixelMap},
310 });
311 if (optAutoDeletedDelayMs.has_value()) {
312 notificationRequest.set(
313 "autoDeletedTime",
314 QDateTime::currentMSecsSinceEpoch() + optAutoDeletedDelayMs.value());
315 }
316
317 return notificationRequest;
318}
319
320class QOhosSystemTrayIcon final : public QPlatformSystemTrayIcon
321{
322public:
324
327 void updateIcon(const QIcon &icon) override;
328 void updateToolTip(const QString &tooltip) override;
329 void updateMenu(QPlatformMenu *menu) override;
330 QRect geometry() const override;
332 const QString &title, const QString &msg, const QIcon &icon,
333 MessageIcon iconType, int msecs) override;
334
337
338 QPlatformMenu *createMenu() const override;
339
340private:
341 QIcon m_icon;
342 std::optional<QString> m_optToolTip;
343 QPlatformMenu *m_menu = nullptr;
344
345 struct JsScopeData
346 {
347 std::shared_ptr<void> m_iconLeftClickListenerHandle;
348 };
349
350 std::optional<QtOhos::JsScopeDataOwner<JsScopeData>> m_optJsScopeData;
351};
352
353QOhosSystemTrayIcon::QOhosSystemTrayIcon() = default;
354
355void QOhosSystemTrayIcon::init()
356{
357 if (m_optJsScopeData)
358 return;
359
360 auto jsStatusBarGroupMenusFactory =
361 m_menu != nullptr
362 ? static_cast<QOhosStatusBarMenu *>(m_menu)->makeJsStatusBarGroupMenusFactory()
363 : makeEmptyJsArrayFactory();
364
365 auto selfRef = QtOhos::makeQThreadSafeRef(this);
366 m_optJsScopeData.emplace();
367 QOhosJsThreadGateway::invokeAndWaitForContinue(
368 [&](QOhosJsState &jsState, QOhosTaskPromise<> taskPromise) {
369 callWithStatusBarManagerThrottlingRetry(
370 jsState,
371 [icon = m_icon, optToolTip = m_optToolTip, jsStatusBarGroupMenusFactory](
372 QOhosJsState &jsState) {
373 auto jsStatusBarIcon = makeJsStatusBarIcon(jsState, icon);
374 auto jsStatusBarGroupMenus = jsStatusBarGroupMenusFactory(jsState);
375 auto optHoverTipsString = qTransform(
376 optToolTip,
377 [](const auto &toolTip) {
378 return toolTip.toStdString();
379 });
380 auto jsStatusBarItem = makeJsStatusBarItem(
381 jsState, jsStatusBarIcon, ohosSystemTrayItemTitle, jsStatusBarGroupMenus,
382 optHoverTipsString);
383
384 return addItemToOhosStatusBar(jsState, jsStatusBarItem);
385 })
386 .onThen(
387 [this, selfRef, taskPromise = std::move(taskPromise)](const QOhosCallbackInfo &cbInfo) {
388 **m_optJsScopeData = JsScopeData {
389 .m_iconLeftClickListenerHandle = registerOhosIconLeftClickListener(
390 cbInfo.jsState(),
391 [selfRef]() {
392 selfRef.visitInQtThreadIfAlive(
393 [](auto &self) {
394 Q_EMIT self.activated(QPlatformSystemTrayIcon::Trigger);
395 });
396 }),
397 };
398
399 m_optJsScopeData->addAsyncCleanup(
400 [](QOhosJsState &jsState, JsScopeData &, QOhosTaskPromise<> donePromise) {
401 callWithStatusBarManagerThrottlingRetry(jsState, &removeIconFromOhosStatusBar)
402 .onCatch(QtOhos::makeErrorLoggingJsCallback("remove system tray icon"))
403 .onFinally(std::move(donePromise).makeChained(Q_FUNC_INFO));
404 });
405
406 taskPromise();
407 })
408 .onCatch(
409 [](const QOhosCallbackInfo &cbInfo) {
410 QtOhos::logJsCallbackError(cbInfo, "Failed to set up the status bar item");
411 qOhosReportFatalErrorAndAbort(
412 "%s: failed to set up the status bar item", Q_FUNC_INFO);
413 });
414 },
415 Q_FUNC_INFO);
416
417 qOhosPrintfDebug("%s: System tray icon initialized", Q_FUNC_INFO);
418}
419
420void QOhosSystemTrayIcon::cleanup()
421{
422 m_optJsScopeData.reset();
423}
424
425void QOhosSystemTrayIcon::updateIcon(const QIcon &icon)
426{
427 if (!m_optJsScopeData)
428 return;
429
430 m_icon = icon;
431
432 QOhosJsThreadGateway::invokeAndWaitForContinue(
433 [&](QOhosJsState &jsState, QOhosTaskPromise<> taskPromise) {
434 callWithStatusBarManagerThrottlingRetry(
435 jsState,
436 [icon](QOhosJsState &jsState) {
437 return updateOhosStatusBarIcon(jsState, makeJsStatusBarIcon(jsState, icon));
438 })
439 .onCatch(QtOhos::makeErrorLoggingJsCallback("updateStatusBarIcon()"))
440 .onFinally(std::move(taskPromise).makeChained(Q_FUNC_INFO));
441 },
442 Q_FUNC_INFO);
443}
444
445void QOhosSystemTrayIcon::updateToolTip(const QString &tooltip)
446{
447 m_optToolTip = tooltip;
448
449 if (m_optJsScopeData) {
450 QOhosJsThreadGateway::invokeAndWaitForContinue(
451 [&](QOhosJsState &jsState, QOhosTaskPromise<> taskPromise) {
452 callWithStatusBarManagerThrottlingRetry(
453 jsState,
454 [tooltip](QOhosJsState &jsState) {
455 return updateOhosStatusBarHoverTips(jsState, tooltip.toStdString());
456 })
457 .onCatch(QtOhos::makeErrorLoggingJsCallback("updateStatusBarHoverTips()"))
458 .onFinally(std::move(taskPromise).makeChained(Q_FUNC_INFO));
459 },
460 Q_FUNC_INFO);
461 }
462}
463
464QRect QOhosSystemTrayIcon::geometry() const
465{
466 return trayIconGeometry;
467}
468
469void QOhosSystemTrayIcon::showMessage(
470 const QString &title, const QString &msg, const QIcon &icon, MessageIcon iconType, int msecs)
471{
472 Q_UNUSED(iconType);
473 QOhosJsThreadGateway::invokeAndWaitForContinue(
474 [&](QOhosJsState &jsState, QOhosTaskPromise<> taskPromise) {
475 auto notificationRequest = makeJsNotificationRequest(
476 jsState, title.toStdString(), msg.toStdString(), icon,
477 msecs > 0 ? std::optional(msecs) : std::nullopt);
478 jsState.evalToPromiseOrRejectOnThrow("@ohos.notificationManager.publish(*)", {notificationRequest})
479 .onCatch(QtOhos::makeErrorLoggingJsCallback("publish()"))
480 .onFinally(std::move(taskPromise).makeChained(Q_FUNC_INFO));
481 },
482 Q_FUNC_INFO);
483}
484
485bool QOhosSystemTrayIcon::isSystemTrayAvailable() const
486{
487 return true;
488}
489
490bool QOhosSystemTrayIcon::supportsMessages() const
491{
492 return true;
493}
494
495QPlatformMenu *QOhosSystemTrayIcon::createMenu() const
496{
497 if (m_menu == nullptr)
498 return makeQOhosStatusBarMenu().release();
499 return m_menu;
500}
501
502void QOhosSystemTrayIcon::updateMenu(QPlatformMenu *menu)
503{
504 qOhosPrintfDebug("%s: Updating status bar menu", Q_FUNC_INFO);
505
506 m_menu = menu;
507
508 if (!m_optJsScopeData) {
509 qOhosPrintfDebug("%s: System tray not initialized yet", Q_FUNC_INFO);
510 return;
511 }
512
513 auto jsStatusBarGroupMenusFactory =
514 m_menu != nullptr
515 ? static_cast<QOhosStatusBarMenu *>(m_menu)->makeJsStatusBarGroupMenusFactory()
516 : makeEmptyJsArrayFactory();
517
518 QOhosJsThreadGateway::invokeAndWaitForContinue(
519 [&](QOhosJsState &jsState, QOhosTaskPromise<> taskPromise) {
520 callWithStatusBarManagerThrottlingRetry(
521 jsState,
522 [jsStatusBarGroupMenusFactory](QOhosJsState &jsState) {
523 return updateOhosStatusBarMenu(jsState, jsStatusBarGroupMenusFactory(jsState));
524 })
525 .onCatch(QtOhos::makeErrorLoggingJsCallback("updateStatusBarMenu()"))
526 .onFinally(std::move(taskPromise).makeChained(Q_FUNC_INFO));
527 },
528 Q_FUNC_INFO);
529}
530
531}
532
534{
535 return std::make_unique<QOhosSystemTrayIcon>();
536}
537
538QT_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.
Combined button and popup list for selecting options.
QNapi::Promise updateOhosStatusBarHoverTips(QOhosJsState &jsState, const std::string &hoverTips)
std::optional< QNapi::Object > tryGetContextForStatusBarManager(QOhosJsState &jsState)
std::function< QNapi::Array(QOhosJsState &)> makeEmptyJsArrayFactory()
QNapi::Promise removeIconFromOhosStatusBar(QOhosJsState &jsState)
QNapi::Object makeJsStatusBarItem(QOhosJsState &jsState, QNapi::Object statusBarIcon, const std::string &title, QNapi::Array statusBarGroupMenus, std::optional< std::string > hoverTips)
QNapi::Promise callToPromiseOrRejectOnThrow(QOhosJsState &jsState, const std::function< QNapi::Promise(QOhosJsState &)> &func)
QNapi::Promise callWithStatusBarManagerThrottlingRetry(QOhosJsState &jsState, std::function< QNapi::Promise(QOhosJsState &)> call)
std::shared_ptr< void > registerOhosIconLeftClickListener(QOhosJsState &jsState, std::function< void()> leftClickListener)
QImage convertIconToScaledImage(const QIcon &icon, const QSize &imageSize, const QColor &fallbackContentColor)
QNapi::Object makeJsStatusBarIcon(QOhosJsState &jsState, const QIcon &icon)
QNapi::Object makeJsPixelMapFromIcon(QOhosJsState &jsState, const QIcon &icon)
std::string applyWorkaroundForEmptyHoverTips(const std::string &hoverTips)
QNapi::Promise updateOhosStatusBarMenu(QOhosJsState &jsState, QNapi::Array statusBarGroupMenus)
QNapi::Promise makePromiseResolvedAfter(QOhosJsState &jsState, std::chrono::milliseconds delay)
QNapi::Object makeJsNotificationRequest(QOhosJsState &jsState, const std::string &title, const std::string &content, const QIcon &icon, std::optional< int > optAutoDeletedDelayMs)
QNapi::Object getContextForStatusBarManager(QOhosJsState &jsState)
QNapi::Promise addItemToOhosStatusBar(QOhosJsState &jsState, QNapi::Object statusBarItem)
constexpr int quickOperationHeight
QNapi::Promise updateOhosStatusBarIcon(QOhosJsState &jsState, QNapi::Object iconData)
const std::string ohosSystemTrayItemTitle
QNapi::Promise makeRejectedPromise(napi_env env, QNapi::Value rejectValue)
QNapi::Object makeJsNotificationContent(QOhosJsState &jsState, const std::string &title, const std::string &text)
int setJsTimeout(QOhosJsState &jsState, std::function< void(const QOhosCallbackInfo &)> timeoutFunc, std::chrono::milliseconds delay)
QT_BEGIN_NAMESPACE std::unique_ptr< QPlatformSystemTrayIcon > makeQOhosSystemTrayIcon()