mumble-voip_mumble/src/mumble/MainWindow.cpp

4205 lines
128 KiB
C++

// Copyright 2007-2023 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 <https://www.mumble.info/LICENSE>.
#include "MainWindow.h"
#include "ACL.h"
#include "ACLEditor.h"
#include "About.h"
#include "AudioInput.h"
#include "AudioStats.h"
#include "AudioWizard.h"
#include "BanEditor.h"
#include "Cert.h"
#include "Channel.h"
#include "ConnectDialog.h"
#include "Connection.h"
#include "Database.h"
#include "DeveloperConsole.h"
#include "Log.h"
#include "Net.h"
#include "NetworkConfig.h"
#include "GlobalShortcut.h"
#include "GlobalShortcutTypes.h"
#ifdef USE_OVERLAY
# include "OverlayClient.h"
#endif
#include "../SignalCurry.h"
#include "ChannelListenerManager.h"
#include "ListenerVolumeSlider.h"
#include "Markdown.h"
#include "MenuLabel.h"
#include "PTTButtonWidget.h"
#include "PluginManager.h"
#include "PositionalAudioViewer.h"
#include "QtWidgetUtils.h"
#include "RichTextEditor.h"
#include "Screen.h"
#include "SearchDialog.h"
#include "ServerHandler.h"
#include "ServerInformation.h"
#include "Settings.h"
#include "SvgIcon.h"
#include "TalkingUI.h"
#include "TextMessage.h"
#include "Themes.h"
#include "Tokens.h"
#include "User.h"
#include "UserEdit.h"
#include "UserInformation.h"
#include "UserLocalNicknameDialog.h"
#include "UserLocalVolumeSlider.h"
#include "UserModel.h"
#include "Utils.h"
#include "VersionCheck.h"
#include "ViewCert.h"
#include "VoiceRecorderDialog.h"
#include "Global.h"
#ifdef Q_OS_WIN
# include "TaskList.h"
#endif
#ifdef Q_OS_MAC
# include "AppNap.h"
#endif
#include <QAccessible>
#include <QtCore/QStandardPaths>
#include <QtCore/QUrlQuery>
#include <QtGui/QClipboard>
#include <QtGui/QDesktopServices>
#include <QtGui/QImageReader>
#include <QtGui/QScreen>
#include <QtWidgets/QDesktopWidget>
#include <QtWidgets/QFileDialog>
#include <QtWidgets/QInputDialog>
#include <QtWidgets/QMessageBox>
#include <QtWidgets/QScrollBar>
#include <QtWidgets/QToolTip>
#include <QtWidgets/QWhatsThis>
#include "widgets/SemanticSlider.h"
#ifdef Q_OS_WIN
# include <dbt.h>
#endif
#include <algorithm>
MessageBoxEvent::MessageBoxEvent(QString m) : QEvent(static_cast< QEvent::Type >(MB_QEVENT)) {
msg = m;
}
OpenURLEvent::OpenURLEvent(QUrl u) : QEvent(static_cast< QEvent::Type >(OU_QEVENT)) {
url = u;
}
MainWindow::MainWindow(QWidget *p)
: QMainWindow(p), m_localVolumeLabel(make_qt_unique< MenuLabel >(tr("Local Volume Adjustment:"), this)),
m_userLocalVolumeSlider(make_qt_unique< UserLocalVolumeSlider >(this)),
m_listenerVolumeSlider(make_qt_unique< ListenerVolumeSlider >(this)) {
SvgIcon::addSvgPixmapsToIcon(qiIconMuteSelf, QLatin1String("skin:muted_self.svg"));
SvgIcon::addSvgPixmapsToIcon(qiIconMuteServer, QLatin1String("skin:muted_server.svg"));
SvgIcon::addSvgPixmapsToIcon(qiIconMuteSuppressed, QLatin1String("skin:muted_suppressed.svg"));
SvgIcon::addSvgPixmapsToIcon(qiIconMutePushToMute, QLatin1String("skin:muted_pushtomute.svg"));
SvgIcon::addSvgPixmapsToIcon(qiIconDeafSelf, QLatin1String("skin:deafened_self.svg"));
SvgIcon::addSvgPixmapsToIcon(qiIconDeafServer, QLatin1String("skin:deafened_server.svg"));
SvgIcon::addSvgPixmapsToIcon(qiTalkingOff, QLatin1String("skin:talking_off.svg"));
SvgIcon::addSvgPixmapsToIcon(qiTalkingOn, QLatin1String("skin:talking_on.svg"));
SvgIcon::addSvgPixmapsToIcon(qiTalkingShout, QLatin1String("skin:talking_alt.svg"));
SvgIcon::addSvgPixmapsToIcon(qiTalkingWhisper, QLatin1String("skin:talking_whisper.svg"));
#ifdef Q_OS_MAC
if (QFile::exists(QLatin1String("skin:mumble.icns")))
qiIcon.addFile(QLatin1String("skin:mumble.icns"));
else
SvgIcon::addSvgPixmapsToIcon(qiIcon, QLatin1String("skin:mumble.svg"));
#else
{ SvgIcon::addSvgPixmapsToIcon(qiIcon, QLatin1String("skin:mumble.svg")); }
// Set application icon except on MacOSX, where the window-icon
// shown in the title-bar usually serves as a draggable version of the
// current open document (i.e. you can copy the open document anywhere
// simply by dragging this icon).
qApp->setWindowIcon(qiIcon);
// Set the icon on the MainWindow directly. This fixes the icon not
// being set on the MainWindow in certain environments (Ex: GTK+).
setWindowIcon(qiIcon);
#endif
#ifdef Q_OS_WIN
uiNewHardware = 1;
#endif
forceQuit = false;
restartOnQuit = false;
bAutoUnmute = false;
Channel::add(Channel::ROOT_ID, tr("Root"));
aclEdit = nullptr;
banEdit = nullptr;
userEdit = nullptr;
tokenEdit = nullptr;
voiceRecorderDialog = nullptr;
qwPTTButtonWidget = nullptr;
qtReconnect = new QTimer(this);
qtReconnect->setInterval(10000);
qtReconnect->setSingleShot(true);
qtReconnect->setObjectName(QLatin1String("Reconnect"));
qmUser = new QMenu(tr("&User"), this);
qmChannel = new QMenu(tr("&Channel"), this);
qmListener = new QMenu(tr("&Listener"), this);
qmDeveloper = new QMenu(tr("&Developer"), this);
qaEmpty = new QAction(tr("No action available..."), this);
qaEmpty->setEnabled(false);
createActions();
setupUi(this);
setupGui();
connect(qmUser, SIGNAL(aboutToShow()), this, SLOT(qmUser_aboutToShow()));
connect(qmChannel, SIGNAL(aboutToShow()), this, SLOT(qmChannel_aboutToShow()));
connect(qmListener, SIGNAL(aboutToShow()), this, SLOT(qmListener_aboutToShow()));
connect(qteChat, SIGNAL(entered(QString)), this, SLOT(sendChatbarText(QString)));
connect(qteChat, &ChatbarTextEdit::ctrlEnterPressed, [this](const QString &msg) { sendChatbarText(msg, true); });
connect(qteChat, SIGNAL(pastedImage(QString)), this, SLOT(sendChatbarMessage(QString)));
// Tray
connect(qstiIcon, SIGNAL(messageClicked()), this, SLOT(showRaiseWindow()));
connect(qaShow, SIGNAL(triggered()), this, SLOT(showRaiseWindow()));
QObject::connect(this, &MainWindow::transmissionModeChanged, this, &MainWindow::updateTransmitModeComboBox);
// Explicitly add actions to mainwindow so their shortcuts are available
// if only the main window is visible (e.Global::get(). minimal mode)
addActions(findChildren< QAction * >());
on_qmServer_aboutToShow();
on_qmSelf_aboutToShow();
qmChannel_aboutToShow();
qmUser_aboutToShow();
on_qmConfig_aboutToShow();
qmDeveloper->addAction(qaDeveloperConsole);
qmDeveloper->addAction(qaPositionalAudioViewer);
setOnTop(Global::get().s.aotbAlwaysOnTop == Settings::OnTopAlways
|| (Global::get().s.bMinimalView && Global::get().s.aotbAlwaysOnTop == Settings::OnTopInMinimal)
|| (!Global::get().s.bMinimalView && Global::get().s.aotbAlwaysOnTop == Settings::OnTopInNormal));
QObject::connect(this, &MainWindow::serverSynchronized, Global::get().pluginManager,
&PluginManager::on_serverSynchronized);
QAccessible::installFactory(AccessibleSlider::semanticSliderFactory);
}
void MainWindow::createActions() {
gsPushTalk = new GlobalShortcut(this, GlobalShortcutType::PushToTalk, tr("Push-to-Talk", "Global Shortcut"));
gsPushTalk->setObjectName(QLatin1String("PushToTalk"));
gsPushTalk->qsToolTip = tr("Push and hold this button to send voice.", "Global Shortcut");
gsPushTalk->qsWhatsThis = tr(
"This configures the push-to-talk button, and as long as you hold this button down, you will transmit voice.",
"Global Shortcut");
gsResetAudio =
new GlobalShortcut(this, GlobalShortcutType::ResetAudio, tr("Reset Audio Processor", "Global Shortcut"));
gsResetAudio->setObjectName(QLatin1String("ResetAudio"));
gsMuteSelf = new GlobalShortcut(this, GlobalShortcutType::MuteSelf, tr("Mute Self", "Global Shortcut"), 0);
gsMuteSelf->setObjectName(QLatin1String("gsMuteSelf"));
gsMuteSelf->qsToolTip = tr("Set self-mute status.", "Global Shortcut");
gsMuteSelf->qsWhatsThis =
tr("This will set or toggle your muted status. If you turn this off, you will also disable self-deafen.",
"Global Shortcut");
gsDeafSelf = new GlobalShortcut(this, GlobalShortcutType::DeafenSelf, tr("Deafen Self", "Global Shortcut"), 0);
gsDeafSelf->setObjectName(QLatin1String("gsDeafSelf"));
gsDeafSelf->qsToolTip = tr("Set self-deafen status.", "Global Shortcut");
gsDeafSelf->qsWhatsThis =
tr("This will set or toggle your deafened status. If you turn this on, you will also enable self-mute.",
"Global Shortcut");
gsUnlink = new GlobalShortcut(this, GlobalShortcutType::UnlinkPlugin, tr("Unlink Plugin", "Global Shortcut"));
gsUnlink->setObjectName(QLatin1String("UnlinkPlugin"));
gsPushMute = new GlobalShortcut(this, GlobalShortcutType::PushToMute, tr("Push-to-Mute", "Global Shortcut"));
gsPushMute->setObjectName(QLatin1String("PushToMute"));
gsJoinChannel = new GlobalShortcut(this, GlobalShortcutType::JoinChannel, tr("Join Channel", "Global Shortcut"));
gsJoinChannel->setObjectName(QLatin1String("MetaChannel"));
gsJoinChannel->qsToolTip = tr("Use in conjunction with Whisper to.", "Global Shortcut");
gsListenChannel =
new GlobalShortcut(this, GlobalShortcutType::ListenToChannel, tr("Listen to Channel", "Global Shortcut"),
QVariant::fromValue(ChannelTarget()));
gsListenChannel->setObjectName(QLatin1String("gsListenChannel"));
gsListenChannel->qsToolTip = tr("Toggles listening to the given channel.", "Global Shortcut");
#ifdef USE_OVERLAY
gsToggleOverlay =
new GlobalShortcut(this, GlobalShortcutType::ToggleOverlay, tr("Toggle Overlay", "Global Shortcut"));
gsToggleOverlay->setObjectName(QLatin1String("ToggleOverlay"));
gsToggleOverlay->qsToolTip = tr("Toggle state of in-game overlay.", "Global Shortcut");
gsToggleOverlay->qsWhatsThis = tr("This will switch the states of the in-game overlay.", "Global Shortcut");
connect(gsToggleOverlay, SIGNAL(down(QVariant)), Global::get().o, SLOT(toggleShow()));
#endif
gsMinimal =
new GlobalShortcut(this, GlobalShortcutType::ToggleMinimalView, tr("Toggle Minimal", "Global Shortcut"));
gsMinimal->setObjectName(QLatin1String("ToggleMinimal"));
gsVolumeUp = new GlobalShortcut(this, GlobalShortcutType::VolumeUp, tr("Volume Up (+10%)", "Global Shortcut"));
gsVolumeUp->setObjectName(QLatin1String("VolumeUp"));
gsVolumeDown =
new GlobalShortcut(this, GlobalShortcutType::VolumeDown, tr("Volume Down (-10%)", "Global Shortcut"));
gsVolumeDown->setObjectName(QLatin1String("VolumeDown"));
qstiIcon = new QSystemTrayIcon(qiIcon, this);
qstiIcon->setToolTip(tr("Mumble -- %1").arg(Version::getRelease()));
qstiIcon->setObjectName(QLatin1String("Icon"));
gsWhisper = new GlobalShortcut(this, GlobalShortcutType::Whisper_Shout, tr("Whisper/Shout"),
QVariant::fromValue(ShortcutTarget()));
gsWhisper->setObjectName(QLatin1String("gsWhisper"));
gsLinkChannel = new GlobalShortcut(this, GlobalShortcutType::LinkChannel, tr("Link Channel", "Global Shortcut"));
gsLinkChannel->setObjectName(QLatin1String("MetaLink"));
gsLinkChannel->qsToolTip = tr("Use in conjunction with Whisper to.", "Global Shortcut");
gsCycleTransmitMode =
new GlobalShortcut(this, GlobalShortcutType::CycleTransmitMode, tr("Cycle Transmit Mode", "Global Shortcut"));
gsCycleTransmitMode->setObjectName(QLatin1String("gsCycleTransmitMode"));
gsToggleMainWindowVisibility = new GlobalShortcut(this, GlobalShortcutType::ToggleMainWindowVisibility,
tr("Hide/show main window", "Global Shortcut"));
gsToggleMainWindowVisibility->setObjectName(QLatin1String("gsToggleMainWindowVisibility"));
gsTransmitModePushToTalk = new GlobalShortcut(this, GlobalShortcutType::UsePushToTalk,
tr("Set Transmit Mode to Push-To-Talk", "Global Shortcut"));
gsTransmitModePushToTalk->setObjectName(QLatin1String("gsTransmitModePushToTalk"));
gsTransmitModeContinuous = new GlobalShortcut(this, GlobalShortcutType::UseContinous,
tr("Set Transmit Mode to Continuous", "Global Shortcut"));
gsTransmitModeContinuous->setObjectName(QLatin1String("gsTransmitModeContinuous"));
gsTransmitModeVAD =
new GlobalShortcut(this, GlobalShortcutType::UseVAD, tr("Set Transmit Mode to VAD", "Global Shortcut"));
gsTransmitModeVAD->setObjectName(QLatin1String("gsTransmitModeVAD"));
gsSendTextMessage = new GlobalShortcut(this, GlobalShortcutType::SendTextMessage,
tr("Send Text Message", "Global Shortcut"), QVariant(QString()));
gsSendTextMessage->setObjectName(QLatin1String("gsSendTextMessage"));
gsSendClipboardTextMessage = new GlobalShortcut(this, GlobalShortcutType::SendTextMessageClipboard,
tr("Send Clipboard Text Message", "Global Shortcut"));
gsSendClipboardTextMessage->setObjectName(QLatin1String("gsSendClipboardTextMessage"));
gsSendClipboardTextMessage->qsWhatsThis =
tr("This will send your Clipboard content to the channel you are currently in.", "Global Shortcut");
gsToggleTalkingUI =
new GlobalShortcut(this, GlobalShortcutType::ToggleTalkingUI, tr("Toggle TalkingUI", "Global shortcut"));
gsToggleTalkingUI->setObjectName(QLatin1String("gsToggleTalkingUI"));
gsToggleTalkingUI->qsWhatsThis = tr("Toggles the visibility of the TalkingUI.", "Global Shortcut");
gsToggleSearch =
new GlobalShortcut(this, GlobalShortcutType::ToggleSearch, tr("Toggle search dialog", "Global Shortcut"));
gsToggleSearch->setObjectName(QLatin1String("gsToggleSearch"));
gsToggleSearch->qsWhatsThis =
tr("This will open or close the search dialog depending on whether it is currently opened already");
gsServerConnect =
new GlobalShortcut(this, GlobalShortcutType::ServerConnect, tr("Connect to a server", "Global Shortcut"));
gsServerConnect->setObjectName(QLatin1String("gsServerConnect"));
gsServerConnect->qsWhatsThis = tr("This will open the server connection dialog", "Global Shortcut");
gsServerDisconnect =
new GlobalShortcut(this, GlobalShortcutType::ServerDisconnect, tr("Disconnect from server", "Global Shortcut"));
gsServerDisconnect->setObjectName(QLatin1String("gsServerDisconnect"));
gsServerDisconnect->qsWhatsThis = tr("This will disconnect you from the server", "Global Shortcut");
gsServerInformation = new GlobalShortcut(this, GlobalShortcutType::ServerInformation,
tr("Open server information", "Global Shortcut"));
gsServerInformation->setObjectName(QLatin1String("gsServerInformation"));
gsServerInformation->qsWhatsThis = tr("This will show information about the server connection", "Global Shortcut");
gsServerTokens =
new GlobalShortcut(this, GlobalShortcutType::ServerTokens, tr("Open server tokens", "Global Shortcut"));
gsServerTokens->setObjectName(QLatin1String("gsServerTokens"));
gsServerTokens->qsWhatsThis = tr("This will open the server tokens dialog", "Global Shortcut");
gsServerUserList =
new GlobalShortcut(this, GlobalShortcutType::ServerUserList, tr("Open server user list", "Global Shortcut"));
gsServerUserList->setObjectName(QLatin1String("gsServerUserList"));
gsServerUserList->qsWhatsThis = tr("This will open the server user list dialog", "Global Shortcut");
gsServerBanList =
new GlobalShortcut(this, GlobalShortcutType::ServerBanList, tr("Open server ban list", "Global Shortcut"));
gsServerBanList->setObjectName(QLatin1String("gsServerBanList"));
gsServerBanList->qsWhatsThis = tr("This will open the server ban list dialog", "Global Shortcut");
gsSelfPrioritySpeaker = new GlobalShortcut(this, GlobalShortcutType::SelfPrioritySpeaker,
tr("Toggle priority speaker", "Global Shortcut"));
gsSelfPrioritySpeaker->setObjectName(QLatin1String("gsSelfPrioritySpeaker"));
gsSelfPrioritySpeaker->qsWhatsThis = tr("This will enable/disable the priority speaker", "Global Shortcut");
gsRecording =
new GlobalShortcut(this, GlobalShortcutType::Recording, tr("Open recording dialog", "Global Shortcut"));
gsRecording->setObjectName(QLatin1String("gsRecording"));
gsRecording->qsWhatsThis = tr("This will open the recording dialog");
gsSelfComment = new GlobalShortcut(this, GlobalShortcutType::SelfComment, tr("Change comment", "Global Shortcut"));
gsSelfComment->setObjectName(QLatin1String("gsSelfComment"));
gsSelfComment->qsWhatsThis = tr("This will open the change comment dialog");
gsServerTexture =
new GlobalShortcut(this, GlobalShortcutType::ServerTexture, tr("Change avatar", "Global Shortcut"));
gsServerTexture->setObjectName(QLatin1String("gsServerTexture"));
gsServerTexture->qsWhatsThis = tr("This will open your file explorer to change your avatar image on this server");
gsServerTextureRemove =
new GlobalShortcut(this, GlobalShortcutType::ServerTextureRemove, tr("Remove avatar", "Global Shortcut"));
gsServerTextureRemove->setObjectName(QLatin1String("gsServerTextureRemove"));
gsServerTextureRemove->qsWhatsThis = tr("This will reset your avatar on the server");
gsSelfRegister =
new GlobalShortcut(this, GlobalShortcutType::SelfRegister, tr("Register on the server", "Global Shortcut"));
gsSelfRegister->setObjectName(QLatin1String("gsSelfRegister"));
gsSelfRegister->qsWhatsThis = tr("This will register you on the server");
gsAudioStats = new GlobalShortcut(this, GlobalShortcutType::AudioStats, tr("Audio statistics", "Global Shortcut"));
gsAudioStats->setObjectName(QLatin1String("gsAudioStats"));
gsAudioStats->qsWhatsThis = tr("This will open the audio statistics dialog");
gsConfigDialog = new GlobalShortcut(this, GlobalShortcutType::ConfigDialog, tr("Open settings", "Global Shortcut"));
gsConfigDialog->setObjectName(QLatin1String("gsConfigDialog"));
gsConfigDialog->qsWhatsThis = tr("This will open the settings dialog");
gsAudioWizard =
new GlobalShortcut(this, GlobalShortcutType::AudioWizard, tr("Start audio wizard", "Global Shortcut"));
gsAudioWizard->setObjectName(QLatin1String("gsAudioWizard"));
gsAudioWizard->qsWhatsThis = tr("This will open the audio wizard dialog");
gsConfigCert =
new GlobalShortcut(this, GlobalShortcutType::ConfigCert, tr("Start certificate wizard", "Global Shortcut"));
gsConfigCert->setObjectName(QLatin1String("gsConfigCert"));
gsConfigCert->qsWhatsThis = tr("This will open the certificate wizard dialog");
gsAudioTTS = new GlobalShortcut(this, GlobalShortcutType::AudioTTS, tr("Toggle text to speech", "Global Shortcut"));
gsAudioTTS->setObjectName(QLatin1String("gsAudioTTS"));
gsAudioTTS->qsWhatsThis = tr("This will enable/disable the text to speech");
gsHelpAbout = new GlobalShortcut(this, GlobalShortcutType::HelpAbout, tr("Open about dialog", "Global Shortcut"));
gsHelpAbout->setObjectName(QLatin1String("gsHelpAbout"));
gsHelpAbout->qsWhatsThis = tr("This will open the about dialog");
gsHelpAboutQt =
new GlobalShortcut(this, GlobalShortcutType::HelpAboutQt, tr("Open about Qt dialog", "Global Shortcut"));
gsHelpAboutQt->setObjectName(QLatin1String("gsHelpAboutQt"));
gsHelpAboutQt->qsWhatsThis = tr("This will open the about Qt dialog");
gsHelpVersionCheck =
new GlobalShortcut(this, GlobalShortcutType::HelpVersionCheck, tr("Check for update", "Global Shortcut"));
gsHelpVersionCheck->setObjectName(QLatin1String("gsHelpVersionCheck"));
gsHelpVersionCheck->qsWhatsThis = tr("This will check if mumble is up to date");
#ifndef Q_OS_MAC
qstiIcon->show();
#endif
}
void MainWindow::setupGui() {
updateWindowTitle();
setCentralWidget(qtvUsers);
setAcceptDrops(true);
#ifdef Q_OS_MAC
QMenu *qmWindow = new QMenu(tr("&Window"), this);
menubar->insertMenu(qmHelp->menuAction(), qmWindow);
qmWindow->addAction(tr("Minimize"), this, SLOT(showMinimized()), QKeySequence(tr("Ctrl+M")));
qtvUsers->setAttribute(Qt::WA_MacShowFocusRect, false);
qteChat->setAttribute(Qt::WA_MacShowFocusRect, false);
qteChat->setFrameShape(QFrame::NoFrame);
qteLog->setFrameStyle(QFrame::NoFrame);
#endif
LogDocument *ld = new LogDocument(qteLog);
qteLog->setDocument(ld);
qteLog->document()->setMaximumBlockCount(Global::get().s.iMaxLogBlocks);
qteLog->document()->setDefaultStyleSheet(qApp->styleSheet());
pmModel = new UserModel(qtvUsers);
qtvUsers->setModel(pmModel);
qtvUsers->setRowHidden(0, QModelIndex(), true);
qtvUsers->ensurePolished();
QObject::connect(this, &MainWindow::userAddedChannelListener, pmModel, &UserModel::addChannelListener);
QObject::connect(
this, &MainWindow::userRemovedChannelListener, pmModel,
static_cast< void (UserModel::*)(const ClientUser *, const Channel *) >(&UserModel::removeChannelListener));
QObject::connect(Global::get().channelListenerManager.get(), &ChannelListenerManager::localVolumeAdjustmentsChanged,
pmModel, &UserModel::on_channelListenerLocalVolumeAdjustmentChanged);
// connect slots to PluginManager
QObject::connect(pmModel, &UserModel::userAdded, Global::get().pluginManager, &PluginManager::on_userAdded);
QObject::connect(pmModel, &UserModel::userRemoved, Global::get().pluginManager, &PluginManager::on_userRemoved);
QObject::connect(pmModel, &UserModel::channelAdded, Global::get().pluginManager, &PluginManager::on_channelAdded);
QObject::connect(pmModel, &UserModel::channelRemoved, Global::get().pluginManager,
&PluginManager::on_channelRemoved);
QObject::connect(pmModel, &UserModel::channelRenamed, Global::get().pluginManager,
&PluginManager::on_channelRenamed);
qaAudioMute->setChecked(Global::get().s.bMute);
qaAudioDeaf->setChecked(Global::get().s.bDeaf);
updateAudioToolTips();
#ifdef USE_NO_TTS
qaAudioTTS->setChecked(false);
qaAudioTTS->setDisabled(true);
#else
qaAudioTTS->setChecked(Global::get().s.bTTS);
#endif
qaFilterToggle->setChecked(Global::get().s.bFilterActive);
on_qaFilterToggle_triggered();
qaHelpWhatsThis->setShortcuts(QKeySequence::WhatsThis);
qaConfigMinimal->setChecked(Global::get().s.bMinimalView);
qaConfigHideFrame->setChecked(Global::get().s.bHideFrame);
connect(gsResetAudio, SIGNAL(down(QVariant)), qaAudioReset, SLOT(trigger()));
connect(gsUnlink, SIGNAL(down(QVariant)), qaAudioUnlink, SLOT(trigger()));
connect(gsMinimal, SIGNAL(down(QVariant)), qaConfigMinimal, SLOT(trigger()));
dtbLogDockTitle = new DockTitleBar();
qdwLog->setTitleBarWidget(dtbLogDockTitle);
foreach (QWidget *w, qdwLog->findChildren< QWidget * >()) {
w->installEventFilter(dtbLogDockTitle);
w->setMouseTracking(true);
}
dtbChatDockTitle = new DockTitleBar();
qdwChat->setTitleBarWidget(dtbChatDockTitle);
qdwChat->installEventFilter(dtbChatDockTitle);
qteChat->setDefaultText(tr("<center>Not connected</center>"), true);
qteChat->setEnabled(false);
QWidget *dummyTitlebar = new QWidget(qdwMinimalViewNote);
qdwMinimalViewNote->setTitleBarWidget(dummyTitlebar);
setShowDockTitleBars((Global::get().s.wlWindowLayout == Settings::LayoutCustom) && !Global::get().s.bLockLayout);
#ifdef Q_OS_MAC
// Workaround for QTBUG-3116 -- using a unified toolbar on Mac OS X
// and using restoreGeometry before the window has updated its frameStrut
// causes the MainWindow to jump around on screen on launch. Workaround
// is to call show() to update the frameStrut and set the windowOpacity to
// 0 to hide any graphical glitches that occur when we add stuff to the
// window.
setWindowOpacity(0.0f);
show();
#endif
connect(qtvUsers->selectionModel(), SIGNAL(currentChanged(const QModelIndex &, const QModelIndex &)),
SLOT(qtvUserCurrentChanged(const QModelIndex &, const QModelIndex &)));
// QtCreator and uic.exe do not allow adding arbitrary widgets
// such as a MUComboBox to a QToolbar, even though they are supported.
qcbTransmitMode = new MUComboBox(qtIconToolbar);
qcbTransmitMode->setObjectName(QLatin1String("qcbTransmitMode"));
qcbTransmitMode->addItem(tr("Continuous"));
qcbTransmitMode->addItem(tr("Voice Activity"));
qcbTransmitMode->addItem(tr("Push-to-Talk"));
qaTransmitModeSeparator = qtIconToolbar->insertSeparator(qaConfigDialog);
qaTransmitMode = qtIconToolbar->insertWidget(qaTransmitModeSeparator, qcbTransmitMode);
connect(qcbTransmitMode, SIGNAL(activated(int)), this, SLOT(qcbTransmitMode_activated(int)));
updateTransmitModeComboBox(Global::get().s.atTransmit);
#ifdef Q_OS_WIN
setupView(false);
#endif
if (Global::get().s.bMinimalView && !Global::get().s.qbaMinimalViewGeometry.isNull())
restoreGeometry(Global::get().s.qbaMinimalViewGeometry);
else if (!Global::get().s.bMinimalView && !Global::get().s.qbaMainWindowGeometry.isNull())
restoreGeometry(Global::get().s.qbaMainWindowGeometry);
if (Global::get().s.bMinimalView && !Global::get().s.qbaMinimalViewState.isNull())
restoreState(Global::get().s.qbaMinimalViewState);
else if (!Global::get().s.bMinimalView && !Global::get().s.qbaMainWindowState.isNull())
restoreState(Global::get().s.qbaMainWindowState);
setupView(false);
qmTray = new QMenu(this);
connect(qmTray, SIGNAL(aboutToShow()), this, SLOT(trayAboutToShow()));
trayAboutToShow();
qstiIcon->setContextMenu(qmTray);
updateTrayIcon();
#ifdef Q_OS_MAC
setWindowOpacity(1.0f);
#endif
}
void MainWindow::updateWindowTitle() {
QString title;
if (Global::get().s.bMinimalView) {
title = tr("Mumble - Minimal View");
} else {
title = tr("Mumble");
}
if (!Global::get().windowTitlePostfix.isEmpty()) {
title += QString::fromLatin1(" | %1").arg(Global::get().windowTitlePostfix);
}
setWindowTitle(title);
}
void MainWindow::updateToolbar() {
bool layoutIsCustom = Global::get().s.wlWindowLayout == Settings::LayoutCustom;
qtIconToolbar->setMovable(layoutIsCustom && !Global::get().s.bLockLayout);
// Update the toolbar so the movable flag takes effect.
if (layoutIsCustom) {
// Update the toolbar, but keep it in place.
addToolBar(toolBarArea(qtIconToolbar), qtIconToolbar);
} else {
// Update the toolbar, and ensure it is at the top of the window.
addToolBar(Qt::TopToolBarArea, qtIconToolbar);
}
}
// Sets whether or not to show the title bars on the MainWindow's
// dock widgets.
void MainWindow::setShowDockTitleBars(bool doShow) {
dtbLogDockTitle->setEnabled(doShow);
dtbChatDockTitle->setEnabled(doShow);
}
MainWindow::~MainWindow() {
delete qwPTTButtonWidget;
delete qdwLog->titleBarWidget();
delete pmModel;
delete qtvUsers;
delete Channel::get(Channel::ROOT_ID);
}
void MainWindow::msgBox(QString msg) {
MessageBoxEvent *mbe = new MessageBoxEvent(msg);
QApplication::postEvent(this, mbe);
}
#ifdef Q_OS_WIN
bool MainWindow::nativeEvent(const QByteArray &, void *message, long *) {
MSG *msg = reinterpret_cast< MSG * >(message);
if (msg->message == WM_DEVICECHANGE && msg->wParam == DBT_DEVNODES_CHANGED)
uiNewHardware++;
return false;
}
#endif
void MainWindow::closeEvent(QCloseEvent *e) {
ServerHandlerPtr sh = Global::get().sh;
QuitBehavior quitBehavior = Global::get().s.quitBehavior;
const bool alwaysAsk = quitBehavior == QuitBehavior::ALWAYS_ASK;
const bool askDueToConnected = sh && sh->isRunning() && quitBehavior == QuitBehavior::ASK_WHEN_CONNECTED;
const bool alwaysMinimize = quitBehavior == QuitBehavior::ALWAYS_MINIMIZE;
const bool minimizeDueToConnected = sh && sh->isRunning() && quitBehavior == QuitBehavior::MINIMIZE_WHEN_CONNECTED;
if (!forceQuit && (alwaysAsk || askDueToConnected)) {
#ifndef Q_OS_MAC
QMessageBox mb(QMessageBox::Warning, QLatin1String("Mumble"),
tr("Are you sure you want to close Mumble? Perhaps you prefer to minimize it instead?"),
QMessageBox::NoButton, this);
QCheckBox *qcbRemember = new QCheckBox(tr("Remember this setting"));
QPushButton *qpbClose = mb.addButton(tr("Close"), QMessageBox::YesRole);
QPushButton *qpbMinimize = mb.addButton(tr("Minimize"), QMessageBox::NoRole);
QPushButton *qpbCancel = mb.addButton(tr("Cancel"), QMessageBox::RejectRole);
mb.setDefaultButton(qpbClose);
mb.setEscapeButton(qpbCancel);
mb.setCheckBox(qcbRemember);
mb.exec();
if (mb.clickedButton() == qpbMinimize) {
showMinimized();
e->ignore();
// If checkbox is checked and not connected, always minimize
// If checkbox is checked and connected, always minimize when connected
if (qcbRemember->isChecked() && !(sh && sh->isRunning())) {
Global::get().s.quitBehavior = QuitBehavior::ALWAYS_MINIMIZE;
} else if (qcbRemember->isChecked()) {
Global::get().s.quitBehavior = QuitBehavior::MINIMIZE_WHEN_CONNECTED;
}
return;
} else if (mb.clickedButton() == qpbCancel) {
e->ignore();
return;
}
// If checkbox is checked, quit always
if (qcbRemember->isChecked()) {
Global::get().s.quitBehavior = QuitBehavior::ALWAYS_QUIT;
}
#endif
} else if (!forceQuit && (alwaysMinimize || minimizeDueToConnected)) {
showMinimized();
e->ignore();
return;
}
sh.reset();
if (Global::get().s.bMinimalView) {
Global::get().s.qbaMinimalViewGeometry = saveGeometry();
Global::get().s.qbaMinimalViewState = saveState();
} else {
Global::get().s.qbaMainWindowGeometry = saveGeometry();
Global::get().s.qbaMainWindowState = saveState();
Global::get().s.qbaHeaderState = qtvUsers->header()->saveState();
}
if (Global::get().talkingUI && Global::get().talkingUI->isVisible()) {
// Save the TalkingUI's position if it is visible
// Note that we explicitly don't save the whole geometry as the TalkingUI's size
// is a flexible thing that'll adjust automatically anyways.
Global::get().s.qpTalkingUI_Position = Global::get().talkingUI->pos();
}
if (m_searchDialog) {
// Save position of search dialog
Global::get().s.searchDialogPosition = { m_searchDialog->x(), m_searchDialog->y() };
}
if (qwPTTButtonWidget) {
qwPTTButtonWidget->close();
qwPTTButtonWidget->deleteLater();
qwPTTButtonWidget = nullptr;
}
Global::get().bQuit = true;
QMainWindow::closeEvent(e);
qApp->exit(restartOnQuit ? MUMBLE_EXIT_CODE_RESTART : 0);
}
void MainWindow::hideEvent(QHideEvent *e) {
#ifdef USE_OVERLAY
if (Global::get().ocIntercept) {
QMetaObject::invokeMethod(Global::get().ocIntercept, "hideGui", Qt::QueuedConnection);
e->ignore();
return;
}
#endif
#ifndef Q_OS_MAC
# ifdef Q_OS_UNIX
if (!qApp->activeModalWidget() && !qApp->activePopupWidget())
# endif
if (Global::get().s.bHideInTray && qstiIcon->isSystemTrayAvailable() && e->spontaneous())
QMetaObject::invokeMethod(this, "hide", Qt::QueuedConnection);
#endif
QMainWindow::hideEvent(e);
}
void MainWindow::showEvent(QShowEvent *e) {
#ifndef Q_OS_MAC
# ifdef Q_OS_UNIX
if (!qApp->activeModalWidget() && !qApp->activePopupWidget())
# endif
if (Global::get().s.bHideInTray && qstiIcon->isSystemTrayAvailable() && e->spontaneous())
QMetaObject::invokeMethod(this, "show", Qt::QueuedConnection);
#endif
QMainWindow::showEvent(e);
}
void MainWindow::changeEvent(QEvent *e) {
QWidget::changeEvent(e);
#ifdef Q_OS_MAC
// On modern macOS/Qt combinations, the code below causes Mumble's
// MainWindow to not be interactive after returning from being minimized.
// (See issue mumble-voip/mumble#2171)
// So, let's not do it on macOS.
#else
if (isMinimized() && qstiIcon->isSystemTrayAvailable() && Global::get().s.bHideInTray) {
// Workaround https://forum.qt.io/topic/4327/minimizing-application-to-tray/24
QTimer::singleShot(0, this, SLOT(hide()));
}
#endif
}
void MainWindow::keyPressEvent(QKeyEvent *e) {
// Pressing F6 switches between the main
// window's main widgets, making it easier
// to navigate Mumble's MainWindow with only
// a keyboard.
if (e->key() == Qt::Key_F6) {
focusNextMainWidget();
} else {
QMainWindow::keyPressEvent(e);
}
}
/// focusNextMainWidget switches the focus to the next main
/// widget of the MainWindow.
///
/// This is used to implement behavior where pressing F6
/// switches between major elements of an application.
/// This behavior is for example seen in Windows's (File) Explorer.
///
/// The main widgets are qteLog (the log view), qteChat (chat input bar)
/// and qtvUsers (users tree view).
void MainWindow::focusNextMainWidget() {
QWidget *mainFocusWidgets[] = {
qteLog,
qteChat,
qtvUsers,
};
const int numMainFocusWidgets = sizeof(mainFocusWidgets) / sizeof(mainFocusWidgets[0]);
int currentMainFocusWidgetIndex = -1;
QWidget *w = focusWidget();
for (int i = 0; i < numMainFocusWidgets; i++) {
QWidget *mainFocusWidget = mainFocusWidgets[i];
if (w == mainFocusWidget || w->isAncestorOf(mainFocusWidget)) {
currentMainFocusWidgetIndex = i;
break;
}
}
Q_ASSERT(currentMainFocusWidgetIndex != -1);
int nextMainFocusWidgetIndex = (currentMainFocusWidgetIndex + 1) % numMainFocusWidgets;
QWidget *nextMainFocusWidget = mainFocusWidgets[nextMainFocusWidgetIndex];
nextMainFocusWidget->setFocus();
}
void MainWindow::updateAudioToolTips() {
if (Global::get().s.bMute)
qaAudioMute->setToolTip(tr("Unmute yourself"));
else
qaAudioMute->setToolTip(tr("Mute yourself"));
if (Global::get().s.bDeaf)
qaAudioDeaf->setToolTip(tr("Undeafen yourself"));
else
qaAudioDeaf->setToolTip(tr("Deafen yourself"));
}
void MainWindow::updateTrayIcon() {
ClientUser *p = ClientUser::get(Global::get().uiSession);
if (Global::get().s.bDeaf) {
qstiIcon->setIcon(qiIconDeafSelf);
} else if (p && p->bDeaf) {
qstiIcon->setIcon(qiIconDeafServer);
} else if (Global::get().s.bMute) {
qstiIcon->setIcon(qiIconMuteSelf);
} else if (p && p->bMute) {
qstiIcon->setIcon(qiIconMuteServer);
} else if (p && p->bSuppress) {
qstiIcon->setIcon(qiIconMuteSuppressed);
} else if (Global::get().s.bStateInTray && Global::get().bPushToMute) {
qstiIcon->setIcon(qiIconMutePushToMute);
} else if (p && Global::get().s.bStateInTray) {
switch (p->tsState) {
case Settings::Talking:
case Settings::MutedTalking:
qstiIcon->setIcon(qiTalkingOn);
break;
case Settings::Whispering:
qstiIcon->setIcon(qiTalkingWhisper);
break;
case Settings::Shouting:
qstiIcon->setIcon(qiTalkingShout);
break;
case Settings::Passive:
default:
qstiIcon->setIcon(qiTalkingOff);
break;
}
} else {
qstiIcon->setIcon(qiIcon);
}
}
void MainWindow::updateUserModel() {
UserModel *um = static_cast< UserModel * >(qtvUsers->model());
um->forceVisualUpdate();
}
void MainWindow::updateTransmitModeComboBox(Settings::AudioTransmit newMode) {
switch (newMode) {
case Settings::Continuous:
qcbTransmitMode->setCurrentIndex(0);
return;
case Settings::VAD:
qcbTransmitMode->setCurrentIndex(1);
return;
case Settings::PushToTalk:
qcbTransmitMode->setCurrentIndex(2);
return;
}
}
QMenu *MainWindow::createPopupMenu() {
if ((Global::get().s.wlWindowLayout == Settings::LayoutCustom) && !Global::get().s.bLockLayout) {
// We have to explicitly create a menu here instead of simply referring to QMainWindow::createPopupMenu as we
// don't want a toggle for showing/hiding the minimal view note (which is also present as a QDockWidget). Thus,
// we have to explicitly add only those widgets that we really want to be toggleable.
QMenu *menu = new QMenu(this);
menu->addAction(qdwChat->toggleViewAction());
menu->addAction(qdwLog->toggleViewAction());
menu->addAction(qtIconToolbar->toggleViewAction());
return menu;
}
return nullptr;
}
Channel *MainWindow::getContextMenuChannel() {
if (cContextChannel)
return cContextChannel.data();
return nullptr;
}
ClientUser *MainWindow::getContextMenuUser() {
if (cuContextUser)
return cuContextUser.data();
return nullptr;
}
ContextMenuTarget MainWindow::getContextMenuTargets() {
ContextMenuTarget target;
if (Global::get().uiSession != 0) {
QModelIndex idx;
if (!qpContextPosition.isNull())
idx = qtvUsers->indexAt(qpContextPosition);
if (!idx.isValid())
idx = qtvUsers->currentIndex();
target.user = pmModel->getUser(idx);
target.channel = pmModel->getChannel(idx);
if (!target.user)
target.user = getContextMenuUser();
if (!target.channel)
target.channel = getContextMenuChannel();
}
cuContextUser = target.user;
cContextChannel = target.channel;
qpContextPosition = QPoint();
return target;
}
bool MainWindow::handleSpecialContextMenu(const QUrl &url, const QPoint &pos_, bool focus) {
// This method abuses QUrls for internal data serialization
// The protocol, host and path parts of the URL may contain
// special values which are only parsable by this method.
if (url.scheme() == QString::fromLatin1("clientid")) {
bool ok = false;
QString maybeUserHash(url.host());
if (maybeUserHash.length() == 40) {
ClientUser *cu = pmModel->getUser(maybeUserHash);
if (cu) {
cuContextUser = cu;
ok = true;
}
} else {
// We expect the host part of the URL to contain the user id in the format
// id.<id>
// where <id> is the user id as integer. This is necessary, because QUrl parses
// plain integers in the host field as IP addresses
QByteArray qbaServerDigest = QByteArray::fromBase64(url.path().remove(0, 1).toLatin1());
QString id = url.host().split(".").value(1, "-1");
cuContextUser = ClientUser::get(id.toUInt(&ok, 10));
ServerHandlerPtr sh = Global::get().sh;
ok = ok && sh && (qbaServerDigest == sh->qbaDigest);
}
if (ok && cuContextUser) {
if (focus) {
qtvUsers->setCurrentIndex(pmModel->index(cuContextUser.data()));
qteChat->setFocus();
} else {
qpContextPosition = QPoint();
qmUser->exec(pos_, nullptr);
}
}
cuContextUser.clear();
} else if (url.scheme() == QString::fromLatin1("channelid")) {
// We expect the host part of the URL to contain the channel id in the format
// id.<id>
// where <id> is the channel id as integer. This is necessary, because QUrl parses
// plain integers in the host field as IP addresses
bool ok;
QByteArray qbaServerDigest = QByteArray::fromBase64(url.path().remove(0, 1).toLatin1());
QString id = url.host().split(".").value(1, "-1");
cContextChannel = Channel::get(id.toUInt(&ok, 10));
ServerHandlerPtr sh = Global::get().sh;
ok = ok && sh && (qbaServerDigest == sh->qbaDigest);
if (ok) {
if (focus) {
qtvUsers->setCurrentIndex(pmModel->index(cContextChannel.data()));
qteChat->setFocus();
} else {
qpContextPosition = QPoint();
qmChannel->exec(pos_, nullptr);
}
}
cContextChannel.clear();
} else {
return false;
}
return true;
}
void MainWindow::on_qtvUsers_customContextMenuRequested(const QPoint &mpos, bool usePositionForGettingContext) {
QModelIndex idx = qtvUsers->indexAt(mpos);
if (!idx.isValid() || !usePositionForGettingContext) {
idx = qtvUsers->currentIndex();
} else {
qtvUsers->setCurrentIndex(idx);
}
ClientUser *p = pmModel->getUser(idx);
Channel *channel = pmModel->getChannel(idx);
qpContextPosition = mpos;
if (pmModel->isChannelListener(idx)) {
// Have a separate context menu for listeners
QModelIndex parent = idx.parent();
if (parent.isValid()) {
// Find the channel in which the action was triggered and set it
// in order to be able to obtain it in the action itself
cContextChannel = pmModel->getChannel(parent);
}
cuContextUser.clear();
qmListener->exec(qtvUsers->mapToGlobal(mpos), nullptr);
cuContextUser.clear();
cContextChannel.clear();
} else {
if (p) {
cuContextUser.clear();
if (!usePositionForGettingContext) {
cuContextUser = p;
}
qmUser->exec(qtvUsers->mapToGlobal(mpos), nullptr);
cuContextUser.clear();
} else {
cContextChannel.clear();
if (!usePositionForGettingContext && channel) {
cContextChannel = channel;
}
qmChannel->exec(qtvUsers->mapToGlobal(mpos), nullptr);
cContextChannel.clear();
}
}
qpContextPosition = QPoint();
}
void MainWindow::on_qteLog_customContextMenuRequested(const QPoint &mpos) {
QString link = qteLog->anchorAt(mpos);
if (!link.isEmpty()) {
QUrl l(link);
if (handleSpecialContextMenu(l, qteLog->mapToGlobal(mpos)))
return;
}
QPoint contentPosition =
QPoint(QApplication::isRightToLeft()
? (qteLog->horizontalScrollBar()->maximum() - qteLog->horizontalScrollBar()->value())
: qteLog->horizontalScrollBar()->value(),
qteLog->verticalScrollBar()->value());
QMenu *menu = qteLog->createStandardContextMenu(mpos + contentPosition);
QTextCursor cursor = qteLog->cursorForPosition(mpos);
QTextCharFormat fmt = cursor.charFormat();
// Work around imprecise cursor image identification
// Apparently, the cursor is shifted half the characters width to the right on the image
// element. This is in contrast to hyperlinks for example, which have correct edge detection.
// For the image, we get the right half (plus the left half of the next character) for the
// image, and have to move the cursor forward to also detect on the left half of the image
// (plus the right half of the previous character).
// It is unclear why we have to use NextCharacter instead of PreviousCharacter.
if (fmt.objectType() == QTextFormat::NoObject) {
cursor.movePosition(QTextCursor::NextCharacter);
fmt = cursor.charFormat();
}
if (cursor.charFormat().isImageFormat()) {
menu->addSeparator();
menu->addAction(tr("Save Image As..."), this, SLOT(saveImageAs(void)));
qtcSaveImageCursor = cursor;
}
menu->addSeparator();
menu->addAction(tr("Clear"), qteLog, SLOT(clear(void)));
menu->exec(qteLog->mapToGlobal(mpos));
delete menu;
}
void MainWindow::saveImageAs() {
QDateTime now = QDateTime::currentDateTime();
QString defaultFname =
QString::fromLatin1("Mumble-%1.jpg").arg(now.toString(QString::fromLatin1("yyyy-MM-dd-HHmmss")));
QString fname = QFileDialog::getSaveFileName(this, tr("Save Image File"), getImagePath(defaultFname),
tr("Images (*.png *.jpg *.jpeg)"));
if (fname.isNull()) {
return;
}
QString resName = qtcSaveImageCursor.charFormat().toImageFormat().name();
QVariant res = qteLog->document()->resource(QTextDocument::ImageResource, resName);
QImage img = res.value< QImage >();
bool ok = img.save(fname);
if (!ok) {
// In case fname did not contain a file extension, try saving with an
// explicit format.
ok = img.save(fname, "PNG");
}
updateImagePath(fname);
if (!ok) {
Global::get().l->log(Log::Warning, tr("Could not save image: %1").arg(fname.toHtmlEscaped()));
}
}
QString MainWindow::getImagePath(QString filename) const {
if (Global::get().s.qsImagePath.isEmpty() || !QDir(Global::get().s.qsImagePath).exists()) {
Global::get().s.qsImagePath = QStandardPaths::writableLocation(QStandardPaths::PicturesLocation);
}
if (filename.isEmpty()) {
return Global::get().s.qsImagePath;
}
return Global::get().s.qsImagePath + QDir::separator() + filename;
}
void MainWindow::updateImagePath(QString filepath) const {
QFileInfo fi(filepath);
Global::get().s.qsImagePath = fi.absolutePath();
}
void MainWindow::setTransmissionMode(Settings::AudioTransmit mode) {
if (Global::get().s.atTransmit != mode) {
Global::get().s.atTransmit = mode;
switch (mode) {
case Settings::Continuous:
Global::get().l->log(Log::Information, tr("Transmit Mode set to Continuous"));
break;
case Settings::VAD:
Global::get().l->log(Log::Information, tr("Transmit Mode set to Voice Activity"));
break;
case Settings::PushToTalk:
Global::get().l->log(Log::Information, tr("Transmit Mode set to Push-to-Talk"));
break;
}
emit transmissionModeChanged(mode);
}
}
void MainWindow::on_qaSearch_triggered() {
toggleSearchDialogVisibility();
}
void MainWindow::toggleSearchDialogVisibility() {
if (!m_searchDialog) {
m_searchDialog = new Search::SearchDialog(this);
QPoint position = Global::get().s.searchDialogPosition;
if (position == Settings::UNSPECIFIED_POSITION) {
// Get MainWindow's position on screen
position = mapToGlobal(QPoint(0, 0));
}
if (Mumble::QtUtils::positionIsOnScreen(position)) {
// Move the search dialog to the same origin as the MainWindow is
m_searchDialog->move(position);
}
}
m_searchDialog->setVisible(!m_searchDialog->isVisible());
}
void MainWindow::enableRecording(bool recordingAllowed) {
qaRecording->setEnabled(recordingAllowed);
Global::get().recordingAllowed = recordingAllowed;
if (!recordingAllowed && voiceRecorderDialog) {
voiceRecorderDialog->reject();
}
}
static void recreateServerHandler() {
ServerHandlerPtr sh = Global::get().sh;
if (sh && sh->isRunning()) {
Global::get().mw->on_qaServerDisconnect_triggered();
sh->disconnect();
sh->wait();
QCoreApplication::instance()->processEvents();
}
Global::get().sh.reset();
while (sh && !sh.unique())
QThread::yieldCurrentThread();
sh.reset();
sh = ServerHandlerPtr(new ServerHandler());
sh->moveToThread(sh.get());
Global::get().sh = sh;
Global::get().mw->connect(sh.get(), SIGNAL(connected()), Global::get().mw, SLOT(serverConnected()));
Global::get().mw->connect(sh.get(), SIGNAL(disconnected(QAbstractSocket::SocketError, QString)), Global::get().mw,
SLOT(serverDisconnected(QAbstractSocket::SocketError, QString)));
Global::get().mw->connect(sh.get(), SIGNAL(error(QAbstractSocket::SocketError, QString)), Global::get().mw,
SLOT(resolverError(QAbstractSocket::SocketError, QString)));
QObject::connect(sh.get(), &ServerHandler::disconnected, Global::get().talkingUI,
&TalkingUI::on_serverDisconnected);
// We have to use direct connections for these here as the PluginManager must be able to access the connection's ID
// and in order for that to be possible the (dis)connection process must not proceed in the background.
Global::get().pluginManager->connect(sh.get(), &ServerHandler::connected, Global::get().pluginManager,
&PluginManager::on_serverConnected, Qt::DirectConnection);
// We connect the plugin manager to "aboutToDisconnect" instead of "disconnect" in order for the slot to be
// guaranteed to be completed *before* the actual disconnect logic (e.g. MainWindow::serverDisconnected) kicks in.
// In order for that to work it is ESSENTIAL to use a DIRECT CONNECTION!
Global::get().pluginManager->connect(sh.get(), &ServerHandler::aboutToDisconnect, Global::get().pluginManager,
&PluginManager::on_serverDisconnected, Qt::DirectConnection);
}
void MainWindow::openUrl(const QUrl &url) {
Global::get().l->log(Log::Information,
tr("Opening URL %1").arg(url.toString(QUrl::RemovePassword).toHtmlEscaped()));
if (url.scheme() == QLatin1String("file")) {
QFile f(url.toLocalFile());
if (!f.exists() || !f.open(QIODevice::ReadOnly)) {
Global::get().l->log(Log::Warning, tr("File does not exist"));
return;
}
f.close();
try {
Settings newSettings;
newSettings.load(f.fileName());
std::swap(newSettings, Global::get().s);
Global::get().l->log(Log::Warning, tr("Settings merged from file."));
} catch (const std::exception &) {
Global::get().l->log(Log::Warning, tr("Invalid settings file encountered."));
}
return;
}
if (url.scheme() != QLatin1String("mumble")) {
Global::get().l->log(Log::Warning, tr("URL scheme is not 'mumble'"));
return;
}
Version::full_t thisVersion = Version::get();
Version::full_t targetVersion = Version::UNKNOWN;
QUrlQuery query(url);
QString version = query.queryItemValue(QLatin1String("version"));
if (version.size() > 0) {
targetVersion = Version::fromString(version);
if (targetVersion == Version::UNKNOWN) {
// The version format is invalid
Global::get().l->log(Log::Warning,
QObject::tr("The provided URL uses an invalid version format: \"%1\"").arg(version));
return;
}
}
// With no version parameter given assume the link refers to our version
if (targetVersion == Version::UNKNOWN) {
targetVersion = thisVersion;
}
// We can't handle URLs for versions < 1.2.0
const bool isPre_120 = targetVersion < Version::fromComponents(1, 2, 0);
// We also can't handle URLs for versions newer than the running Mumble instance
const bool isFuture = thisVersion < targetVersion;
if (isPre_120 || isFuture) {
Global::get().l->log(
Log::Warning,
tr("This version of Mumble can't handle URLs for Mumble version %1").arg(Version::toString(targetVersion)));
return;
}
QString host = url.host();
unsigned short port = static_cast< unsigned short >(url.port(DEFAULT_MUMBLE_PORT));
QString user = url.userName();
QString pw = url.password();
qsDesiredChannel = url.path();
QString name;
if (query.hasQueryItem(QLatin1String("title")))
name = query.queryItemValue(QLatin1String("title"));
if (Global::get().sh && Global::get().sh->isRunning()) {
QString oHost, oUser, oPw;
unsigned short oport;
Global::get().sh->getConnectionInfo(oHost, oport, oUser, oPw);
ClientUser *p = ClientUser::get(Global::get().uiSession);
if ((user.isEmpty() || (p && p->iId >= 0) || (user == oUser))
&& (host.isEmpty() || ((host == oHost) && (port == oport)))) {
findDesiredChannel();
return;
}
}
Global::get().db->fuzzyMatch(name, user, pw, host, port);
if (user.isEmpty()) {
bool ok;
user = QInputDialog::getText(this, tr("Connecting to %1").arg(url.toString()), tr("Enter username"),
QLineEdit::Normal, Global::get().s.qsUsername, &ok);
if (!ok || user.isEmpty())
return;
Global::get().s.qsUsername = user;
}
if (name.isEmpty())
name = QString::fromLatin1("%1@%2").arg(user).arg(host);
recreateServerHandler();
Global::get().s.qsLastServer = name;
rtLast = MumbleProto::Reject_RejectType_None;
bRetryServer = true;
qaServerDisconnect->setEnabled(true);
Global::get().l->log(Log::Information,
tr("Connecting to server %1.").arg(Log::msgColor(host.toHtmlEscaped(), Log::Server)));
Global::get().sh->setConnectionInfo(host, port, user, pw);
Global::get().sh->start(QThread::TimeCriticalPriority);
}
/**
* This function tries to join a desired channel on connect. It gets called
* directly after server synchronization is completed.
* @see void MainWindow::msgServerSync(const MumbleProto::ServerSync &msg)
*/
void MainWindow::findDesiredChannel() {
bool found = false;
QStringList qlChans = qsDesiredChannel.split(QLatin1String("/"));
Channel *chan = Channel::get(Channel::ROOT_ID);
QString str = QString();
while (chan && qlChans.count() > 0) {
QString elem = qlChans.takeFirst().toLower();
if (elem.isEmpty())
continue;
if (str.isNull())
str = elem;
else
str = str + QLatin1String("/") + elem;
foreach (Channel *c, chan->qlChannels) {
if (c->qsName.toLower() == str) {
str = QString();
found = true;
chan = c;
break;
}
}
}
if (found) {
if (chan != ClientUser::get(Global::get().uiSession)->cChannel) {
Global::get().sh->joinChannel(Global::get().uiSession, chan->iId);
}
qtvUsers->setCurrentIndex(pmModel->index(chan));
} else if (Global::get().uiSession) {
qtvUsers->setCurrentIndex(pmModel->index(ClientUser::get(Global::get().uiSession)->cChannel));
}
updateMenuPermissions();
}
void MainWindow::setOnTop(bool top) {
Qt::WindowFlags wf = windowFlags();
if (wf.testFlag(Qt::WindowStaysOnTopHint) != top) {
if (top)
wf |= Qt::WindowStaysOnTopHint;
else
wf &= ~Qt::WindowStaysOnTopHint;
setWindowFlags(wf);
show();
}
}
void MainWindow::setupView(bool toggle_minimize) {
bool showit = !Global::get().s.bMinimalView;
switch (Global::get().s.wlWindowLayout) {
case Settings::LayoutClassic:
removeDockWidget(qdwLog);
addDockWidget(Qt::LeftDockWidgetArea, qdwLog);
qdwLog->show();
splitDockWidget(qdwLog, qdwChat, Qt::Vertical);
qdwChat->show();
break;
case Settings::LayoutStacked:
removeDockWidget(qdwLog);
addDockWidget(Qt::BottomDockWidgetArea, qdwLog);
qdwLog->show();
splitDockWidget(qdwLog, qdwChat, Qt::Vertical);
qdwChat->show();
break;
case Settings::LayoutHybrid:
removeDockWidget(qdwLog);
removeDockWidget(qdwChat);
addDockWidget(Qt::LeftDockWidgetArea, qdwLog);
qdwLog->show();
addDockWidget(Qt::BottomDockWidgetArea, qdwChat);
qdwChat->show();
break;
default:
break;
}
updateToolbar();
qteChat->updateGeometry();
QRect geom = frameGeometry();
if (toggle_minimize) {
if (!showit) {
Global::get().s.qbaMainWindowGeometry = saveGeometry();
Global::get().s.qbaMainWindowState = saveState();
Global::get().s.qbaHeaderState = qtvUsers->header()->saveState();
} else {
Global::get().s.qbaMinimalViewGeometry = saveGeometry();
Global::get().s.qbaMinimalViewState = saveState();
}
}
Qt::WindowFlags f = Qt::Window;
if (!showit) {
if (Global::get().s.bHideFrame) {
f |= Qt::FramelessWindowHint;
}
}
if (Global::get().s.aotbAlwaysOnTop == Settings::OnTopAlways
|| (Global::get().s.bMinimalView && Global::get().s.aotbAlwaysOnTop == Settings::OnTopInMinimal)
|| (!Global::get().s.bMinimalView && Global::get().s.aotbAlwaysOnTop == Settings::OnTopInNormal)) {
f |= Qt::WindowStaysOnTopHint;
}
if (!graphicsProxyWidget())
setWindowFlags(f);
if (Global::get().s.bShowContextMenuInMenuBar) {
bool found = false;
foreach (QAction *a, menuBar()->actions()) {
if (a == qmUser->menuAction()) {
found = true;
break;
}
}
if (!found) {
menuBar()->insertMenu(qmConfig->menuAction(), qmUser);
menuBar()->insertMenu(qmConfig->menuAction(), qmChannel);
}
} else {
menuBar()->removeAction(qmUser->menuAction());
menuBar()->removeAction(qmChannel->menuAction());
}
if (Global::get().s.bEnableDeveloperMenu) {
bool found = false;
foreach (QAction *a, menuBar()->actions()) {
if (a == qmDeveloper->menuAction()) {
found = true;
break;
}
}
if (!found) {
menuBar()->insertMenu(qmHelp->menuAction(), qmDeveloper);
}
} else {
menuBar()->removeAction(qmDeveloper->menuAction());
}
if (toggle_minimize) {
if (!showit) {
if (!Global::get().s.qbaMinimalViewGeometry.isNull())
restoreGeometry(Global::get().s.qbaMinimalViewGeometry);
if (!Global::get().s.qbaMinimalViewState.isNull())
restoreState(Global::get().s.qbaMinimalViewState);
} else {
if (!Global::get().s.qbaMainWindowGeometry.isNull())
restoreGeometry(Global::get().s.qbaMainWindowGeometry);
if (!Global::get().s.qbaMainWindowState.isNull())
restoreState(Global::get().s.qbaMainWindowState);
}
} else {
QRect newgeom = frameGeometry();
resize(geometry().width() - newgeom.width() + geom.width(),
geometry().height() - newgeom.height() + geom.height());
move(geom.x(), geom.y());
}
// Explicitly hide UI elements, if we're entering minimal view
// Note that showing them again is handled above via restoreState/restoreGeometry calls
if (!showit) {
qdwLog->setVisible(false);
qdwChat->setVisible(false);
qtIconToolbar->setVisible(false);
}
menuBar()->setVisible(showit);
if (showit) {
qdwMinimalViewNote->hide();
} else if (!Global::get().sh) {
// Show the note, if we're not connected to a server
qdwMinimalViewNote->show();
}
// Display the Transmit Mode Dropdown, if configured to do so, otherwise
// hide it.
if (Global::get().s.bShowTransmitModeComboBox) {
qaTransmitMode->setVisible(true);
qaTransmitModeSeparator->setVisible(true);
} else {
qaTransmitMode->setVisible(false);
qaTransmitModeSeparator->setVisible(false);
}
show();
activateWindow();
// If activated show the PTT window
if (Global::get().s.bShowPTTButtonWindow && Global::get().s.atTransmit == Settings::PushToTalk) {
if (qwPTTButtonWidget) {
qwPTTButtonWidget->show();
} else {
qwPTTButtonWidget = new PTTButtonWidget();
qwPTTButtonWidget->show();
connect(qwPTTButtonWidget, SIGNAL(triggered(bool, QVariant)),
SLOT(on_PushToTalk_triggered(bool, QVariant)));
}
} else {
if (qwPTTButtonWidget) {
qwPTTButtonWidget->deleteLater();
qwPTTButtonWidget = nullptr;
}
}
}
void MainWindow::on_qaServerConnect_triggered(bool autoconnect) {
openServerConnectDialog(autoconnect);
}
void MainWindow::on_Reconnect_timeout() {
if (Global::get().sh->isRunning())
return;
Global::get().l->log(Log::Information, tr("Reconnecting."));
Global::get().sh->start(QThread::TimeCriticalPriority);
}
void MainWindow::on_qmSelf_aboutToShow() {
ClientUser *user = ClientUser::get(Global::get().uiSession);
qaServerTexture->setEnabled(user != nullptr);
qaSelfComment->setEnabled(user != nullptr);
qaServerTextureRemove->setEnabled(user && !user->qbaTextureHash.isEmpty());
qaSelfRegister->setEnabled(user && (user->iId < 0) && !user->qsHash.isEmpty()
&& (Global::get().pPermissions & (ChanACL::SelfRegister | ChanACL::Write)));
if (Global::get().sh && Global::get().sh->m_version >= Version::fromComponents(1, 2, 3)) {
qaSelfPrioritySpeaker->setEnabled(user && Global::get().pPermissions & (ChanACL::Write | ChanACL::MuteDeafen));
qaSelfPrioritySpeaker->setChecked(user && user->bPrioritySpeaker);
} else {
qaSelfPrioritySpeaker->setEnabled(false);
qaSelfPrioritySpeaker->setChecked(false);
}
}
void MainWindow::on_qaSelfComment_triggered() {
openSelfCommentDialog();
}
void MainWindow::on_qaSelfRegister_triggered() {
selfRegister();
}
void MainWindow::qcbTransmitMode_activated(int index) {
switch (index) {
case 0: // Continuous
setTransmissionMode(Settings::Continuous);
break;
case 1: // Voice Activity
setTransmissionMode(Settings::VAD);
break;
case 2: // Push-to-Talk
setTransmissionMode(Settings::PushToTalk);
break;
}
}
void MainWindow::on_qmServer_aboutToShow() {
qmServer->clear();
qmServer->addAction(qaServerConnect);
qmServer->addSeparator();
qmServer->addAction(qaServerDisconnect);
qmServer->addAction(qaServerInformation);
qmServer->addAction(qaSearch);
qmServer->addAction(qaServerTokens);
qmServer->addAction(qaServerUserList);
qmServer->addAction(qaServerBanList);
qmServer->addSeparator();
#if !defined(Q_OS_MAC)
// Don't add qaHide on macOS.
// There is no way to bring the window back (no 'tray' for Mumble on macOS),
// and the system has built-in hide functionality via Cmd-H.
if (qstiIcon->isSystemTrayAvailable())
qmServer->addAction(qaHide);
#endif
qmServer->addAction(qaQuit);
qaServerBanList->setEnabled(Global::get().pPermissions & (ChanACL::Ban | ChanACL::Write));
qaServerUserList->setEnabled(Global::get().pPermissions & (ChanACL::Register | ChanACL::Write));
qaServerInformation->setEnabled(Global::get().uiSession != 0);
qaServerTokens->setEnabled(Global::get().uiSession != 0);
if (!qlServerActions.isEmpty()) {
qmServer->addSeparator();
foreach (QAction *a, qlServerActions)
qmServer->addAction(a);
}
}
void MainWindow::on_qaServerDisconnect_triggered() {
disconnectFromServer();
}
void MainWindow::on_qaServerBanList_triggered() {
openServerBanListDialog();
}
void MainWindow::on_qaServerUserList_triggered() {
openServerUserListDialog();
}
void MainWindow::on_qaServerInformation_triggered() {
openServerInformationDialog();
}
void MainWindow::on_qaServerTexture_triggered() {
changeServerTexture();
}
void MainWindow::on_qaServerTextureRemove_triggered() {
removeServerTexture();
}
void MainWindow::on_qaServerTokens_triggered() {
openServerTokensDialog();
}
void MainWindow::voiceRecorderDialog_finished(int) {
voiceRecorderDialog->deleteLater();
voiceRecorderDialog = nullptr;
}
void MainWindow::qmUser_aboutToShow() {
ClientUser *p = getContextMenuTargets().user;
const ClientUser *self = ClientUser::get(Global::get().uiSession);
bool isSelf = p == self;
qmUser->clear();
if (self && p && !isSelf && self->cChannel != p->cChannel) {
qmUser->addAction(qaUserJoin);
qmUser->addAction(qaUserMove);
qmUser->addSeparator();
}
if (Global::get().pPermissions & (ChanACL::Kick | ChanACL::Ban | ChanACL::Write))
qmUser->addAction(qaUserKick);
if (Global::get().pPermissions & (ChanACL::Ban | ChanACL::Write))
qmUser->addAction(qaUserBan);
qmUser->addAction(qaUserMute);
qmUser->addAction(qaUserDeaf);
if (Global::get().sh && Global::get().sh->m_version >= Version::fromComponents(1, 2, 3))
qmUser->addAction(qaUserPrioritySpeaker);
qmUser->addAction(qaUserLocalMute);
qmUser->addAction(qaUserLocalIgnore);
if (Global::get().s.bTTS)
qmUser->addAction(qaUserLocalIgnoreTTS);
if (p && !isSelf) {
qmUser->addSeparator();
qmUser->addAction(m_localVolumeLabel.get());
m_userLocalVolumeSlider->setUser(p->uiSession);
qmUser->addAction(m_userLocalVolumeSlider.get());
qmUser->addSeparator();
}
qmUser->addAction(qaUserLocalNickname);
if (isSelf)
qmUser->addAction(qaSelfComment);
else {
qmUser->addAction(qaUserCommentView);
qmUser->addAction(qaUserCommentReset);
qmUser->addAction(qaUserTextureReset);
}
qmUser->addAction(qaUserTextMessage);
if (Global::get().sh && Global::get().sh->m_version >= Version::fromComponents(1, 2, 2))
qmUser->addAction(qaUserInformation);
if (p && (p->iId < 0) && !p->qsHash.isEmpty()
&& (Global::get().pPermissions & ((isSelf ? ChanACL::SelfRegister : ChanACL::Register) | ChanACL::Write))) {
qmUser->addSeparator();
qmUser->addAction(qaUserRegister);
}
if (p && !p->qsHash.isEmpty() && (!p->qsFriendName.isEmpty() || (p->uiSession != Global::get().uiSession))) {
qmUser->addSeparator();
if (p->qsFriendName.isEmpty())
qmUser->addAction(qaUserFriendAdd);
else {
if (p->qsFriendName != p->qsName)
qmUser->addAction(qaUserFriendUpdate);
qmUser->addAction(qaUserFriendRemove);
}
}
if (isSelf) {
qmUser->addSeparator();
qmUser->addAction(qaAudioMute);
qmUser->addAction(qaAudioDeaf);
}
#ifndef Q_OS_MAC
if (Global::get().s.bMinimalView) {
qmUser->addSeparator();
qmUser->addMenu(qmServer);
qmUser->addMenu(qmSelf);
qmUser->addMenu(qmConfig);
qmUser->addMenu(qmHelp);
}
#endif
if (!qlUserActions.isEmpty()) {
qmUser->addSeparator();
foreach (QAction *a, qlUserActions)
qmUser->addAction(a);
}
if (!p) {
qaUserKick->setEnabled(false);
qaUserBan->setEnabled(false);
qaUserTextMessage->setEnabled(false);
qaUserLocalNickname->setEnabled(false);
qaUserLocalMute->setEnabled(false);
qaUserLocalIgnore->setEnabled(false);
qaUserLocalIgnoreTTS->setEnabled(false);
qaUserCommentReset->setEnabled(false);
qaUserTextureReset->setEnabled(false);
qaUserCommentView->setEnabled(false);
} else {
qaUserKick->setEnabled(!isSelf);
qaUserBan->setEnabled(!isSelf);
qaUserTextMessage->setEnabled(true);
qaUserLocalNickname->setEnabled(!isSelf);
qaUserLocalMute->setEnabled(!isSelf);
qaUserLocalIgnore->setEnabled(!isSelf);
qaUserLocalIgnoreTTS->setEnabled(!isSelf);
// If the server's version is less than 1.4.0 it won't support the new permission to reset a comment/avatar, so
// fall back to the old method
if (Global::get().sh->m_version < Version::fromComponents(1, 4, 0)) {
qaUserCommentReset->setEnabled(!p->qbaCommentHash.isEmpty()
&& (Global::get().pPermissions & (ChanACL::Move | ChanACL::Write)));
qaUserTextureReset->setEnabled(!p->qbaTextureHash.isEmpty()
&& (Global::get().pPermissions & (ChanACL::Move | ChanACL::Write)));
} else {
qaUserCommentReset->setEnabled(
!p->qbaCommentHash.isEmpty()
&& (Global::get().pPermissions & (ChanACL::ResetUserContent | ChanACL::Write)));
qaUserTextureReset->setEnabled(
!p->qbaTextureHash.isEmpty()
&& (Global::get().pPermissions & (ChanACL::ResetUserContent | ChanACL::Write)));
}
qaUserCommentView->setEnabled(!p->qbaCommentHash.isEmpty());
qaUserMute->setChecked(p->bMute || p->bSuppress);
qaUserDeaf->setChecked(p->bDeaf);
qaUserPrioritySpeaker->setChecked(p->bPrioritySpeaker);
qaUserLocalMute->setChecked(p->bLocalMute);
qaUserLocalIgnore->setChecked(p->bLocalIgnore);
qaUserLocalIgnoreTTS->setChecked(p->bLocalIgnoreTTS);
}
updateMenuPermissions();
}
void MainWindow::qmListener_aboutToShow() {
ClientUser *p = getContextMenuTargets().user;
bool self = p && (p->uiSession == Global::get().uiSession);
qmListener->clear();
if (self) {
qmListener->addAction(m_localVolumeLabel.get());
Channel *channel = getContextMenuChannel();
if (channel) {
m_listenerVolumeSlider->setListenedChannel(*channel);
qmListener->addAction(m_listenerVolumeSlider.get());
qmListener->addSeparator();
}
if (cContextChannel) {
qmListener->addAction(qaChannelListen);
qaChannelListen->setChecked(
Global::get().channelListenerManager->isListening(Global::get().uiSession, cContextChannel->iId));
}
} else {
qmListener->addAction(qaEmpty);
}
}
void MainWindow::on_qaUserMute_triggered() {
ClientUser *p = getContextMenuUser();
if (!p)
return;
MumbleProto::UserState mpus;
mpus.set_session(p->uiSession);
if (p->bMute || p->bSuppress) {
if (p->bMute)
mpus.set_mute(false);
if (p->bSuppress)
mpus.set_suppress(false);
} else {
mpus.set_mute(true);
}
Global::get().sh->sendMessage(mpus);
}
void MainWindow::on_qaUserLocalMute_triggered() {
ClientUser *p = getContextMenuUser();
if (!p) {
return;
}
bool muted = qaUserLocalMute->isChecked();
p->setLocalMute(muted);
if (!p->qsHash.isEmpty()) {
Global::get().db->setLocalMuted(p->qsHash, muted);
} else {
logChangeNotPermanent(QObject::tr("Local Mute"), p);
}
}
void MainWindow::on_qaUserLocalIgnore_triggered() {
ClientUser *p = getContextMenuUser();
if (!p) {
return;
}
bool ignored = qaUserLocalIgnore->isChecked();
p->setLocalIgnore(ignored);
if (!p->qsHash.isEmpty()) {
Global::get().db->setLocalIgnored(p->qsHash, ignored);
} else {
logChangeNotPermanent(QObject::tr("Ignore Messages"), p);
}
}
void MainWindow::on_qaUserLocalIgnoreTTS_triggered() {
ClientUser *p = getContextMenuUser();
if (!p) {
return;
}
bool ignoredTTS = qaUserLocalIgnoreTTS->isChecked();
p->setLocalIgnoreTTS(ignoredTTS);
if (!p->qsHash.isEmpty()) {
Global::get().db->setLocalIgnoredTTS(p->qsHash, ignoredTTS);
} else {
logChangeNotPermanent(QObject::tr("Disable Text-To-Speech"), p);
}
}
void MainWindow::on_qaUserDeaf_triggered() {
ClientUser *p = getContextMenuUser();
if (!p)
return;
MumbleProto::UserState mpus;
mpus.set_session(p->uiSession);
mpus.set_deaf(!p->bDeaf);
Global::get().sh->sendMessage(mpus);
}
void MainWindow::on_qaSelfPrioritySpeaker_triggered() {
toggleSelfPrioritySpeaker();
}
void MainWindow::on_qaUserPrioritySpeaker_triggered() {
ClientUser *p = getContextMenuUser();
if (!p)
return;
MumbleProto::UserState mpus;
mpus.set_session(p->uiSession);
mpus.set_priority_speaker(!p->bPrioritySpeaker);
Global::get().sh->sendMessage(mpus);
}
void MainWindow::on_qaUserRegister_triggered() {
ClientUser *p = getContextMenuUser();
if (!p)
return;
unsigned int session = p->uiSession;
QMessageBox::StandardButton result;
if (session == Global::get().uiSession)
result = QMessageBox::question(
this, tr("Register yourself as %1").arg(p->qsName),
tr("<p>You are about to register yourself on this server. This action cannot be undone, and your username "
"cannot be changed once this is done. You will forever be known as '%1' on this server.</p><p>Are you "
"sure you want to register yourself?</p>")
.arg(p->qsName.toHtmlEscaped()),
QMessageBox::Yes | QMessageBox::No);
else
result = QMessageBox::question(
this, tr("Register user %1").arg(p->qsName),
tr("<p>You are about to register %1 on the server. This action cannot be undone, the username cannot be "
"changed, and as a registered user, %1 will have access to the server even if you change the server "
"password.</p><p>From this point on, %1 will be authenticated with the certificate currently in "
"use.</p><p>Are you sure you want to register %1?</p>")
.arg(p->qsName.toHtmlEscaped()),
QMessageBox::Yes | QMessageBox::No);
if (result == QMessageBox::Yes) {
p = ClientUser::get(session);
if (!p)
return;
Global::get().sh->registerUser(p->uiSession);
}
}
void MainWindow::on_qaUserFriendAdd_triggered() {
ClientUser *p = getContextMenuUser();
if (!p)
return;
Global::get().db->addFriend(p->qsName, p->qsHash);
pmModel->setFriendName(p, p->qsName);
}
void MainWindow::on_qaUserFriendUpdate_triggered() {
on_qaUserFriendAdd_triggered();
}
void MainWindow::on_qaUserFriendRemove_triggered() {
ClientUser *p = getContextMenuUser();
if (!p)
return;
Global::get().db->removeFriend(p->qsHash);
pmModel->setFriendName(p, QString());
}
void MainWindow::on_qaUserKick_triggered() {
ClientUser *p = getContextMenuUser();
if (!p)
return;
unsigned int session = p->uiSession;
bool ok;
QString reason = QInputDialog::getText(this, tr("Kicking user %1").arg(p->qsName), tr("Enter reason"),
QLineEdit::Normal, QString(), &ok);
p = ClientUser::get(session);
if (!p)
return;
if (ok) {
Global::get().sh->kickBanUser(p->uiSession, reason, false);
}
}
void MainWindow::on_qaUserBan_triggered() {
ClientUser *p = getContextMenuUser();
if (!p)
return;
unsigned int session = p->uiSession;
bool ok;
QString reason = QInputDialog::getText(this, tr("Banning user %1").arg(p->qsName), tr("Enter reason"),
QLineEdit::Normal, QString(), &ok);
p = ClientUser::get(session);
if (!p)
return;
if (ok) {
Global::get().sh->kickBanUser(p->uiSession, reason, true);
}
}
void MainWindow::on_qaUserTextMessage_triggered() {
ClientUser *p = getContextMenuUser();
if (!p)
return;
openTextMessageDialog(p);
}
void MainWindow::openTextMessageDialog(ClientUser *p) {
unsigned int session = p->uiSession;
::TextMessage *texm = new ::TextMessage(this, tr("Sending message to %1").arg(p->qsName));
int res = texm->exec();
// Try to get find the user using the session id.
// This will return nullptr if the user disconnected while typing the message.
p = ClientUser::get(session);
if (p && (res == QDialog::Accepted)) {
QString msg = texm->message();
if (!msg.isEmpty()) {
Global::get().sh->sendUserTextMessage(p->uiSession, msg);
Global::get().l->log(Log::TextMessage,
tr("To %1: %2").arg(Log::formatClientUser(p, Log::Target), texm->message()),
tr("Message to %1").arg(p->qsName), true);
}
}
delete texm;
}
void MainWindow::on_qaUserLocalNickname_triggered() {
ClientUser *p = getContextMenuUser();
if (!p)
return;
openUserLocalNicknameDialog(*p);
}
void MainWindow::openUserLocalNicknameDialog(const ClientUser &p) {
unsigned int session = p.uiSession;
UserLocalNicknameDialog::present(session, qmUserNicknameTracker);
}
void MainWindow::on_qaUserCommentView_triggered() {
ClientUser *p = getContextMenuUser();
// This has to be done here because UserModel could've set it.
cuContextUser.clear();
if (!p)
return;
if (!p->qbaCommentHash.isEmpty() && p->qsComment.isEmpty()) {
p->qsComment = QString::fromUtf8(Global::get().db->blob(p->qbaCommentHash));
if (p->qsComment.isEmpty()) {
pmModel->uiSessionComment = ~(p->uiSession);
MumbleProto::RequestBlob mprb;
mprb.add_session_comment(p->uiSession);
Global::get().sh->sendMessage(mprb);
return;
}
}
pmModel->seenComment(pmModel->index(p));
::TextMessage *texm = new ::TextMessage(this, tr("View comment on user %1").arg(p->qsName));
texm->rteMessage->setText(p->qsComment, true);
texm->setAttribute(Qt::WA_DeleteOnClose, true);
texm->show();
}
void MainWindow::on_qaUserCommentReset_triggered() {
ClientUser *p = getContextMenuUser();
if (!p)
return;
unsigned int session = p->uiSession;
int ret = QMessageBox::question(
this, QLatin1String("Mumble"),
tr("Are you sure you want to reset the comment of user %1?").arg(p->qsName.toHtmlEscaped()), QMessageBox::Yes,
QMessageBox::No);
if (ret == QMessageBox::Yes) {
Global::get().sh->setUserComment(session, QString());
}
}
void MainWindow::on_qaUserTextureReset_triggered() {
ClientUser *p = getContextMenuUser();
if (!p)
return;
unsigned int session = p->uiSession;
int ret = QMessageBox::question(
this, QLatin1String("Mumble"),
tr("Are you sure you want to reset the avatar of user %1?").arg(p->qsName.toHtmlEscaped()), QMessageBox::Yes,
QMessageBox::No);
if (ret == QMessageBox::Yes) {
Global::get().sh->setUserTexture(session, QByteArray());
}
}
void MainWindow::on_qaUserInformation_triggered() {
ClientUser *p = getContextMenuUser();
if (!p)
return;
Global::get().sh->requestUserStats(p->uiSession, false);
}
void MainWindow::on_qaHide_triggered() {
hide();
}
void MainWindow::on_qaQuit_triggered() {
forceQuit = true;
this->close();
}
void MainWindow::sendChatbarText(QString qsText, bool plainText) {
if (plainText) {
// Escape HTML, unify line endings, then convert spaces to non-breaking ones to prevent multiple
// simultaneous ones from being collapsed into one (as a normal HTML renderer does).
qsText = qsText.toHtmlEscaped()
.replace("\r\n", "\n")
.replace("\r", "\n")
.replace("\n", "<br>")
.replace(" ", "&nbsp;");
} else {
// Markdown::markdownToHTML also takes care of replacing line breaks (\n) with the respective
// HTML code <br/>. Therefore if Markdown support is ever going to be removed from this
// function, this job has to be done explicitly as otherwise line breaks won't be shown on
// the receiving end of this text message.
qsText = Markdown::markdownToHTML(qsText);
}
sendChatbarMessage(qsText);
qteChat->clear();
}
void MainWindow::sendChatbarMessage(QString qsMessage) {
if (Global::get().uiSession == 0)
return; // Check if text & connection is available
ClientUser *p = pmModel->getUser(qtvUsers->currentIndex());
Channel *c = pmModel->getChannel(qtvUsers->currentIndex());
if (!Global::get().s.bChatBarUseSelection || !p || p->uiSession == Global::get().uiSession) {
// Channel message
if (!Global::get().s.bChatBarUseSelection || !c) // If no channel selected fallback to current one
c = ClientUser::get(Global::get().uiSession)->cChannel;
Global::get().sh->sendChannelTextMessage(c->iId, qsMessage, false);
Global::get().l->log(Log::TextMessage, tr("To %1: %2").arg(Log::formatChannel(c), qsMessage),
tr("Message to channel %1").arg(c->qsName), true);
} else {
// User message
Global::get().sh->sendUserTextMessage(p->uiSession, qsMessage);
Global::get().l->log(Log::TextMessage, tr("To %1: %2").arg(Log::formatClientUser(p, Log::Target), qsMessage),
tr("Message to %1").arg(p->qsName), true);
}
}
/// Handles Backtab/Shift-Tab for qteChat, which allows
/// users to move focus to the previous widget in
/// MainWindow.
void MainWindow::on_qteChat_backtabPressed() {
focusPreviousChild();
}
void MainWindow::on_qteChat_ctrlSpacePressed() {
autocompleteUsername();
}
void MainWindow::on_qteChat_tabPressed() {
// Only autocomplete the username, if the user entered text starts with a "@".
// Otherwise TAB should be reserved for accessible keyboard navigation.
QString currentText = qteChat->toPlainText();
if (currentText.startsWith("@")) {
currentText.remove(0, 1);
qteChat->clear();
QTextCursor tc = qteChat->textCursor();
tc.insertText(currentText);
qteChat->setTextCursor(tc);
autocompleteUsername();
return;
}
focusNextMainWidget();
}
void MainWindow::autocompleteUsername() {
unsigned int res = qteChat->completeAtCursor();
if (res == 0) {
return;
}
qtvUsers->setCurrentIndex(pmModel->index(ClientUser::get(res)));
}
void MainWindow::on_qmConfig_aboutToShow() {
// Don't remove the config, as that messes up OSX.
foreach (QAction *a, qmConfig->actions())
if (a != qaConfigDialog)
qmConfig->removeAction(a);
qmConfig->addAction(qaAudioWizard);
qmConfig->addAction(qaConfigCert);
qmConfig->addSeparator();
qaAudioTTS->setChecked(Global::get().s.bTTS);
qmConfig->addAction(qaAudioTTS);
qmConfig->addSeparator();
qmConfig->addAction(qaConfigMinimal);
qmConfig->addAction(qaFilterToggle);
qaTalkingUIToggle->setChecked(Global::get().talkingUI && Global::get().talkingUI->isVisible());
qmConfig->addAction(qaTalkingUIToggle);
if (Global::get().s.bMinimalView)
qmConfig->addAction(qaConfigHideFrame);
}
void MainWindow::qmChannel_aboutToShow() {
Channel *c = getContextMenuTargets().channel;
qmChannel->clear();
if (c && c->iId != ClientUser::get(Global::get().uiSession)->cChannel->iId) {
qmChannel->addAction(qaChannelJoin);
}
if (c && Global::get().sh && Global::get().sh->m_version >= Version::fromComponents(1, 4, 0)) {
// If the server's version is less than 1.4, the listening feature is not supported yet
// and thus it doesn't make sense to show the action for it
qmChannel->addAction(qaChannelListen);
qaChannelListen->setChecked(Global::get().channelListenerManager->isListening(Global::get().uiSession, c->iId));
}
qmChannel->addSeparator();
qmChannel->addAction(qaChannelAdd);
qmChannel->addAction(qaChannelACL);
qmChannel->addAction(qaChannelRemove);
qmChannel->addSeparator();
qmChannel->addAction(qaChannelLink);
qmChannel->addAction(qaChannelUnlink);
qmChannel->addAction(qaChannelUnlinkAll);
qmChannel->addSeparator();
qmChannel->addAction(qaChannelCopyURL);
qmChannel->addAction(qaChannelSendMessage);
// hiding the root is nonsense
if (c && c->cParent) {
qmChannel->addSeparator();
qmChannel->addAction(qaChannelHide);
qmChannel->addAction(qaChannelPin);
}
#ifndef Q_OS_MAC
if (Global::get().s.bMinimalView) {
qmChannel->addSeparator();
qmChannel->addMenu(qmServer);
qmChannel->addMenu(qmSelf);
qmChannel->addMenu(qmConfig);
qmChannel->addMenu(qmHelp);
}
#endif
if (!qlChannelActions.isEmpty()) {
qmChannel->addSeparator();
foreach (QAction *a, qlChannelActions)
qmChannel->addAction(a);
}
bool add, remove, acl, link, unlink, unlinkall, msg;
add = remove = acl = link = unlink = unlinkall = msg = false;
if (Global::get().uiSession != 0) {
add = true;
acl = true;
msg = true;
Channel *home = ClientUser::get(Global::get().uiSession)->cChannel;
if (c && c->iId != 0) {
remove = true;
}
if (!c)
c = Channel::get(Channel::ROOT_ID);
unlinkall = (home->qhLinks.count() > 0);
if (home != c) {
if (c->allLinks().contains(home))
unlink = true;
else
link = true;
}
}
if (c) {
qaChannelHide->setChecked(c->m_filterMode == ChannelFilterMode::HIDE);
qaChannelPin->setChecked(c->m_filterMode == ChannelFilterMode::PIN);
}
qaChannelAdd->setEnabled(add);
qaChannelRemove->setEnabled(remove);
qaChannelACL->setEnabled(acl);
qaChannelLink->setEnabled(link);
qaChannelUnlink->setEnabled(unlink);
qaChannelUnlinkAll->setEnabled(unlinkall);
qaChannelSendMessage->setEnabled(msg);
updateMenuPermissions();
}
void MainWindow::on_qaChannelJoin_triggered() {
Channel *c = getContextMenuChannel();
if (c) {
Global::get().sh->joinChannel(Global::get().uiSession, c->iId);
}
}
void MainWindow::on_qaUserJoin_triggered() {
const ClientUser *user = getContextMenuUser();
if (user) {
const Channel *channel = user->cChannel;
if (channel) {
Global::get().sh->joinChannel(Global::get().uiSession, channel->iId);
}
}
}
void MainWindow::on_qaUserMove_triggered() {
const ClientUser *user = getContextMenuUser();
if (user) {
const Channel *channel = ClientUser::get(Global::get().uiSession)->cChannel;
if (channel) {
Global::get().sh->joinChannel(user->uiSession, channel->iId);
}
}
}
void MainWindow::on_qaChannelListen_triggered() {
Channel *c = getContextMenuChannel();
if (c) {
if (qaChannelListen->isChecked()) {
Global::get().sh->startListeningToChannel(c->iId);
} else {
Global::get().sh->stopListeningToChannel(c->iId);
}
}
}
void MainWindow::on_qaChannelHide_triggered() {
Channel *c = getContextMenuChannel();
if (c) {
UserModel *um = static_cast< UserModel * >(qtvUsers->model());
if (qaChannelHide->isChecked()) {
c->setFilterMode(ChannelFilterMode::HIDE);
} else {
c->setFilterMode(ChannelFilterMode::NORMAL);
}
um->forceVisualUpdate(c);
}
}
void MainWindow::on_qaChannelPin_triggered() {
Channel *c = getContextMenuChannel();
if (c) {
UserModel *um = static_cast< UserModel * >(qtvUsers->model());
if (qaChannelPin->isChecked()) {
c->setFilterMode(ChannelFilterMode::PIN);
} else {
c->setFilterMode(ChannelFilterMode::NORMAL);
}
um->forceVisualUpdate(c);
}
}
void MainWindow::on_qaChannelAdd_triggered() {
Channel *c = getContextMenuChannel();
if (aclEdit) {
aclEdit->reject();
delete aclEdit;
aclEdit = nullptr;
}
aclEdit = new ACLEditor(c ? c->iId : 0, this);
if (c && (c->uiPermissions & ChanACL::Cached) && !(c->uiPermissions & (ChanACL::Write | ChanACL::MakeChannel))) {
aclEdit->qcbChannelTemporary->setEnabled(false);
aclEdit->qcbChannelTemporary->setChecked(true);
}
aclEdit->show();
}
void MainWindow::on_qaChannelRemove_triggered() {
int ret;
Channel *c = getContextMenuChannel();
if (!c)
return;
unsigned int id = c->iId;
ret = QMessageBox::question(
this, QLatin1String("Mumble"),
tr("Are you sure you want to delete %1 and all its sub-channels?").arg(c->qsName.toHtmlEscaped()),
QMessageBox::Yes, QMessageBox::No);
c = Channel::get(id);
if (!c)
return;
if (ret == QMessageBox::Yes) {
Global::get().sh->removeChannel(c->iId);
}
}
void MainWindow::on_qaChannelACL_triggered() {
Channel *c = getContextMenuChannel();
if (!c)
c = Channel::get(Channel::ROOT_ID);
unsigned int id = c->iId;
if (!c->qbaDescHash.isEmpty() && c->qsDesc.isEmpty()) {
c->qsDesc = QString::fromUtf8(Global::get().db->blob(c->qbaDescHash));
if (c->qsDesc.isEmpty()) {
MumbleProto::RequestBlob mprb;
mprb.add_channel_description(id);
Global::get().sh->sendMessage(mprb);
}
}
Global::get().sh->requestACL(id);
if (aclEdit) {
aclEdit->reject();
delete aclEdit;
aclEdit = nullptr;
}
}
void MainWindow::on_qaChannelLink_triggered() {
Channel *c = ClientUser::get(Global::get().uiSession)->cChannel;
Channel *l = getContextMenuChannel();
if (!l)
l = Channel::get(Channel::ROOT_ID);
Global::get().sh->addChannelLink(c->iId, l->iId);
}
void MainWindow::on_qaChannelUnlink_triggered() {
Channel *c = ClientUser::get(Global::get().uiSession)->cChannel;
Channel *l = getContextMenuChannel();
if (!l)
l = Channel::get(Channel::ROOT_ID);
Global::get().sh->removeChannelLink(c->iId, l->iId);
}
void MainWindow::on_qaChannelUnlinkAll_triggered() {
Channel *c = ClientUser::get(Global::get().uiSession)->cChannel;
MumbleProto::ChannelState mpcs;
mpcs.set_channel_id(c->iId);
foreach (Channel *l, c->qsPermLinks)
mpcs.add_links_remove(l->iId);
Global::get().sh->sendMessage(mpcs);
}
void MainWindow::on_qaChannelSendMessage_triggered() {
Channel *c = getContextMenuChannel();
if (!c)
return;
unsigned int id = c->iId;
::TextMessage *texm = new ::TextMessage(this, tr("Sending message to channel %1").arg(c->qsName), true);
int res = texm->exec();
c = Channel::get(id);
if (c && (res == QDialog::Accepted)) {
Global::get().sh->sendChannelTextMessage(id, texm->message(), texm->bTreeMessage);
if (texm->bTreeMessage)
Global::get().l->log(Log::TextMessage, tr("To %1 (Tree): %2").arg(Log::formatChannel(c), texm->message()),
tr("Message to tree %1").arg(c->qsName), true);
else
Global::get().l->log(Log::TextMessage, tr("To %1: %2").arg(Log::formatChannel(c), texm->message()),
tr("Message to channel %1").arg(c->qsName), true);
}
delete texm;
}
void MainWindow::on_qaChannelCopyURL_triggered() {
Channel *c = getContextMenuChannel();
QString host, uname, pw, channel;
unsigned short port;
if (!c)
return;
Global::get().sh->getConnectionInfo(host, port, uname, pw);
// walk back up the channel list to build the URL.
while (c->cParent) {
channel.prepend(c->qsName);
channel.prepend(QLatin1String("/"));
c = c->cParent;
}
QApplication::clipboard()->setMimeData(ServerItem::toMimeData(c->qsName, host, port, channel),
QClipboard::Clipboard);
}
/**
* This function updates the UI according to the permission of the user in the current channel.
* If possible the permissions are fetched from a cache. Otherwise they are requested by the server
* via a PermissionQuery call (whose reply updates the cache and calls this function again).
* @see MainWindow::msgPermissionQuery(const MumbleProto::PermissionQuery &msg)
*/
void MainWindow::updateMenuPermissions() {
ContextMenuTarget target = getContextMenuTargets();
ChanACL::Permissions p =
target.channel ? static_cast< ChanACL::Permissions >(target.channel->uiPermissions) : ChanACL::None;
if (target.channel && !p) {
Global::get().sh->requestChannelPermissions(target.channel->iId);
if (target.channel->iId == 0)
p = Global::get().pPermissions;
else
p = ChanACL::All;
target.channel->uiPermissions = p;
}
Channel *cparent = target.channel ? target.channel->cParent : nullptr;
ChanACL::Permissions pparent =
cparent ? static_cast< ChanACL::Permissions >(cparent->uiPermissions) : ChanACL::None;
if (cparent && !pparent) {
Global::get().sh->requestChannelPermissions(cparent->iId);
if (cparent->iId == 0)
pparent = Global::get().pPermissions;
else
pparent = ChanACL::All;
cparent->uiPermissions = pparent;
}
ClientUser *user = Global::get().uiSession ? ClientUser::get(Global::get().uiSession) : nullptr;
Channel *homec = user ? user->cChannel : nullptr;
ChanACL::Permissions homep = homec ? static_cast< ChanACL::Permissions >(homec->uiPermissions) : ChanACL::None;
if (homec && !homep) {
Global::get().sh->requestChannelPermissions(homec->iId);
if (homec->iId == 0)
homep = Global::get().pPermissions;
else
homep = ChanACL::All;
homec->uiPermissions = homep;
}
if (target.user) {
qaUserMute->setEnabled(p & (ChanACL::Write | ChanACL::MuteDeafen)
&& ((target.user != user) || target.user->bMute || target.user->bSuppress));
qaUserDeaf->setEnabled(p & (ChanACL::Write | ChanACL::MuteDeafen)
&& ((target.user != user) || target.user->bDeaf));
qaUserPrioritySpeaker->setEnabled(p & (ChanACL::Write | ChanACL::MuteDeafen));
qaUserTextMessage->setEnabled(p & (ChanACL::Write | ChanACL::TextMessage));
qaUserInformation->setEnabled((Global::get().pPermissions & (ChanACL::Write | ChanACL::Register))
|| (p & (ChanACL::Write | ChanACL::Enter)) || (target.user == user));
} else {
qaUserMute->setEnabled(false);
qaUserDeaf->setEnabled(false);
qaUserPrioritySpeaker->setEnabled(false);
qaUserTextMessage->setEnabled(false);
qaUserInformation->setEnabled(false);
}
qaChannelJoin->setEnabled(p & (ChanACL::Write | ChanACL::Enter));
qaChannelAdd->setEnabled(p & (ChanACL::Write | ChanACL::MakeChannel | ChanACL::MakeTempChannel));
qaChannelRemove->setEnabled(p & ChanACL::Write);
qaChannelACL->setEnabled((p & ChanACL::Write) || (pparent & ChanACL::Write));
qaChannelLink->setEnabled((p & (ChanACL::Write | ChanACL::LinkChannel))
&& (homep & (ChanACL::Write | ChanACL::LinkChannel)));
qaChannelUnlink->setEnabled((p & (ChanACL::Write | ChanACL::LinkChannel))
|| (homep & (ChanACL::Write | ChanACL::LinkChannel)));
qaChannelUnlinkAll->setEnabled(p & (ChanACL::Write | ChanACL::LinkChannel));
qaChannelCopyURL->setEnabled(target.channel);
qaChannelSendMessage->setEnabled(p & (ChanACL::Write | ChanACL::TextMessage));
qaChannelHide->setEnabled(target.channel);
qaChannelPin->setEnabled(target.channel);
bool chatBarEnabled = false;
if (Global::get().uiSession) {
if (Global::get().s.bChatBarUseSelection && (target.channel || target.user)) {
chatBarEnabled = p & (ChanACL::Write | ChanACL::TextMessage);
} else if (homec) {
chatBarEnabled = homep & (ChanACL::Write | ChanACL::TextMessage);
}
}
qteChat->setEnabled(chatBarEnabled);
}
void MainWindow::userStateChanged() {
if (Global::get().s.bStateInTray) {
updateTrayIcon();
}
ClientUser *user = ClientUser::get(Global::get().uiSession);
if (!user) {
Global::get().bAttenuateOthers = false;
Global::get().prioritySpeakerActiveOverride = false;
return;
}
switch (user->tsState) {
case Settings::Talking:
case Settings::Whispering:
case Settings::Shouting:
Global::get().bAttenuateOthers = Global::get().s.bAttenuateOthersOnTalk;
Global::get().prioritySpeakerActiveOverride =
Global::get().s.bAttenuateUsersOnPrioritySpeak && user->bPrioritySpeaker;
break;
case Settings::Passive:
case Settings::MutedTalking:
default:
Global::get().bAttenuateOthers = false;
Global::get().prioritySpeakerActiveOverride = false;
break;
}
}
void MainWindow::on_qaAudioReset_triggered() {
AudioInputPtr ai = Global::get().ai;
if (ai)
ai->bResetProcessor = true;
}
void MainWindow::on_qaFilterToggle_triggered() {
Global::get().s.bFilterActive = qaFilterToggle->isChecked();
if (!Global::get().s.bFilterActive) {
qtvUsers->setAccessibleName(tr("Channels and users"));
} else {
qtvUsers->setAccessibleName(tr("Filtered channels and users"));
}
updateUserModel();
}
void MainWindow::on_qaAudioMute_triggered() {
if (Global::get().bInAudioWizard) {
qaAudioMute->setChecked(!qaAudioMute->isChecked());
return;
}
AudioInputPtr ai = Global::get().ai;
if (ai)
ai->tIdle.restart();
Global::get().s.bMute = qaAudioMute->isChecked();
if (!Global::get().s.bMute && Global::get().s.bDeaf) {
Global::get().s.bDeaf = false;
qaAudioDeaf->setChecked(false);
Global::get().l->log(Log::SelfUndeaf, tr("Unmuted and undeafened."));
} else if (!Global::get().s.bMute) {
Global::get().l->log(Log::SelfUnmute, tr("Unmuted."));
} else {
Global::get().l->log(Log::SelfMute, tr("Muted."));
}
if (Global::get().sh) {
Global::get().sh->setSelfMuteDeafState(Global::get().s.bMute, Global::get().s.bDeaf);
}
updateAudioToolTips();
updateTrayIcon();
}
void MainWindow::setAudioMute(bool mute) {
// Pretend the user pushed the button manually
qaAudioMute->setChecked(mute);
qaAudioMute->triggered(mute);
}
void MainWindow::on_qaAudioDeaf_triggered() {
if (Global::get().bInAudioWizard) {
qaAudioDeaf->setChecked(!qaAudioDeaf->isChecked());
return;
}
if (!qaAudioDeaf->isChecked() && bAutoUnmute) {
qaAudioDeaf->setChecked(true);
qaAudioMute->setChecked(false);
on_qaAudioMute_triggered();
return;
}
AudioInputPtr ai = Global::get().ai;
if (ai)
ai->tIdle.restart();
Global::get().s.bDeaf = qaAudioDeaf->isChecked();
if (Global::get().s.bDeaf && !Global::get().s.bMute) {
bAutoUnmute = true;
Global::get().s.bMute = true;
qaAudioMute->setChecked(true);
Global::get().l->log(Log::SelfDeaf, tr("Muted and deafened."));
} else if (Global::get().s.bDeaf) {
Global::get().l->log(Log::SelfDeaf, tr("Deafened."));
bAutoUnmute = false;
} else {
Global::get().l->log(Log::SelfUndeaf, tr("Undeafened."));
}
if (Global::get().sh) {
Global::get().sh->setSelfMuteDeafState(Global::get().s.bMute, Global::get().s.bDeaf);
}
updateAudioToolTips();
updateTrayIcon();
}
void MainWindow::setAudioDeaf(bool deaf) {
// Pretend the user pushed the button manually
qaAudioDeaf->setChecked(deaf);
qaAudioDeaf->triggered(deaf);
}
void MainWindow::on_qaRecording_triggered() {
recording();
}
void MainWindow::on_qaAudioTTS_triggered() {
enableAudioTTS(qaAudioTTS->isChecked());
}
void MainWindow::on_qaAudioStats_triggered() {
openAudioStatsDialog();
}
void MainWindow::on_qaAudioUnlink_triggered() {
Global::get().pluginManager->unlinkPositionalData();
}
void MainWindow::on_qaConfigDialog_triggered() {
openConfigDialog();
}
void MainWindow::on_qaConfigMinimal_triggered() {
Global::get().s.bMinimalView = qaConfigMinimal->isChecked();
updateWindowTitle();
setupView();
}
void MainWindow::on_qaConfigHideFrame_triggered() {
Global::get().s.bHideFrame = qaConfigHideFrame->isChecked();
setupView(false);
}
void MainWindow::on_qaConfigCert_triggered() {
openCertWizardDialog();
}
void MainWindow::on_qaAudioWizard_triggered() {
openAudioWizardDialog();
}
void MainWindow::on_qaDeveloperConsole_triggered() {
Global::get().c->show();
}
void MainWindow::on_qaPositionalAudioViewer_triggered() {
if (m_paViewer) {
m_paViewer->raise();
} else {
m_paViewer = std::make_unique< PositionalAudioViewer >();
connect(m_paViewer.get(), &PositionalAudioViewer::finished, this, [this]() { m_paViewer.reset(); });
m_paViewer->show();
}
}
void MainWindow::on_qaHelpWhatsThis_triggered() {
QWhatsThis::enterWhatsThisMode();
}
void MainWindow::on_qaHelpAbout_triggered() {
openAboutDialog();
}
void MainWindow::on_qaHelpAboutQt_triggered() {
openAboutQtDialog();
}
void MainWindow::on_qaHelpVersionCheck_triggered() {
versionCheck();
}
void MainWindow::on_gsMuteSelf_down(QVariant v) {
int val = v.toInt();
if (((val > 0) && !Global::get().s.bMute) || ((val < 0) && Global::get().s.bMute) || (val == 0)) {
qaAudioMute->setChecked(!qaAudioMute->isChecked());
on_qaAudioMute_triggered();
}
}
void MainWindow::on_gsDeafSelf_down(QVariant v) {
int val = v.toInt();
if (((val > 0) && !Global::get().s.bDeaf) || ((val < 0) && Global::get().s.bDeaf) || (val == 0)) {
qaAudioDeaf->setChecked(!qaAudioDeaf->isChecked());
on_qaAudioDeaf_triggered();
}
}
void MainWindow::on_PushToTalk_triggered(bool down, QVariant) {
Global::get().iPrevTarget = 0;
if (down) {
Global::get().uiDoublePush = Global::get().tDoublePush.restart();
Global::get().iPushToTalk++;
} else if (Global::get().iPushToTalk > 0) {
QTimer::singleShot(static_cast< int >(Global::get().s.pttHold), this, SLOT(pttReleased()));
}
}
void MainWindow::pttReleased() {
if (Global::get().iPushToTalk > 0) {
Global::get().iPushToTalk--;
}
}
void MainWindow::on_PushToMute_triggered(bool down, QVariant) {
Global::get().bPushToMute = down;
updateTrayIcon();
updateUserModel();
}
void MainWindow::on_VolumeUp_triggered(bool down, QVariant) {
if (down) {
float vol = Global::get().s.fVolume + 0.1f;
if (vol > 2.0f) {
vol = 2.0f;
}
Global::get().s.fVolume = vol;
}
}
void MainWindow::on_VolumeDown_triggered(bool down, QVariant) {
if (down) {
float vol = Global::get().s.fVolume - 0.1f;
if (vol < 0.0f) {
vol = 0.0f;
}
Global::get().s.fVolume = vol;
}
}
Channel *MainWindow::mapChannel(int idx) const {
if (!Global::get().uiSession)
return nullptr;
Channel *c = nullptr;
if (idx < 0) {
switch (idx) {
case SHORTCUT_TARGET_ROOT:
c = Channel::get(Channel::ROOT_ID);
break;
case SHORTCUT_TARGET_PARENT:
case SHORTCUT_TARGET_CURRENT:
c = ClientUser::get(Global::get().uiSession)->cChannel;
if (idx == SHORTCUT_TARGET_PARENT)
c = c->cParent;
break;
default:
if (idx <= SHORTCUT_TARGET_PARENT_SUBCHANNEL)
c = pmModel->getSubChannel(ClientUser::get(Global::get().uiSession)->cChannel->cParent,
SHORTCUT_TARGET_PARENT_SUBCHANNEL - idx);
else
c = pmModel->getSubChannel(ClientUser::get(Global::get().uiSession)->cChannel,
SHORTCUT_TARGET_SUBCHANNEL - idx);
break;
}
} else {
c = Channel::get(static_cast< unsigned int >(idx));
}
return c;
}
void MainWindow::updateTarget() {
Global::get().iPrevTarget = Global::get().iTarget;
if (qmCurrentTargets.isEmpty()) {
Global::get().bCenterPosition = false;
Global::get().iTarget = 0;
} else {
bool center = false;
QList< ShortcutTarget > ql;
foreach (const ShortcutTarget &st, qmCurrentTargets.keys()) {
ShortcutTarget nt;
center = center || st.bForceCenter;
nt.bUsers = st.bUsers;
nt.bCurrentSelection = st.bCurrentSelection;
if (nt.bCurrentSelection) {
Channel *c = pmModel->getSelectedChannel();
if (c) {
nt.bUsers = false;
nt.iChannel = static_cast< int >(c->iId);
nt.bLinks = st.bLinks;
nt.bChildren = st.bChildren;
ql << nt;
} else {
ClientUser *user = pmModel->getSelectedUser();
if (user) {
nt.bUsers = true;
nt.qlSessions << user->uiSession;
ql << nt;
}
}
} else if (st.bUsers) {
foreach (const QString &hash, st.qlUsers) {
ClientUser *p = pmModel->getUser(hash);
if (p)
nt.qlSessions.append(p->uiSession);
}
if (!nt.qlSessions.isEmpty())
ql << nt;
} else {
Channel *c = mapChannel(st.iChannel);
if (c) {
nt.bLinks = st.bLinks;
nt.bChildren = st.bChildren;
nt.iChannel = static_cast< int >(c->iId);
nt.qsGroup = st.qsGroup;
ql << nt;
}
}
}
if (ql.isEmpty()) {
Global::get().iTarget = -1;
} else {
++iTargetCounter;
int idx = qmTargets.value(ql);
if (idx == 0) {
// An idx of 0 means that we don't have a mapping for this shortcut yet
// Thus we'll register it here
QMap< int, int > qm;
QMap< int, int >::const_iterator i;
// We reverse the qmTargetsUse map into qm so that each key becomes a value and vice versa
for (i = qmTargetUse.constBegin(); i != qmTargetUse.constEnd(); ++i) {
qm.insert(i.value(), i.key());
}
// The reversal and the promise that when iterating over a QMap, the keys will appear sorted
// leads to us now being able to get the next target ID as the value of the first entry in
// the map.
i = qm.constBegin();
idx = i.value();
// Sets up a VoiceTarget (which is identified by the targetID idx) on the server for the given set
// of ShortcutTargets
MumbleProto::VoiceTarget mpvt;
mpvt.set_id(static_cast< unsigned int >(idx));
foreach (const ShortcutTarget &st, ql) {
MumbleProto::VoiceTarget_Target *t = mpvt.add_targets();
// st.bCurrentSelection has been taken care of at this point already (if it was set) so
// we don't have to check for that here.
if (st.bUsers) {
foreach (unsigned int uisession, st.qlSessions)
t->add_session(uisession);
} else {
t->set_channel_id(static_cast< unsigned int >(st.iChannel));
if (st.bChildren)
t->set_children(true);
if (st.bLinks)
t->set_links(true);
if (!st.qsGroup.isEmpty())
t->set_group(u8(st.qsGroup));
}
}
Global::get().sh->sendMessage(mpvt);
// Store a mapping of the list of ShortcutTargets and the used targetID
qmTargets.insert(ql, idx);
// Advance the iteration of qm (which contains the reverse mapping of qmTargetUse) by two.
// Note that qmTargetUse is first populated in Messages.cpp so we will not overflow the map
// by this.
++i;
++i;
// Get the target ID for the targetID after next
int oldidx = i.value();
if (oldidx) {
QHash< QList< ShortcutTarget >, int >::iterator mi;
for (mi = qmTargets.begin(); mi != qmTargets.end(); ++mi) {
if (mi.value() == oldidx) {
// If we have used the targetID after next before, we clear the VoiceTarget for that
// targetID on the server in order to be able to reuse that ID once we need it. We do
// it 2 steps in advance as to not run into timing problems where the server might
// receive this clearing message too late for us to recycle the ID.
qmTargets.erase(mi);
mpvt.Clear();
mpvt.set_id(static_cast< unsigned int >(oldidx));
Global::get().sh->sendMessage(mpvt);
break;
}
}
}
}
// This is where the magic happens. We replace the old value the used targetID was mapped to with
// iTargetCounter. iTargetCounter is guaranteed to be bigger than any number a targetID is currently
// mapped to in this map. This causes the mapping for the most recently used targetID to appear last
// in the qm map the next time this function gets called. This causes targetIDs to be sorted according
// to the time they have been assigned for the last time so that the targetID that comes last in qm will
// be the one that has been assigned most recently. This trick turns qmTargetUse (or rather qm) into
// something similar to a RingBuffer inside this method.
qmTargetUse.insert(idx, iTargetCounter);
Global::get().bCenterPosition = center;
Global::get().iTarget = idx;
}
}
}
void MainWindow::on_gsWhisper_triggered(bool down, QVariant scdata) {
ShortcutTarget st = scdata.value< ShortcutTarget >();
if (down) {
if (gsJoinChannel->active()) {
if (!st.bUsers) {
Channel *c = mapChannel(st.iChannel);
if (c) {
Global::get().sh->joinChannel(Global::get().uiSession, c->iId);
}
return;
}
}
if (gsLinkChannel->active()) {
if (!st.bUsers) {
Channel *c = ClientUser::get(Global::get().uiSession)->cChannel;
Channel *l = mapChannel(st.iChannel);
if (l) {
if (c->qsPermLinks.contains(l)) {
Global::get().sh->removeChannelLink(c->iId, l->iId);
} else {
Global::get().sh->addChannelLink(c->iId, l->iId);
}
}
return;
}
}
addTarget(&st);
updateTarget();
Global::get().iPushToTalk++;
} else if (Global::get().iPushToTalk > 0) {
SignalCurry *fwd = new SignalCurry(scdata, true, this);
connect(fwd, SIGNAL(called(QVariant)), SLOT(whisperReleased(QVariant)));
QTimer::singleShot(static_cast< int >(Global::get().s.pttHold), fwd, SLOT(call()));
}
}
/* Add and remove ShortcutTargets from the qmCurrentTargets Map, which counts
* the number of push-to-talk events for a given ShortcutTarget. If this number
* reaches 0, the ShortcutTarget is removed from qmCurrentTargets.
*/
void MainWindow::addTarget(ShortcutTarget *st) {
if (qmCurrentTargets.contains(*st))
qmCurrentTargets[*st] += 1;
else
qmCurrentTargets[*st] = 1;
}
void MainWindow::removeTarget(ShortcutTarget *st) {
if (!qmCurrentTargets.contains(*st))
return;
if (qmCurrentTargets[*st] == 1)
qmCurrentTargets.remove(*st);
else
qmCurrentTargets[*st] -= 1;
}
void MainWindow::on_gsCycleTransmitMode_triggered(bool down, QVariant) {
if (down) {
QString qsNewMode;
switch (Global::get().s.atTransmit) {
case Settings::Continuous:
setTransmissionMode(Settings::VAD);
break;
case Settings::VAD:
setTransmissionMode(Settings::PushToTalk);
break;
case Settings::PushToTalk:
setTransmissionMode(Settings::Continuous);
break;
}
}
}
void MainWindow::on_gsToggleMainWindowVisibility_triggered(bool down, QVariant) {
if (down) {
if (Global::get().mw->isVisible()) {
Global::get().mw->hide();
} else {
Global::get().mw->show();
}
}
}
void MainWindow::on_gsListenChannel_triggered(bool down, QVariant scdata) {
ChannelTarget target = scdata.value< ChannelTarget >();
const Channel *c = Channel::get(target.channelID);
if (down && c) {
if (!Global::get().channelListenerManager->isListening(Global::get().uiSession, c->iId)) {
Global::get().sh->startListeningToChannel(c->iId);
} else {
Global::get().sh->stopListeningToChannel(c->iId);
}
}
}
void MainWindow::on_gsTransmitModePushToTalk_triggered(bool down, QVariant) {
if (down) {
setTransmissionMode(Settings::PushToTalk);
}
}
void MainWindow::on_gsTransmitModeContinuous_triggered(bool down, QVariant) {
if (down) {
setTransmissionMode(Settings::Continuous);
}
}
void MainWindow::on_gsTransmitModeVAD_triggered(bool down, QVariant) {
if (down) {
setTransmissionMode(Settings::VAD);
}
}
void MainWindow::on_gsSendTextMessage_triggered(bool down, QVariant scdata) {
if (!down || !Global::get().sh || !Global::get().sh->isRunning() || Global::get().uiSession == 0) {
return;
}
QString qsText = scdata.toString();
if (qsText.isEmpty()) {
return;
}
Channel *c = ClientUser::get(Global::get().uiSession)->cChannel;
Global::get().sh->sendChannelTextMessage(c->iId, qsText, false);
Global::get().l->log(Log::TextMessage, tr("To %1: %2").arg(Log::formatChannel(c), qsText),
tr("Message to channel %1").arg(c->qsName), true);
}
void MainWindow::on_gsSendClipboardTextMessage_triggered(bool down, QVariant) {
if (!down || (QApplication::clipboard()->text().isEmpty())) {
return;
}
// call sendChatbarMessage() instead of on_gsSendTextMessage_triggered() to handle
// formatting of the content in the clipboard, i.e., href.
sendChatbarMessage(QApplication::clipboard()->text());
}
void MainWindow::on_gsToggleTalkingUI_triggered(bool down, QVariant) {
if (down) {
qaTalkingUIToggle->trigger();
}
}
void MainWindow::on_gsToggleSearch_triggered(bool down, QVariant) {
if (!down) {
return;
}
toggleSearchDialogVisibility();
}
void MainWindow::on_gsServerConnect_triggered(bool down, QVariant) {
if (!down) {
return;
}
openServerConnectDialog();
}
void MainWindow::on_gsServerDisconnect_triggered(bool down, QVariant) {
if (!down) {
return;
}
disconnectFromServer();
}
void MainWindow::on_gsServerInformation_triggered(bool down, QVariant) {
if (!down) {
return;
}
openServerInformationDialog();
}
void MainWindow::on_gsServerTokens_triggered(bool down, QVariant) {
if (!down) {
return;
}
openServerTokensDialog();
}
void MainWindow::on_gsServerUserList_triggered(bool down, QVariant) {
if (!down) {
return;
}
openServerUserListDialog();
}
void MainWindow::on_gsServerBanList_triggered(bool down, QVariant) {
if (!down) {
return;
}
openServerBanListDialog();
}
void MainWindow::on_gsSelfPrioritySpeaker_triggered(bool down, QVariant) {
if (!down) {
return;
}
toggleSelfPrioritySpeaker();
}
void MainWindow::on_gsRecording_triggered(bool down, QVariant) {
if (!down) {
return;
}
recording();
}
void MainWindow::on_gsSelfComment_triggered(bool down, QVariant) {
if (!down) {
return;
}
openSelfCommentDialog();
}
void MainWindow::on_gsServerTexture_triggered(bool down, QVariant) {
if (!down) {
return;
}
changeServerTexture();
}
void MainWindow::on_gsServerTextureRemove_triggered(bool down, QVariant) {
if (!down) {
return;
}
removeServerTexture();
}
void MainWindow::on_gsSelfRegister_triggered(bool down, QVariant) {
if (!down) {
return;
}
selfRegister();
}
void MainWindow::on_gsAudioStats_triggered(bool down, QVariant) {
if (!down) {
return;
}
openAudioStatsDialog();
}
void MainWindow::on_gsConfigDialog_triggered(bool down, QVariant) {
if (!down) {
return;
}
openConfigDialog();
}
void MainWindow::on_gsAudioWizard_triggered(bool down, QVariant) {
if (!down) {
return;
}
openAudioWizardDialog();
}
void MainWindow::on_gsConfigCert_triggered(bool down, QVariant) {
if (!down) {
return;
}
openCertWizardDialog();
}
void MainWindow::on_gsAudioTTS_triggered(bool down, QVariant) {
if (!down) {
return;
}
enableAudioTTS(!Global::get().s.bTTS);
}
void MainWindow::on_gsHelpAbout_triggered(bool down, QVariant) {
if (!down) {
return;
}
openAboutDialog();
}
void MainWindow::on_gsHelpAboutQt_triggered(bool down, QVariant) {
if (!down) {
return;
}
openAboutQtDialog();
}
void MainWindow::on_gsHelpVersionCheck_triggered(bool down, QVariant) {
if (!down) {
return;
}
versionCheck();
}
void MainWindow::whisperReleased(QVariant scdata) {
if (Global::get().iPushToTalk <= 0)
return;
ShortcutTarget st = scdata.value< ShortcutTarget >();
Global::get().iPushToTalk--;
removeTarget(&st);
updateTarget();
}
void MainWindow::onResetAudio() {
qWarning("MainWindow: Start audio reset");
Audio::stop();
Audio::start();
qWarning("MainWindow: Audio reset complete");
}
void MainWindow::viewCertificate(bool) {
ViewCert vc(Global::get().sh->qscCert, this);
vc.exec();
}
/**
* This function prepares the UI for receiving server data. It gets called once the
* connection to the server is established but before the server Sync is complete.
*/
void MainWindow::serverConnected() {
Global::get().uiSession = 0;
Global::get().pPermissions = ChanACL::None;
#ifdef Q_OS_MAC
// Suppress AppNap while we're connected to a server.
MUSuppressAppNap(true);
#endif
Global::get().l->clearIgnore();
Global::get().l->setIgnore(Log::UserJoin);
Global::get().l->setIgnore(Log::OtherSelfMute);
QString host, uname, pw;
unsigned short port;
Global::get().sh->getConnectionInfo(host, port, uname, pw);
Global::get().l->log(Log::ServerConnected, tr("Connected."));
qaServerDisconnect->setEnabled(true);
qaServerInformation->setEnabled(true);
qaServerBanList->setEnabled(true);
Channel *root = Channel::get(Channel::ROOT_ID);
pmModel->renameChannel(root, tr("Root"));
pmModel->setCommentHash(root, QByteArray());
root->uiPermissions = 0;
qtvUsers->setRowHidden(0, QModelIndex(), false);
Global::get().bAllowHTML = true;
Global::get().uiMessageLength = 5000;
Global::get().uiImageLength = 131072;
Global::get().uiMaxUsers = 0;
enableRecording(true);
if (Global::get().s.bMute || Global::get().s.bDeaf) {
Global::get().sh->setSelfMuteDeafState(Global::get().s.bMute, Global::get().s.bDeaf);
}
// Update QActions and menus
on_qmServer_aboutToShow();
on_qmSelf_aboutToShow();
qmChannel_aboutToShow();
qmUser_aboutToShow();
on_qmConfig_aboutToShow();
#ifdef Q_OS_WIN
TaskList::addToRecentList(Global::get().s.qsLastServer, uname, host, port);
#endif
qdwMinimalViewNote->hide();
}
void MainWindow::serverDisconnected(QAbstractSocket::SocketError err, QString reason) {
// clear ChannelListener
Global::get().channelListenerManager->clear();
Global::get().uiSession = 0;
Global::get().pPermissions = ChanACL::None;
Global::get().bAttenuateOthers = false;
qaServerDisconnect->setEnabled(false);
qaServerInformation->setEnabled(false);
qaServerBanList->setEnabled(false);
qtvUsers->setCurrentIndex(QModelIndex());
qteChat->setEnabled(false);
updateTrayIcon();
#ifdef Q_OS_MAC
// Remove App Nap suppression now that we're disconnected.
MUSuppressAppNap(false);
#endif
QString uname, pw, host;
unsigned short port;
Global::get().sh->getConnectionInfo(host, port, uname, pw);
if (Global::get().sh->hasSynchronized()) {
QList< Shortcut > &shortcuts = Global::get().s.qlShortcuts;
// Only save server-specific shortcuts if the client and server have been synchronized before as only then
// did the client actually load them from the DB. If we store them without having loaded them, we will
// effectively clear the server-specific shortcuts for this server.
Global::get().db->setShortcuts(Global::get().sh->qbaDigest, shortcuts);
// Clear server-specific shortcuts from the list of known shortcuts
auto it = std::remove_if(shortcuts.begin(), shortcuts.end(),
[](const Shortcut &shortcut) { return shortcut.isServerSpecific(); });
if (it != shortcuts.end()) {
// Some shortcuts have to be removed
shortcuts.erase(it, shortcuts.end());
GlobalShortcutEngine::engine->bNeedRemap = true;
}
}
if (aclEdit) {
aclEdit->reject();
delete aclEdit;
aclEdit = nullptr;
}
if (banEdit) {
banEdit->reject();
delete banEdit;
banEdit = nullptr;
}
if (userEdit) {
userEdit->reject();
delete userEdit;
userEdit = nullptr;
}
if (tokenEdit) {
tokenEdit->reject();
delete tokenEdit;
tokenEdit = nullptr;
}
QSet< QAction * > qs;
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
qs += QSet< QAction * >(qlServerActions.begin(), qlServerActions.end());
qs += QSet< QAction * >(qlChannelActions.begin(), qlChannelActions.end());
qs += QSet< QAction * >(qlUserActions.begin(), qlUserActions.end());
#else
// In Qt 5.14 QList::toSet() has been deprecated as there exists a dedicated constructor of QSet for this now
qs += qlServerActions.toSet();
qs += qlChannelActions.toSet();
qs += qlUserActions.toSet();
#endif
foreach (QAction *a, qs)
delete a;
qlServerActions.clear();
qlChannelActions.clear();
qlUserActions.clear();
pmModel->removeAll();
qtvUsers->setRowHidden(0, QModelIndex(), true);
// Update QActions and menus
on_qmServer_aboutToShow();
on_qmSelf_aboutToShow();
qmChannel_aboutToShow();
qmUser_aboutToShow();
on_qmConfig_aboutToShow();
// We can't record without a server anyway, so we disable the functionality here
enableRecording(false);
if (!Global::get().sh->qlErrors.isEmpty()) {
foreach (QSslError e, Global::get().sh->qlErrors)
Global::get().l->log(Log::Warning, tr("SSL Verification failed: %1").arg(e.errorString().toHtmlEscaped()));
if (!Global::get().sh->qscCert.isEmpty()) {
QSslCertificate c = Global::get().sh->qscCert.at(0);
QString basereason;
QString actual_digest = QString::fromLatin1(c.digest(QCryptographicHash::Sha1).toHex());
QString digests_section =
tr("<li>Server certificate digest (SHA-1):\t%1</li>").arg(ViewCert::prettifyDigest(actual_digest));
QString expected_digest = Global::get().db->getDigest(host, port);
if (!expected_digest.isNull()) {
basereason =
tr("<b>WARNING:</b> The server presented a certificate that was different from the stored one.");
digests_section.append(tr("<li>Expected certificate digest (SHA-1):\t%1</li>")
.arg(ViewCert::prettifyDigest(expected_digest)));
} else {
basereason = tr("Server presented a certificate which failed verification.");
}
QStringList qsl;
foreach (QSslError e, Global::get().sh->qlErrors)
qsl << QString::fromLatin1("<li>%1</li>").arg(e.errorString().toHtmlEscaped());
QMessageBox qmb(QMessageBox::Warning, QLatin1String("Mumble"),
tr("<p>%1</p><ul>%2</ul><p>The specific errors with this certificate are:</p><ol>%3</ol>"
"<p>Do you wish to accept this certificate anyway?<br />(It will also be stored so you "
"won't be asked this again.)</p>")
.arg(basereason)
.arg(digests_section)
.arg(qsl.join(QString())),
QMessageBox::Yes | QMessageBox::No, this);
qmb.setDefaultButton(QMessageBox::No);
qmb.setEscapeButton(QMessageBox::No);
QPushButton *qp = qmb.addButton(tr("&View Certificate"), QMessageBox::ActionRole);
forever {
int res = qmb.exec();
if ((res == 0) && (qmb.clickedButton() == qp)) {
ViewCert vc(Global::get().sh->qscCert, this);
vc.exec();
continue;
} else if (res == QMessageBox::Yes) {
Global::get().db->setDigest(host, port,
QString::fromLatin1(c.digest(QCryptographicHash::Sha1).toHex()));
qaServerDisconnect->setEnabled(true);
on_Reconnect_timeout();
}
break;
}
}
} else if (err == QAbstractSocket::SslHandshakeFailedError) {
QMessageBox::warning(this, tr("SSL Version mismatch"),
tr("This server is using an older encryption standard, and is no longer supported by "
"modern versions of Mumble."),
QMessageBox::Ok);
} else {
bool ok = false;
if (!reason.isEmpty()) {
Global::get().l->log(Log::ServerDisconnected,
tr("Server connection failed: %1.").arg(reason.toHtmlEscaped()));
} else {
Global::get().l->log(Log::ServerDisconnected, tr("Disconnected from server."));
}
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
// Qt 5.15 introduced a default constructor that initializes the flags to be set to no flags
Qt::WindowFlags wf;
#elif defined(Q_OS_MAC)
Qt::WindowFlags wf = Qt::Sheet;
#else
// Before Qt 5.15 we have emulate the default constructor by assigning a literal zero
Qt::WindowFlags wf = 0;
#endif
bool matched = true;
switch (rtLast) {
case MumbleProto::Reject_RejectType_InvalidUsername:
uname = QInputDialog::getText(this, tr("Invalid username"),
tr("You connected with an invalid username, please try another one."),
QLineEdit::Normal, uname, &ok, wf);
break;
case MumbleProto::Reject_RejectType_UsernameInUse:
uname = QInputDialog::getText(this, tr("Username in use"),
tr("That username is already in use, please try another username."),
QLineEdit::Normal, uname, &ok, wf);
break;
case MumbleProto::Reject_RejectType_WrongUserPW:
pw = QInputDialog::getText(this, tr("Wrong certificate or password"),
tr("Wrong certificate or password for registered user. If you are\n"
"certain this user is protected by a password please retry.\n"
"Otherwise abort and check your certificate and username."),
QLineEdit::Password, pw, &ok, wf);
break;
case MumbleProto::Reject_RejectType_WrongServerPW:
pw = QInputDialog::getText(this, tr("Wrong password"),
tr("Wrong server password for unregistered user account, please try again."),
QLineEdit::Password, pw, &ok, wf);
break;
default:
matched = false;
break;
}
if (ok && matched) {
if (!Global::get().s.bSuppressIdentity)
Global::get().db->setPassword(host, port, uname, pw);
qaServerDisconnect->setEnabled(true);
Global::get().sh->setConnectionInfo(host, port, uname, pw);
on_Reconnect_timeout();
} else if (!matched && Global::get().s.bReconnect && !reason.isEmpty()) {
qaServerDisconnect->setEnabled(true);
if (bRetryServer) {
qtReconnect->start();
}
}
}
qstiIcon->setToolTip(tr("Mumble -- %1").arg(Version::getRelease()));
AudioInput::setMaxBandwidth(-1);
if (Global::get().s.bMinimalView) {
qdwMinimalViewNote->show();
}
}
void MainWindow::resolverError(QAbstractSocket::SocketError, QString reason) {
if (!reason.isEmpty()) {
Global::get().l->log(Log::ServerDisconnected, tr("Server connection failed: %1.").arg(reason.toHtmlEscaped()));
} else {
Global::get().l->log(Log::ServerDisconnected, tr("Server connection failed."));
}
if (Global::get().s.bReconnect) {
qaServerDisconnect->setEnabled(true);
if (bRetryServer) {
qtReconnect->start();
}
}
}
void MainWindow::trayAboutToShow() {
bool top = false;
QPoint p = qstiIcon->geometry().center();
if (p.isNull()) {
p = QCursor::pos();
}
QScreen *screen = Mumble::Screen::screenAt(p);
if (screen) {
QRect qr = screen->geometry();
if (p.y() < (qr.height() / 2))
top = true;
qmTray->clear();
if (top) {
qmTray->addAction(qaQuit);
qmTray->addAction(qaShow);
qmTray->addSeparator();
qmTray->addAction(qaAudioDeaf);
qmTray->addAction(qaAudioMute);
} else {
qmTray->addAction(qaAudioMute);
qmTray->addAction(qaAudioDeaf);
qmTray->addSeparator();
qmTray->addAction(qaShow);
qmTray->addAction(qaQuit);
}
}
}
void MainWindow::showRaiseWindow() {
if (isMinimized()) {
setWindowState((windowState() & ~Qt::WindowMinimized) | Qt::WindowActive);
}
show();
raise();
activateWindow();
}
void MainWindow::on_Icon_activated(QSystemTrayIcon::ActivationReason reason) {
switch (reason) {
case QSystemTrayIcon::Trigger:
case QSystemTrayIcon::DoubleClick:
case QSystemTrayIcon::MiddleClick:
if (isMinimized()) {
showRaiseWindow();
} else {
showMinimized();
}
default:
break;
}
}
void MainWindow::on_qaTalkingUIToggle_triggered() {
if (!Global::get().talkingUI) {
qCritical("MainWindow: Attempting to show Talking UI before it has been created!");
return;
}
Global::get().talkingUI->setVisible(!Global::get().talkingUI->isVisible());
Global::get().s.bShowTalkingUI = Global::get().talkingUI->isVisible();
}
/**
* This function updates the qteChat bar default text according to
* the selected user/channel in the users treeview.
*/
void MainWindow::qtvUserCurrentChanged(const QModelIndex &, const QModelIndex &) {
updateChatBar();
}
void MainWindow::updateChatBar() {
User *p = pmModel->getUser(qtvUsers->currentIndex());
Channel *c = pmModel->getChannel(qtvUsers->currentIndex());
if (Global::get().uiSession == 0) {
qteChat->setDefaultText(tr("<center>Not connected</center>"), true);
} else if (!Global::get().s.bChatBarUseSelection || !p || p->uiSession == Global::get().uiSession) {
// Channel tree target
if (!Global::get().s.bChatBarUseSelection || !c) // If no channel selected fallback to current one
c = ClientUser::get(Global::get().uiSession)->cChannel;
qteChat->setDefaultText(
tr("<center>Type message to channel '%1' here</center>").arg(c->qsName.toHtmlEscaped()));
} else {
// User target
qteChat->setDefaultText(tr("<center>Type message to user '%1' here</center>").arg(p->qsName.toHtmlEscaped()));
}
updateMenuPermissions();
}
void MainWindow::customEvent(QEvent *evt) {
if (evt->type() == MB_QEVENT) {
MessageBoxEvent *mbe = static_cast< MessageBoxEvent * >(evt);
Global::get().l->log(Log::Information, mbe->msg);
return;
} else if (evt->type() == OU_QEVENT) {
OpenURLEvent *oue = static_cast< OpenURLEvent * >(evt);
openUrl(oue->url);
return;
} else if (evt->type() != SERVERSEND_EVENT) {
return;
}
ServerHandlerMessageEvent *shme = static_cast< ServerHandlerMessageEvent * >(evt);
#ifdef QT_NO_DEBUG
# define PROCESS_MUMBLE_TCP_MESSAGE(name, value) \
case Mumble::Protocol::TCPMessageType::name: { \
MumbleProto::name msg; \
if (msg.ParseFromArray(shme->qbaMsg.constData(), shme->qbaMsg.size())) \
msg##name(msg); \
break; \
}
#else
# define PROCESS_MUMBLE_TCP_MESSAGE(name, value) \
case Mumble::Protocol::TCPMessageType::name: { \
MumbleProto::name msg; \
if (msg.ParseFromArray(shme->qbaMsg.constData(), shme->qbaMsg.size())) { \
printf("%s:\n", #name); \
msg.PrintDebugString(); \
msg##name(msg); \
} \
break; \
}
#endif
switch (shme->type) { MUMBLE_ALL_TCP_MESSAGES }
#undef PROCESS_MUMBLE_TCP_MESSAGE
}
void MainWindow::on_qteLog_anchorClicked(const QUrl &url) {
if (!handleSpecialContextMenu(url, QCursor::pos(), true)) {
#if defined(Q_OS_MAC) && defined(USE_OVERLAY)
// Clicking a link can cause the user's default browser to pop up while
// we're intercepting all events. This can be very confusing (because
// the user can't click on anything before they dismiss the overlay
// by hitting their toggle hotkey), so let's disallow clicking links
// when embedded into the overlay for now.
if (Global::get().ocIntercept)
return;
#endif
if (url.scheme() != QLatin1String("file") && url.scheme() != QLatin1String("qrc") && !url.isRelative())
QDesktopServices::openUrl(url);
}
}
void MainWindow::on_qteLog_highlighted(const QUrl &url) {
if (url.scheme() == QString::fromLatin1("clientid") || url.scheme() == QString::fromLatin1("channelid"))
return;
if (!url.isValid())
QToolTip::hideText();
else {
if (isActiveWindow()) {
QToolTip::showText(QCursor::pos(), url.toString(), qteLog, QRect());
}
}
}
void MainWindow::context_triggered() {
QAction *a = qobject_cast< QAction * >(sender());
Channel *c = pmModel->getChannel(qtvUsers->currentIndex());
ClientUser *p = pmModel->getUser(qtvUsers->currentIndex());
MumbleProto::ContextAction mpca;
mpca.set_action(u8(a->data().toString()));
if (p && p->uiSession)
mpca.set_session(p->uiSession);
if (c)
mpca.set_channel_id(c->iId);
Global::get().sh->sendMessage(mpca);
}
/**
* Presents a file open dialog, opens the selected picture and returns it.
* @return Pair consisting of the raw file contents and the image. Uninitialized on error or cancel.
*/
QPair< QByteArray, QImage > MainWindow::openImageFile() {
QPair< QByteArray, QImage > retval;
QString fname =
QFileDialog::getOpenFileName(this, tr("Choose image file"), getImagePath(), tr("Images (*.png *.jpg *.jpeg)"));
if (fname.isNull())
return retval;
QFile f(fname);
if (!f.open(QIODevice::ReadOnly)) {
QMessageBox::warning(this, tr("Failed to load image"), tr("Could not open file for reading."));
return retval;
}
updateImagePath(fname);
QByteArray qba = f.readAll();
f.close();
QBuffer qb(&qba);
qb.open(QIODevice::ReadOnly);
QImageReader qir;
qir.setAutoDetectImageFormat(false);
QByteArray fmt;
if (!RichTextImage::isValidImage(qba, fmt)) {
QMessageBox::warning(this, tr("Failed to load image"), tr("Image format not recognized."));
return retval;
}
qir.setFormat(fmt);
qir.setDevice(&qb);
QImage img = qir.read();
if (img.isNull()) {
QMessageBox::warning(this, tr("Failed to load image"), tr("Image format not recognized."));
return retval;
}
retval.first = qba;
retval.second = img;
return retval;
}
void MainWindow::logChangeNotPermanent(const QString &actionName, ClientUser *const p) const {
Global::get().l->log(
Log::Warning,
QObject::tr(
"\"%1\" could not be saved permanently and is lost on restart because %2 does not have a certificate.")
.arg(actionName)
.arg(Log::formatClientUser(p, Log::Target)));
}
void MainWindow::destroyUserInformation() {
UserInformation *ui = static_cast< UserInformation * >(sender());
QMap< unsigned int, UserInformation * >::iterator i;
for (i = qmUserInformations.begin(); i != qmUserInformations.end(); ++i) {
if (i.value() == ui) {
qmUserInformations.erase(i);
return;
}
}
}
void MainWindow::openServerConnectDialog(bool autoconnect) {
ConnectDialog *cd = new ConnectDialog(this, autoconnect);
int res = cd->exec();
if (cd->qsServer.isEmpty() || (cd->usPort == 0) || cd->qsUsername.isEmpty())
res = QDialog::Rejected;
if (res == QDialog::Accepted) {
recreateServerHandler();
qsDesiredChannel = QString();
rtLast = MumbleProto::Reject_RejectType_None;
bRetryServer = true;
qaServerDisconnect->setEnabled(true);
Global::get().l->log(
Log::Information,
tr("Connecting to server %1.").arg(Log::msgColor(cd->qsServer.toHtmlEscaped(), Log::Server)));
Global::get().sh->setConnectionInfo(cd->qsServer, cd->usPort, cd->qsUsername, cd->qsPassword);
Global::get().sh->start(QThread::TimeCriticalPriority);
}
delete cd;
}
void MainWindow::disconnectFromServer() {
if (qtReconnect->isActive()) {
qtReconnect->stop();
qaServerDisconnect->setEnabled(false);
}
if (Global::get().sh && Global::get().sh->isRunning())
Global::get().sh->disconnect();
}
void MainWindow::openServerInformationDialog() {
ServerInformation *infoDialog = new ServerInformation(this);
infoDialog->show();
}
void MainWindow::openServerTokensDialog() {
if (tokenEdit) {
tokenEdit->reject();
delete tokenEdit;
tokenEdit = nullptr;
}
tokenEdit = new Tokens(this);
tokenEdit->show();
}
void MainWindow::openServerUserListDialog() {
Global::get().sh->requestUserList();
if (userEdit) {
userEdit->reject();
delete userEdit;
userEdit = nullptr;
}
}
void MainWindow::openServerBanListDialog() {
Global::get().sh->requestBanList();
if (banEdit) {
banEdit->reject();
delete banEdit;
banEdit = nullptr;
}
}
void MainWindow::toggleSelfPrioritySpeaker() {
ClientUser *p = ClientUser::get(Global::get().uiSession);
if (!p)
return;
MumbleProto::UserState mpus;
mpus.set_session(p->uiSession);
mpus.set_priority_speaker(!p->bPrioritySpeaker);
Global::get().sh->sendMessage(mpus);
}
void MainWindow::recording() {
if (voiceRecorderDialog) {
voiceRecorderDialog->show();
voiceRecorderDialog->raise();
voiceRecorderDialog->activateWindow();
} else {
voiceRecorderDialog = new VoiceRecorderDialog(this);
connect(voiceRecorderDialog, SIGNAL(finished(int)), this, SLOT(voiceRecorderDialog_finished(int)));
QObject::connect(Global::get().sh.get(), &ServerHandler::disconnected, voiceRecorderDialog, &QDialog::reject);
voiceRecorderDialog->show();
}
}
void MainWindow::openSelfCommentDialog() {
ClientUser *p = ClientUser::get(Global::get().uiSession);
if (!p)
return;
if (!p->qbaCommentHash.isEmpty() && p->qsComment.isEmpty()) {
p->qsComment = QString::fromUtf8(Global::get().db->blob(p->qbaCommentHash));
if (p->qsComment.isEmpty()) {
pmModel->uiSessionComment = ~(p->uiSession);
MumbleProto::RequestBlob mprb;
mprb.add_session_comment(p->uiSession);
Global::get().sh->sendMessage(mprb);
return;
}
}
unsigned int session = p->uiSession;
::TextMessage *texm = new ::TextMessage(this, tr("Change your comment"));
texm->rteMessage->setText(p->qsComment);
int res = texm->exec();
p = ClientUser::get(session);
if (p && (res == QDialog::Accepted)) {
const QString &msg = texm->message();
MumbleProto::UserState mpus;
mpus.set_session(session);
mpus.set_comment(u8(msg));
Global::get().sh->sendMessage(mpus);
if (!msg.isEmpty())
Global::get().db->setBlob(sha1(msg), msg.toUtf8());
}
delete texm;
}
void MainWindow::changeServerTexture() {
QPair< QByteArray, QImage > choice = openImageFile();
if (choice.first.isEmpty())
return;
const QImage &img = choice.second;
if ((img.height() <= 1024) && (img.width() <= 1024))
Global::get().sh->setUserTexture(Global::get().uiSession, choice.first);
}
void MainWindow::removeServerTexture() {
Global::get().sh->setUserTexture(Global::get().uiSession, QByteArray());
}
void MainWindow::selfRegister() {
ClientUser *p = ClientUser::get(Global::get().uiSession);
if (!p)
return;
QMessageBox::StandardButton result;
result =
QMessageBox::question(this, tr("Register yourself as %1").arg(p->qsName),
tr("<p>You are about to register yourself on this server. This action cannot be undone, "
"and your username cannot be changed once this is done. You will forever be known as "
"'%1' on this server.</p><p>Are you sure you want to register yourself?</p>")
.arg(p->qsName.toHtmlEscaped()),
QMessageBox::Yes | QMessageBox::No);
if (result == QMessageBox::Yes)
Global::get().sh->registerUser(p->uiSession);
}
void MainWindow::openAudioStatsDialog() {
AudioStats *as = new AudioStats(this);
as->show();
}
void MainWindow::openConfigDialog() {
ConfigDialog *dlg = new ConfigDialog(this);
Global::get().inConfigUI = true;
QObject::connect(dlg, &ConfigDialog::settingsAccepted, Global::get().talkingUI, &TalkingUI::on_settingsChanged);
if (dlg->exec() == QDialog::Accepted) {
setupView(false);
updateTransmitModeComboBox(Global::get().s.atTransmit);
updateTrayIcon();
updateUserModel();
if (Global::get().s.requireRestartToApply) {
if (Global::get().s.requireRestartToApply
&& QMessageBox::question(
this, tr("Restart Mumble?"),
tr("Some settings will only apply after a restart of Mumble. Restart Mumble now?"),
QMessageBox::Yes | QMessageBox::No)
== QMessageBox::Yes) {
forceQuit = true;
restartOnQuit = true;
close();
}
}
}
Global::get().inConfigUI = false;
delete dlg;
}
void MainWindow::openAudioWizardDialog() {
AudioWizard *aw = new AudioWizard(this);
aw->exec();
delete aw;
}
void MainWindow::openCertWizardDialog() {
CertWizard *cw = new CertWizard(this);
cw->exec();
delete cw;
}
void MainWindow::enableAudioTTS(bool enable) {
Global::get().s.bTTS = enable;
}
void MainWindow::openAboutDialog() {
AboutDialog adAbout(this);
adAbout.exec();
}
void MainWindow::openAboutQtDialog() {
QMessageBox::aboutQt(this, tr("About Qt"));
}
void MainWindow::versionCheck() {
new VersionCheck(false, this);
}
void MainWindow::on_muteCuePopup_triggered() {
if (Global::get().s.muteCueShown || Global::get().inConfigUI) {
return;
}
Global::get().s.muteCueShown = true;
QMessageBox mb(
QMessageBox::Warning, QLatin1String("Mumble"),
tr("That sound was the mute cue. It activates when you speak while muted. Would you like to keep it enabled?"),
QMessageBox::NoButton, this);
QPushButton *accept = mb.addButton(tr("Yes"), QMessageBox::YesRole);
QPushButton *reject = mb.addButton(tr("No"), QMessageBox::NoRole);
mb.setDefaultButton(accept);
mb.setEscapeButton(accept);
mb.exec();
if (mb.clickedButton() == reject) {
Global::get().s.bTxMuteCue = false;
}
}