diff --git a/src/mumble/CMakeLists.txt b/src/mumble/CMakeLists.txt index fa41283bcca..aef67ab2ef8 100644 --- a/src/mumble/CMakeLists.txt +++ b/src/mumble/CMakeLists.txt @@ -309,6 +309,8 @@ set(MUMBLE_SOURCES "widgets/SearchDialogTree.h" "widgets/SemanticSlider.cpp" "widgets/SemanticSlider.h" + "widgets/TrayIcon.cpp" + "widgets/TrayIcon.h" "${SHARED_SOURCE_DIR}/ACL.cpp" diff --git a/src/mumble/Global.cpp b/src/mumble/Global.cpp index 06cab015838..de46978b0fd 100644 --- a/src/mumble/Global.cpp +++ b/src/mumble/Global.cpp @@ -93,6 +93,7 @@ void Global::migrateDataDir(const QDir &toDir) { Global::Global(const QString &qsConfigPath) { mw = 0; + trayIcon = 0; db = 0; pluginManager = 0; nam = 0; diff --git a/src/mumble/Global.h b/src/mumble/Global.h index c129d26e214..3f624e67fa4 100644 --- a/src/mumble/Global.h +++ b/src/mumble/Global.h @@ -34,6 +34,7 @@ class OverlayClient; class LogEmitter; class DeveloperConsole; class TalkingUI; +class TrayIcon; class QNetworkAccessManager; @@ -50,6 +51,7 @@ struct Global Q_DECL_FINAL { static Global &get(); MainWindow *mw; + TrayIcon *trayIcon; Settings s; boost::shared_ptr< ServerHandler > sh; boost::shared_ptr< AudioInput > ai; diff --git a/src/mumble/Log_unix.cpp b/src/mumble/Log_unix.cpp index 7e9159dcbab..ef3bfdbde15 100644 --- a/src/mumble/Log_unix.cpp +++ b/src/mumble/Log_unix.cpp @@ -1,14 +1,32 @@ -// Copyright 2012-2023 The Mumble Developers. All rights reserved. +// Copyright 2012-2024 The Mumble Developers. All rights reserved. // Use of this source code is governed by a BSD-style license // that can be found in the LICENSE file at the root of the // Mumble source tree or at . #include "Log.h" #include "MainWindow.h" -#include "Settings.h" - -#include +#include "widgets/TrayIcon.h" +#include "Global.h" void Log::postNotification(MsgType mt, const QString &plain) { - // FIXME + if (mt == MsgType::TextMessage || mt == MsgType::PrivateTextMessage) { + // Use custom icon for text messages + Global::get().trayIcon->showMessage(msgName(mt), plain, Global::get().mw->iconComment); + return; + } + + QSystemTrayIcon::MessageIcon msgIcon; + switch (mt) { + case DebugInfo: + case CriticalError: + msgIcon = QSystemTrayIcon::Critical; + break; + case Warning: + msgIcon = QSystemTrayIcon::Warning; + break; + default: + msgIcon = QSystemTrayIcon::Information; + break; + } + Global::get().trayIcon->showMessage(msgName(mt), plain, msgIcon); } diff --git a/src/mumble/Log_win.cpp b/src/mumble/Log_win.cpp index 04384099f0f..ef3bfdbde15 100644 --- a/src/mumble/Log_win.cpp +++ b/src/mumble/Log_win.cpp @@ -1,10 +1,32 @@ -// Copyright 2012-2023 The Mumble Developers. All rights reserved. +// Copyright 2012-2024 The Mumble Developers. All rights reserved. // Use of this source code is governed by a BSD-style license // that can be found in the LICENSE file at the root of the // Mumble source tree or at . #include "Log.h" +#include "MainWindow.h" +#include "widgets/TrayIcon.h" +#include "Global.h" void Log::postNotification(MsgType mt, const QString &plain) { - // FIXME + if (mt == MsgType::TextMessage || mt == MsgType::PrivateTextMessage) { + // Use custom icon for text messages + Global::get().trayIcon->showMessage(msgName(mt), plain, Global::get().mw->iconComment); + return; + } + + QSystemTrayIcon::MessageIcon msgIcon; + switch (mt) { + case DebugInfo: + case CriticalError: + msgIcon = QSystemTrayIcon::Critical; + break; + case Warning: + msgIcon = QSystemTrayIcon::Warning; + break; + default: + msgIcon = QSystemTrayIcon::Information; + break; + } + Global::get().trayIcon->showMessage(msgName(mt), plain, msgIcon); } diff --git a/src/mumble/MainWindow.cpp b/src/mumble/MainWindow.cpp index b4e46f3f5f2..9e96ed428f6 100644 --- a/src/mumble/MainWindow.cpp +++ b/src/mumble/MainWindow.cpp @@ -82,6 +82,7 @@ #include #include "widgets/SemanticSlider.h" +#include "widgets/TrayIcon.h" #ifdef Q_OS_WIN # include @@ -111,6 +112,7 @@ MainWindow::MainWindow(QWidget *p) SvgIcon::addSvgPixmapsToIcon(qiTalkingOn, QLatin1String("skin:talking_on.svg")); SvgIcon::addSvgPixmapsToIcon(qiTalkingShout, QLatin1String("skin:talking_alt.svg")); SvgIcon::addSvgPixmapsToIcon(qiTalkingWhisper, QLatin1String("skin:talking_whisper.svg")); + SvgIcon::addSvgPixmapsToIcon(iconComment, QLatin1String("skin:comment.svg")); #ifdef Q_OS_MAC if (QFile::exists(QLatin1String("skin:mumble.icns"))) @@ -193,6 +195,7 @@ MainWindow::MainWindow(QWidget *p) QObject::connect(this, &MainWindow::serverSynchronized, Global::get().pluginManager, &PluginManager::on_serverSynchronized); + QObject::connect(this, &MainWindow::serverSynchronized, this, &MainWindow::userStateChanged); QAccessible::installFactory(AccessibleSlider::semanticSliderFactory); } @@ -622,7 +625,7 @@ void MainWindow::closeEvent(QCloseEvent *e) { mb.setCheckBox(qcbRemember); mb.exec(); if (mb.clickedButton() == qpbMinimize) { - showMinimized(); + setWindowState(windowState() | Qt::WindowMinimized); e->ignore(); // If checkbox is checked and not connected, always minimize @@ -645,7 +648,7 @@ void MainWindow::closeEvent(QCloseEvent *e) { } #endif } else if (!forceQuit && (alwaysMinimize || minimizeDueToConnected)) { - showMinimized(); + setWindowState(windowState() | Qt::WindowMinimized); e->ignore(); return; } @@ -700,7 +703,24 @@ void MainWindow::showEvent(QShowEvent *e) { QMainWindow::showEvent(e); } +#include + void MainWindow::changeEvent(QEvent *e) { + // Hide in tray when minimized + if (Global::get().s.bHideInTray && e->type() == QEvent::WindowStateChange) { + // This code block is not triggered on (X)Wayland due to a Qt bug we can do nothing about (QTBUG-74310) + QWindowStateChangeEvent *windowStateEvent = static_cast< QWindowStateChangeEvent * >(e); + if (windowStateEvent) { + bool wasMinimizedState = (windowStateEvent->oldState() & Qt::WindowMinimized) != 0; + bool isMinimizedState = (windowState() & Qt::WindowMinimized) != 0; + qDebug() << "was_minimized " << wasMinimizedState << " is_minimized " << isMinimizedState; + if (!wasMinimizedState && isMinimizedState) { + emit Global::get().trayIcon->on_hideAction_triggered(); + } + return; + } + } + QWidget::changeEvent(e); } @@ -2536,6 +2556,8 @@ void MainWindow::updateMenuPermissions() { } void MainWindow::userStateChanged() { + Global::get().trayIcon->updateIcon(); + ClientUser *user = ClientUser::get(Global::get().uiSession); if (!user) { Global::get().bAttenuateOthers = false; @@ -2606,6 +2628,7 @@ void MainWindow::on_qaAudioMute_triggered() { } updateAudioToolTips(); + Global::get().trayIcon->updateIcon(); } void MainWindow::setAudioMute(bool mute) { @@ -2650,6 +2673,7 @@ void MainWindow::on_qaAudioDeaf_triggered() { } updateAudioToolTips(); + Global::get().trayIcon->updateIcon(); } void MainWindow::setAudioDeaf(bool deaf) { @@ -2762,6 +2786,7 @@ void MainWindow::pttReleased() { void MainWindow::on_PushToMute_triggered(bool down, QVariant) { Global::get().bPushToMute = down; updateUserModel(); + Global::get().trayIcon->updateIcon(); } void MainWindow::on_VolumeUp_triggered(bool down, QVariant) { @@ -3375,6 +3400,7 @@ void MainWindow::serverDisconnected(QAbstractSocket::SocketError err, QString re qaServerBanList->setEnabled(false); qtvUsers->setCurrentIndex(QModelIndex()); qteChat->setEnabled(false); + Global::get().trayIcon->updateIcon(); #ifdef Q_OS_MAC // Remove App Nap suppression now that we're disconnected. @@ -3582,6 +3608,8 @@ void MainWindow::serverDisconnected(QAbstractSocket::SocketError err, QString re if (Global::get().s.bMinimalView) { qdwMinimalViewNote->show(); } + + Global::get().trayIcon->updateIcon(); } void MainWindow::resolverError(QAbstractSocket::SocketError, QString reason) { @@ -3600,13 +3628,13 @@ void MainWindow::resolverError(QAbstractSocket::SocketError, QString reason) { } void MainWindow::showRaiseWindow() { - if (isMinimized()) { - setWindowState((windowState() & ~Qt::WindowMinimized) | Qt::WindowActive); - } - - show(); - raise(); - activateWindow(); + setWindowState(windowState() & ~Qt::WindowMinimized); + QTimer::singleShot(1, [this]() { + show(); + raise(); + activateWindow(); + setWindowState(windowState() | Qt::WindowActive); + }); } void MainWindow::on_qaTalkingUIToggle_triggered() { @@ -3984,6 +4012,7 @@ void MainWindow::openConfigDialog() { setupView(false); updateTransmitModeComboBox(Global::get().s.atTransmit); updateUserModel(); + Global::get().trayIcon->updateIcon(); if (Global::get().s.requireRestartToApply) { if (Global::get().s.requireRestartToApply diff --git a/src/mumble/MainWindow.h b/src/mumble/MainWindow.h index db02e150b0a..8b8cc3e7085 100644 --- a/src/mumble/MainWindow.h +++ b/src/mumble/MainWindow.h @@ -80,6 +80,7 @@ class MainWindow : public QMainWindow, public Ui::MainWindow { QIcon qiIcon, qiIconMutePushToMute, qiIconMuteSelf, qiIconMuteServer, qiIconDeafSelf, qiIconDeafServer, qiIconMuteSuppressed; QIcon qiTalkingOn, qiTalkingWhisper, qiTalkingShout, qiTalkingOff; + QIcon iconComment; std::unordered_map< unsigned int, qt_unique_ptr< UserLocalNicknameDialog > > qmUserNicknameTracker; /// "Action" for when there are no actions available diff --git a/src/mumble/main.cpp b/src/mumble/main.cpp index be28b37cf8e..73d90ccfee8 100644 --- a/src/mumble/main.cpp +++ b/src/mumble/main.cpp @@ -51,6 +51,8 @@ #include "VersionCheck.h" #include "Global.h" +#include "widgets/TrayIcon.h" + #include #include #include @@ -684,6 +686,8 @@ int main(int argc, char **argv) { Global::get().mw = new MainWindow(nullptr); Global::get().mw->show(); + Global::get().trayIcon = new TrayIcon(); + Global::get().talkingUI = new TalkingUI(); // Set TalkingUI's position diff --git a/src/mumble/widgets/TrayIcon.cpp b/src/mumble/widgets/TrayIcon.cpp new file mode 100644 index 00000000000..d34454da152 --- /dev/null +++ b/src/mumble/widgets/TrayIcon.cpp @@ -0,0 +1,131 @@ +// Copyright 2024 The Mumble Developers. All rights reserved. +// Use of this source code is governed by a BSD-style license +// that can be found in the LICENSE file at the root of the +// Mumble source tree or at . + +#include "TrayIcon.h" + +#include "../ClientUser.h" +#include "../MainWindow.h" +#include "../Global.h" + +#include + +TrayIcon::TrayIcon() : QSystemTrayIcon(Global::get().mw), m_statusIcon(nullptr) { + setIcon(Global::get().mw->qiIcon); + setToolTip("Mumble"); + + QObject::connect(this, &QSystemTrayIcon::activated, this, &TrayIcon::on_icon_clicked); + + // messageClicked is buggy in Qt on some platforms and we can not do anything about this (QTBUG-87329) + QObject::connect(this, &QSystemTrayIcon::messageClicked, this, &TrayIcon::on_showAction_triggered); + + m_showAction = new QAction(tr("Show"), Global::get().mw); + QObject::connect(m_showAction, &QAction::triggered, this, &TrayIcon::on_showAction_triggered); + + m_hideAction = new QAction(tr("Hide"), Global::get().mw); + QObject::connect(m_hideAction, &QAction::triggered, this, &TrayIcon::on_hideAction_triggered); + + m_contextMenu = new QMenu(Global::get().mw); + QObject::connect(m_contextMenu, &QMenu::aboutToShow, this, &TrayIcon::updateContextMenu); + + // Some window managers hate it when a tray icon sets an empty context menu... + updateContextMenu(); + + setContextMenu(m_contextMenu); + + show(); +} + +void TrayIcon::updateIcon() { + QIcon *newIcon = nullptr; + + ClientUser *p = ClientUser::get(Global::get().uiSession); + + if (Global::get().s.bDeaf) { + newIcon = &Global::get().mw->qiIconDeafSelf; + } else if (p && p->bDeaf) { + newIcon = &Global::get().mw->qiIconDeafServer; + } else if (Global::get().s.bMute) { + newIcon = &Global::get().mw->qiIconMuteSelf; + } else if (p && p->bMute) { + newIcon = &Global::get().mw->qiIconMuteServer; + } else if (p && p->bSuppress) { + newIcon = &Global::get().mw->qiIconMuteSuppressed; + } else if (Global::get().s.bStateInTray && Global::get().bPushToMute) { + newIcon = &Global::get().mw->qiIconMutePushToMute; + } else if (p && Global::get().s.bStateInTray) { + switch (p->tsState) { + case Settings::Talking: + case Settings::MutedTalking: + newIcon = &Global::get().mw->qiTalkingOn; + break; + case Settings::Whispering: + newIcon = &Global::get().mw->qiTalkingWhisper; + break; + case Settings::Shouting: + newIcon = &Global::get().mw->qiTalkingShout; + break; + case Settings::Passive: + default: + newIcon = &Global::get().mw->qiTalkingOff; + break; + } + } else { + newIcon = &Global::get().mw->qiIcon; + } + + if (newIcon != m_statusIcon) { + m_statusIcon = newIcon; + setIcon(*m_statusIcon); + } +} + +void TrayIcon::on_icon_clicked(QSystemTrayIcon::ActivationReason reason) { + qDebug() << reason; + switch (reason) { + case QSystemTrayIcon::Trigger: + if (Global::get().mw->isVisible() && !Global::get().mw->isMinimized()) { + on_hideAction_triggered(); + } else { + on_showAction_triggered(); + } + break; + case QSystemTrayIcon::Unknown: + case QSystemTrayIcon::Context: + case QSystemTrayIcon::DoubleClick: + case QSystemTrayIcon::MiddleClick: + default: + break; + } +} + +void TrayIcon::updateContextMenu() { + qDebug() << "updateContextMenu..."; + m_contextMenu->clear(); + + if (Global::get().mw->isVisible() && !Global::get().mw->isMinimized()) { + m_contextMenu->addAction(m_hideAction); + } else { + m_contextMenu->addAction(m_showAction); + } + + m_contextMenu->addSeparator(); + m_contextMenu->addAction(Global::get().mw->qaAudioMute); + m_contextMenu->addAction(Global::get().mw->qaAudioDeaf); + m_contextMenu->addAction(Global::get().mw->qaTalkingUIToggle); + m_contextMenu->addSeparator(); + m_contextMenu->addAction(Global::get().mw->qaQuit); +} + +void TrayIcon::on_showAction_triggered() { + qDebug() << "Show window!"; + Global::get().mw->showRaiseWindow(); + updateContextMenu(); +} + +void TrayIcon::on_hideAction_triggered() { + qDebug() << "Hide window!"; + Global::get().mw->hide(); + updateContextMenu(); +} diff --git a/src/mumble/widgets/TrayIcon.h b/src/mumble/widgets/TrayIcon.h new file mode 100644 index 00000000000..45ad4b61940 --- /dev/null +++ b/src/mumble/widgets/TrayIcon.h @@ -0,0 +1,34 @@ +// Copyright 2024 The Mumble Developers. All rights reserved. +// Use of this source code is governed by a BSD-style license +// that can be found in the LICENSE file at the root of the +// Mumble source tree or at . + +#ifndef MUMBLE_MUMBLE_WIDGETS_TRAYICCON_H_ +#define MUMBLE_MUMBLE_WIDGETS_TRAYICCON_H_ + +#include "../../QtUtils.h" +#include +#include +#include + +class TrayIcon : public QSystemTrayIcon { + Q_OBJECT + +public: + TrayIcon(); + + void updateIcon(); + void on_showAction_triggered(); + void on_hideAction_triggered(); + +private: + QIcon *m_statusIcon; + QMenu *m_contextMenu; + QAction *m_showAction; + QAction *m_hideAction; + + void on_icon_clicked(QSystemTrayIcon::ActivationReason reason); + void updateContextMenu(); +}; + +#endif // MUMBLE_MUMBLE_WIDGETS_TRAYICCON_H_