// Copyright (C) 2011 ~ 2018 Deepin Technology Co., Ltd. // SPDX-FileCopyrightText: 2018 - 2023 UnionTech Software Technology Co., Ltd. // // SPDX-License-Identifier: LGPL-3.0-or-later #include "snitrayitemwidget.h" #include "themeappicon.h" #include "tipswidget.h" #include "utils.h" #include #include #include #include #include #include #include #include #include DGUI_USE_NAMESPACE #define IconSize 20 const QStringList ItemCategoryList {"ApplicationStatus", "Communications", "SystemServices", "Hardware"}; const QStringList ItemStatusList {"Passive", "Active", "NeedsAttention"}; const QStringList LeftClickInvalidIdList {"sogou-qimpanel",}; QPointer SNITrayItemWidget::PopupWindow = nullptr; Dock::Position SNITrayItemWidget::DockPosition = Dock::Position::Bottom; using namespace Dock; SNITrayItemWidget::SNITrayItemWidget(const QString &sniServicePath, QWidget *parent) : BaseTrayWidget(parent) , m_dbusMenuImporter(nullptr) , m_menu(nullptr) , m_updateIconTimer(new QTimer(this)) , m_updateOverlayIconTimer(new QTimer(this)) , m_updateAttentionIconTimer(new QTimer(this)) , m_sniServicePath(sniServicePath) , m_popupTipsDelayTimer(new QTimer(this)) , m_handleMouseReleaseTimer(new QTimer(this)) , m_tipsLabel(new TipsWidget) , m_popupShown(false) { m_popupTipsDelayTimer->setInterval(500); m_popupTipsDelayTimer->setSingleShot(true); m_handleMouseReleaseTimer->setSingleShot(true); m_handleMouseReleaseTimer->setInterval(100); connect(m_handleMouseReleaseTimer, &QTimer::timeout, this, &SNITrayItemWidget::handleMouseRelease); connect(m_popupTipsDelayTimer, &QTimer::timeout, this, &SNITrayItemWidget::showHoverTips); if (PopupWindow.isNull()) { DockPopupWindow *arrowRectangle = new DockPopupWindow(nullptr); arrowRectangle->setRadius(6); arrowRectangle->setObjectName("snitraypopup"); PopupWindow = arrowRectangle; if (Utils::IS_WAYLAND_DISPLAY) PopupWindow->setWindowFlags(PopupWindow->windowFlags() | Qt::FramelessWindowHint); connect(qApp, &QApplication::aboutToQuit, PopupWindow, &DockPopupWindow::deleteLater); } if (m_sniServicePath.startsWith("/") || !m_sniServicePath.contains("/")) { qDebug() << "SNI service path invalid"; return; } QPair pair = serviceAndPath(m_sniServicePath); m_dbusService = pair.first; m_dbusPath = pair.second; QDBusConnection conn = QDBusConnection::sessionBus(); setOwnerPID(conn.interface()->servicePid(m_dbusService)); m_sniInter = new StatusNotifierItem(m_dbusService, m_dbusPath, QDBusConnection::sessionBus(), this); m_sniInter->setSync(false); if (!m_sniInter->isValid()) { qDebug() << "SNI dbus interface is invalid!" << m_dbusService << m_dbusPath << m_sniInter->lastError(); return; } m_updateIconTimer->setInterval(100); m_updateIconTimer->setSingleShot(true); m_updateOverlayIconTimer->setInterval(500); m_updateOverlayIconTimer->setSingleShot(true); m_updateAttentionIconTimer->setInterval(1000); m_updateAttentionIconTimer->setSingleShot(true); connect(DGuiApplicationHelper::instance(), &DGuiApplicationHelper::themeTypeChanged, this, &SNITrayItemWidget::refreshIcon); connect(m_updateIconTimer, &QTimer::timeout, this, &SNITrayItemWidget::refreshIcon); connect(m_updateOverlayIconTimer, &QTimer::timeout, this, &SNITrayItemWidget::refreshOverlayIcon); connect(m_updateAttentionIconTimer, &QTimer::timeout, this, &SNITrayItemWidget::refreshAttentionIcon); // SNI property change // thses signals of properties may not be emit automatically!! // since the SniInter in on async mode we can not call property's getter function to obtain property directly // the way to refresh properties(emit the following signals) is call property's getter function and wait these signals connect(m_sniInter, &StatusNotifierItem::AttentionIconNameChanged, this, &SNITrayItemWidget::onSNIAttentionIconNameChanged); connect(m_sniInter, &StatusNotifierItem::AttentionIconPixmapChanged, this, &SNITrayItemWidget::onSNIAttentionIconPixmapChanged); connect(m_sniInter, &StatusNotifierItem::AttentionMovieNameChanged, this, &SNITrayItemWidget::onSNIAttentionMovieNameChanged); connect(m_sniInter, &StatusNotifierItem::CategoryChanged, this, &SNITrayItemWidget::onSNICategoryChanged); connect(m_sniInter, &StatusNotifierItem::IconNameChanged, this, &SNITrayItemWidget::onSNIIconNameChanged); connect(m_sniInter, &StatusNotifierItem::IconPixmapChanged, this, &SNITrayItemWidget::onSNIIconPixmapChanged); connect(m_sniInter, &StatusNotifierItem::IconThemePathChanged, this, &SNITrayItemWidget::onSNIIconThemePathChanged); connect(m_sniInter, &StatusNotifierItem::IdChanged, this, &SNITrayItemWidget::onSNIIdChanged); connect(m_sniInter, &StatusNotifierItem::MenuChanged, this, &SNITrayItemWidget::onSNIMenuChanged); connect(m_sniInter, &StatusNotifierItem::OverlayIconNameChanged, this, &SNITrayItemWidget::onSNIOverlayIconNameChanged); connect(m_sniInter, &StatusNotifierItem::OverlayIconPixmapChanged, this, &SNITrayItemWidget::onSNIOverlayIconPixmapChanged); connect(m_sniInter, &StatusNotifierItem::StatusChanged, this, &SNITrayItemWidget::onSNIStatusChanged); // the following signals can be emit automatically // need refresh cached properties in these slots connect(m_sniInter, &StatusNotifierItem::NewIcon, [ = ] { m_sniIconName = m_sniInter->iconName(); m_sniIconPixmap = m_sniInter->iconPixmap(); m_sniIconThemePath = m_sniInter->iconThemePath(); m_updateIconTimer->start(); }); connect(m_sniInter, &StatusNotifierItem::NewOverlayIcon, [ = ] { m_sniOverlayIconName = m_sniInter->overlayIconName(); m_sniOverlayIconPixmap = m_sniInter->overlayIconPixmap(); m_sniIconThemePath = m_sniInter->iconThemePath(); m_updateOverlayIconTimer->start(); }); connect(m_sniInter, &StatusNotifierItem::NewAttentionIcon, [ = ] { m_sniAttentionIconName = m_sniInter->attentionIconName(); m_sniAttentionIconPixmap = m_sniInter->attentionIconPixmap(); m_sniIconThemePath = m_sniInter->iconThemePath(); m_updateAttentionIconTimer->start(); }); connect(m_sniInter, &StatusNotifierItem::NewStatus, [ = ] { onSNIStatusChanged(m_sniInter->status()); }); QMetaObject::invokeMethod(this, &SNITrayItemWidget::initMember, Qt::QueuedConnection); } SNITrayItemWidget::~SNITrayItemWidget() { m_tipsLabel->deleteLater(); } QString SNITrayItemWidget::itemKeyForConfig() { return QString("sni:%1").arg(m_sniId.isEmpty() ? m_sniServicePath : m_sniId); } void SNITrayItemWidget::updateIcon() { m_updateIconTimer->start(); } void SNITrayItemWidget::sendClick(uint8_t mouseButton, int x, int y) { switch (mouseButton) { case XCB_BUTTON_INDEX_1: { QFuture future = QtConcurrent::run([ = ] { StatusNotifierItem inter(m_dbusService, m_dbusPath, QDBusConnection::sessionBus()); QDBusPendingReply<> reply = inter.Activate(x, y); // try to invoke context menu while calling activate get a error. // primarily work for apps using libappindicator. reply.waitForFinished(); if (reply.isError()) { showContextMenu(x,y); } }); } break; case XCB_BUTTON_INDEX_2: m_sniInter->SecondaryActivate(x, y); break; case XCB_BUTTON_INDEX_3: showContextMenu(x, y); break; default: qDebug() << "unknown mouse button key"; break; } } bool SNITrayItemWidget::isValid() { return m_sniInter->isValid(); } SNITrayItemWidget::ItemStatus SNITrayItemWidget::status() { if (!ItemStatusList.contains(m_sniStatus)) { m_sniStatus = "Active"; return ItemStatus::Active; } return static_cast(ItemStatusList.indexOf(m_sniStatus)); } SNITrayItemWidget::ItemCategory SNITrayItemWidget::category() { if (!ItemCategoryList.contains(m_sniCategory)) { return UnknownCategory; } return static_cast(ItemCategoryList.indexOf(m_sniCategory)); } QString SNITrayItemWidget::toSNIKey(const QString &sniServicePath) { return QString("sni:%1").arg(sniServicePath); } bool SNITrayItemWidget::isSNIKey(const QString &itemKey) { return itemKey.startsWith("sni:"); } QPair SNITrayItemWidget::serviceAndPath(const QString &servicePath) { QStringList list = servicePath.split("/"); QPair pair; pair.first = list.takeFirst(); for (auto i : list) { pair.second.append("/"); pair.second.append(i); } return pair; } uint SNITrayItemWidget::servicePID(const QString &servicePath) { QString serviceName = serviceAndPath(servicePath).first; QDBusConnection conn = QDBusConnection::sessionBus(); return conn.interface()->servicePid(serviceName); } void SNITrayItemWidget::initMenu() { const QString &sniMenuPath = m_sniMenuPath.path(); if (sniMenuPath.isEmpty()) { qDebug() << "Error: current sni menu path is empty of dbus service:" << m_dbusService << "id:" << m_sniId; return; } qDebug() << "using sni service path:" << m_dbusService << "menu path:" << sniMenuPath; m_dbusMenuImporter = new DBusMenuImporter(m_dbusService, sniMenuPath, ASYNCHRONOUS, this); qDebug() << "generate the sni menu object"; m_menu = m_dbusMenuImporter->menu(); qDebug() << "the sni menu obect is:" << m_menu; } void SNITrayItemWidget::refreshIcon() { QPixmap pix = newIconPixmap(Icon); if (pix.isNull()) { return; } m_pixmap = pix; update(); Q_EMIT iconChanged(); if (!isVisible()) { Q_EMIT needAttention(); } } void SNITrayItemWidget::refreshOverlayIcon() { QPixmap pix = newIconPixmap(OverlayIcon); if (pix.isNull()) { return; } m_overlayPixmap = pix; update(); Q_EMIT iconChanged(); if (!isVisible()) { Q_EMIT needAttention(); } } void SNITrayItemWidget::refreshAttentionIcon() { /* TODO: A new approach may be needed to deal with attentionIcon */ QPixmap pix = newIconPixmap(AttentionIcon); if (pix.isNull()) { return; } m_pixmap = pix; update(); Q_EMIT iconChanged(); if (!isVisible()) { Q_EMIT needAttention(); } } void SNITrayItemWidget::showContextMenu(int x, int y) { // 这里的PopupWindow属性是置顶的,如果不隐藏,会导致菜单显示不出来 hidePopup(); // ContextMenu does not work if (m_sniMenuPath.path().startsWith("/NO_DBUSMENU")) { m_sniInter->ContextMenu(x, y); } else { if (!m_menu) { qDebug() << "context menu has not be ready, init menu"; initMenu(); } if (m_menu) m_menu->popup(QPoint(x, y)); } } void SNITrayItemWidget::onSNIAttentionIconNameChanged(const QString &value) { m_sniAttentionIconName = value; m_updateAttentionIconTimer->start(); } void SNITrayItemWidget::onSNIAttentionIconPixmapChanged(DBusImageList value) { m_sniAttentionIconPixmap = value; m_updateAttentionIconTimer->start(); } void SNITrayItemWidget::onSNIAttentionMovieNameChanged(const QString &value) { m_sniAttentionMovieName = value; m_updateAttentionIconTimer->start(); } void SNITrayItemWidget::onSNICategoryChanged(const QString &value) { m_sniCategory = value; } void SNITrayItemWidget::onSNIIconNameChanged(const QString &value) { m_sniIconName = value; m_updateIconTimer->start(); } void SNITrayItemWidget::onSNIIconPixmapChanged(DBusImageList value) { m_sniIconPixmap = value; m_updateIconTimer->start(); } void SNITrayItemWidget::onSNIIconThemePathChanged(const QString &value) { m_sniIconThemePath = value; m_updateIconTimer->start(); } void SNITrayItemWidget::onSNIIdChanged(const QString &value) { m_sniId = value; } void SNITrayItemWidget::onSNIMenuChanged(const QDBusObjectPath &value) { m_sniMenuPath = value; } void SNITrayItemWidget::onSNIOverlayIconNameChanged(const QString &value) { m_sniOverlayIconName = value; m_updateOverlayIconTimer->start(); } void SNITrayItemWidget::onSNIOverlayIconPixmapChanged(DBusImageList value) { m_sniOverlayIconPixmap = value; m_updateOverlayIconTimer->start(); } void SNITrayItemWidget::onSNIStatusChanged(const QString &status) { if (!ItemStatusList.contains(status) || m_sniStatus == status) { return; } m_sniStatus = status; Q_EMIT statusChanged(static_cast(ItemStatusList.indexOf(status))); } void SNITrayItemWidget::paintEvent(QPaintEvent *e) { Q_UNUSED(e); if (!needShow()) { return; } if (m_pixmap.isNull()) return; QPainter painter; painter.begin(this); painter.setRenderHint(QPainter::Antialiasing); //#ifdef QT_DEBUG // painter.fillRect(rect(), Qt::green); //#endif const QRectF &rf = QRect(rect()); const QRectF &rfp = QRect(m_pixmap.rect()); const QPointF &p = rf.center() - rfp.center() / m_pixmap.devicePixelRatioF(); painter.drawPixmap(p, m_pixmap); if (!m_overlayPixmap.isNull()) { painter.drawPixmap(p, m_overlayPixmap); } painter.end(); } QPixmap SNITrayItemWidget::newIconPixmap(IconType iconType) { QPixmap pixmap; if (iconType == UnknownIconType) { return pixmap; } QString iconName; DBusImageList dbusImageList; QString iconThemePath = m_sniIconThemePath; switch (iconType) { case Icon: iconName = m_sniIconName; dbusImageList = m_sniIconPixmap; break; case OverlayIcon: iconName = m_sniOverlayIconName; dbusImageList = m_sniOverlayIconPixmap; break; case AttentionIcon: iconName = m_sniAttentionIconName; dbusImageList = m_sniAttentionIconPixmap; break; case AttentionMovieIcon: iconName = m_sniAttentionMovieName; break; default: break; } const auto ratio = devicePixelRatioF(); const int iconSizeScaled = IconSize * ratio; do { // load icon from sni dbus if (!dbusImageList.isEmpty() && !dbusImageList.first().pixels.isEmpty()) { for (DBusImage dbusImage : dbusImageList) { char *image_data = dbusImage.pixels.data(); if (QSysInfo::ByteOrder == QSysInfo::LittleEndian) { for (int i = 0; i < dbusImage.pixels.size(); i += 4) { *(qint32 *)(image_data + i) = qFromBigEndian(*(qint32 *)(image_data + i)); } } QImage image((const uchar *)dbusImage.pixels.constData(), dbusImage.width, dbusImage.height, QImage::Format_ARGB32); pixmap = QPixmap::fromImage(image.scaled(iconSizeScaled, iconSizeScaled, Qt::KeepAspectRatio, Qt::SmoothTransformation)); pixmap.setDevicePixelRatio(ratio); if (!pixmap.isNull()) { break; } } } // load icon from specified file if (!iconThemePath.isEmpty() && !iconName.isEmpty()) { QDirIterator it(iconThemePath, QDirIterator::Subdirectories); while (it.hasNext()) { it.next(); if (it.fileName().startsWith(iconName, Qt::CaseInsensitive)) { QImage image(it.filePath()); pixmap = QPixmap::fromImage(image.scaled(iconSizeScaled, iconSizeScaled, Qt::KeepAspectRatio, Qt::SmoothTransformation)); pixmap.setDevicePixelRatio(ratio); if (!pixmap.isNull()) { break; } } } if (!pixmap.isNull()) { break; } } // load icon from theme // Note: this will ensure return a None-Null pixmap // so, it should be the last fallback if (!iconName.isEmpty()) { // ThemeAppIcon::getIcon 会处理高分屏缩放问题 ThemeAppIcon::getIcon(pixmap, iconName, IconSize); if (!pixmap.isNull()) { break; } } if (pixmap.isNull()) { qDebug() << "get icon faild!" << iconType; } } while (false); // QLabel *l = new QLabel; // l->setPixmap(pixmap); // l->setFixedSize(100, 100); // l->show(); return pixmap; } void SNITrayItemWidget::enterEvent(QEvent *event) { // 触屏不显示hover效果 if (!qApp->property(IS_TOUCH_STATE).toBool()) { m_popupTipsDelayTimer->start(); } BaseTrayWidget::enterEvent(event); } void SNITrayItemWidget::leaveEvent(QEvent *event) { m_popupTipsDelayTimer->stop(); if (m_popupShown && !PopupWindow->model()) hidePopup(); update(); BaseTrayWidget::leaveEvent(event); } void SNITrayItemWidget::mousePressEvent(QMouseEvent *event) { // call QWidget::mousePressEvent means to show dock-context-menu // when right button of mouse is pressed immediately in fashion mode // here we hide the right button press event when it is click in the special area m_popupTipsDelayTimer->stop(); if (event->button() == Qt::RightButton && perfectIconRect().contains(event->pos(), true)) { event->accept(); setMouseData(event); return; } QWidget::mousePressEvent(event); } void SNITrayItemWidget::mouseReleaseEvent(QMouseEvent *e) { //e->accept(); // 由于 XWindowTrayWidget 中对 发送鼠标事件到X窗口的函数, 如 sendClick/sendHoverEvent 中 // 使用了 setX11PassMouseEvent, 而每次调用 setX11PassMouseEvent 时都会导致产生 mousePress 和 mouseRelease 事件 // 因此如果直接在这里处理事件会导致一些问题, 所以使用 Timer 来延迟处理 100 毫秒内的最后一个事件 setMouseData(e); QWidget::mouseReleaseEvent(e); } void SNITrayItemWidget::handleMouseRelease() { Q_ASSERT(sender() == m_handleMouseReleaseTimer); // do not dealwith all mouse event of SystemTray, class SystemTrayItem will handle it if (trayType() == SystemTray) return; const QPoint point(m_lastMouseReleaseData.first - rect().center()); if (point.manhattanLength() > 24) return; QPoint globalPos = QCursor::pos(); uint8_t buttonIndex = XCB_BUTTON_INDEX_1; switch (m_lastMouseReleaseData.second) { case Qt:: MiddleButton: buttonIndex = XCB_BUTTON_INDEX_2; break; case Qt::RightButton: buttonIndex = XCB_BUTTON_INDEX_3; break; default: break; } sendClick(buttonIndex, globalPos.x(), globalPos.y()); // left mouse button clicked if (buttonIndex == XCB_BUTTON_INDEX_1) { Q_EMIT clicked(); } } void SNITrayItemWidget::initMember() { onSNIAttentionIconNameChanged(m_sniInter->attentionIconName()); onSNIAttentionIconPixmapChanged(m_sniInter->attentionIconPixmap()); onSNIAttentionMovieNameChanged(m_sniInter->attentionMovieName()); onSNICategoryChanged(m_sniInter->category()); onSNIIconNameChanged(m_sniInter->iconName()); onSNIIconPixmapChanged(m_sniInter->iconPixmap()); onSNIIconThemePathChanged(m_sniInter->iconThemePath()); onSNIIdChanged(m_sniInter->id()); onSNIMenuChanged(m_sniInter->menu()); onSNIOverlayIconNameChanged(m_sniInter->overlayIconName()); onSNIOverlayIconPixmapChanged(m_sniInter->overlayIconPixmap()); onSNIStatusChanged(m_sniInter->status()); m_updateIconTimer->start(); m_updateOverlayIconTimer->start(); m_updateAttentionIconTimer->start(); } void SNITrayItemWidget::showHoverTips() { if (PopupWindow->model()) return; QProcess p; p.start("qdbus", {m_dbusService}); if (!p.waitForFinished(1000)) { qDebug() << "sni dbus service error : " << m_dbusService; return; } QDBusInterface infc(m_dbusService, m_dbusPath); QDBusMessage msg = infc.call("Get", "org.kde.StatusNotifierItem", "ToolTip"); if (msg.type() == QDBusMessage::ReplyMessage) { QDBusArgument arg = msg.arguments().at(0).value().variant().value(); DBusToolTip tooltip = qdbus_cast(arg); if (tooltip.title.isEmpty()) return; // 当提示信息中有换行符时,需要使用setTextList if (tooltip.title.contains('\n')) m_tipsLabel->setTextList(tooltip.title.split('\n')); else m_tipsLabel->setText(tooltip.title); m_tipsLabel->setAccessibleName(itemKeyForConfig().replace("sni:","")); showPopupWindow(m_tipsLabel); } } void SNITrayItemWidget::hideNonModel() { // auto hide if popup is not model window if (m_popupShown && !PopupWindow->model()) hidePopup(); } void SNITrayItemWidget::popupWindowAccept() { if (!PopupWindow->isVisible()) return; hidePopup(); } void SNITrayItemWidget::hidePopup() { m_popupTipsDelayTimer->stop(); m_popupShown = false; PopupWindow->hide(); emit PopupWindow->accept(); emit requestWindowAutoHide(true); } // 获取在最外层的窗口(MainWindow)中的位置 const QPoint SNITrayItemWidget::topleftPoint() const { QPoint p; const QWidget *w = this; do { p += w->pos(); w = qobject_cast(w->parent()); } while (w); return p; } const QPoint SNITrayItemWidget::popupMarkPoint() const { QPoint p(topleftPoint()); const QRect r = rect(); const QRect wr = window()->rect(); switch (DockPosition) { case Dock::Position::Top: p += QPoint(r.width() / 2, r.height() + (wr.height() - r.height()) / 2); break; case Dock::Position::Bottom: p += QPoint(r.width() / 2, 0 - (wr.height() - r.height()) / 2); break; case Dock::Position::Left: p += QPoint(r.width() + (wr.width() - r.width()) / 2, r.height() / 2); break; case Dock::Position::Right: p += QPoint(0 - (wr.width() - r.width()) / 2, r.height() / 2); break; } return p; } QPixmap SNITrayItemWidget::icon() { return m_pixmap; } void SNITrayItemWidget::showPopupWindow(QWidget *const content, const bool model) { m_popupShown = true; if (model) emit requestWindowAutoHide(false); DockPopupWindow *popup = PopupWindow.data(); QWidget *lastContent = popup->getContent(); if (lastContent) lastContent->setVisible(false); popup->setPosition(DockPosition); popup->resize(content->sizeHint()); popup->setContent(content); QPoint p = popupMarkPoint(); if (!popup->isVisible()) QMetaObject::invokeMethod(popup, "show", Qt::QueuedConnection, Q_ARG(QPoint, p), Q_ARG(bool, model)); else popup->show(p, model); } void SNITrayItemWidget::setMouseData(QMouseEvent *e) { m_lastMouseReleaseData.first = e->pos(); m_lastMouseReleaseData.second = e->button(); m_handleMouseReleaseTimer->start(); } bool SNITrayItemWidget::containsPoint(const QPoint &pos) { QPoint ptGlobal = mapToGlobal(QPoint(0, 0)); QRect rectGlobal(ptGlobal, this->size()); if (rectGlobal.contains(pos)) return true; if (!m_menu) { if (m_dbusMenuImporter) { qInfo() << "importer exists: " << m_dbusMenuImporter; m_menu = m_dbusMenuImporter->menu(); } else { qInfo() << "importer not exists."; initMenu(); } } // 如果菜单列表隐藏,则认为不在区域内 if (!m_menu->isVisible()) return false; // 判断鼠标是否在菜单区域 return m_menu->geometry().contains(pos); }