885 lines
29 KiB
C++
885 lines
29 KiB
C++
// Copyright 2020-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 "TalkingUI.h"
|
|
#include "Channel.h"
|
|
#include "ChannelListenerManager.h"
|
|
#include "ClientUser.h"
|
|
#include "MainWindow.h"
|
|
#include "TalkingUIComponent.h"
|
|
#include "UserModel.h"
|
|
#include "widgets/MultiStyleWidgetWrapper.h"
|
|
#include "Global.h"
|
|
|
|
#include <QGroupBox>
|
|
#include <QGuiApplication>
|
|
#include <QHBoxLayout>
|
|
#include <QItemSelectionModel>
|
|
#include <QLabel>
|
|
#include <QModelIndex>
|
|
#include <QMouseEvent>
|
|
#include <QPalette>
|
|
#include <QScreen>
|
|
#include <QTextDocumentFragment>
|
|
#include <QVBoxLayout>
|
|
#include <QtCore/QDateTime>
|
|
#include <QtCore/QStringList>
|
|
#include <QtCore/QTimer>
|
|
#include <QtGui/QPainter>
|
|
#include <QtGui/QPixmap>
|
|
|
|
#include <algorithm>
|
|
#include <cassert>
|
|
|
|
TalkingUI::TalkingUI(QWidget *parent) : QWidget(parent), m_containers(), m_currentSelection(nullptr) {
|
|
setupUI();
|
|
}
|
|
|
|
int TalkingUI::findContainer(int associatedChannelID, ContainerType type) const {
|
|
for (std::size_t i = 0; i < m_containers.size(); i++) {
|
|
const std::unique_ptr< TalkingUIContainer > ¤tContainer = m_containers[i];
|
|
|
|
if (currentContainer->getType() == type && currentContainer->getAssociatedChannelID() == associatedChannelID) {
|
|
return static_cast< int >(i);
|
|
}
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
std::unique_ptr< TalkingUIContainer > TalkingUI::removeContainer(const TalkingUIContainer &container) {
|
|
return removeContainer(container.getAssociatedChannelID(), container.getType());
|
|
}
|
|
|
|
std::unique_ptr< TalkingUIContainer > TalkingUI::removeContainer(int associatedChannelID, ContainerType type) {
|
|
int index = findContainer(associatedChannelID, type);
|
|
|
|
std::unique_ptr< TalkingUIContainer > container(nullptr);
|
|
|
|
if (index >= 0) {
|
|
// Move the container out of the vector
|
|
container = std::move(m_containers[static_cast< std::size_t >(index)]);
|
|
m_containers.erase(m_containers.begin() + index);
|
|
|
|
// If the container is currently selected, clear the selection
|
|
if (isSelected(*container)) {
|
|
setSelection(EmptySelection());
|
|
}
|
|
}
|
|
|
|
return container;
|
|
}
|
|
|
|
std::unique_ptr< TalkingUIContainer > TalkingUI::removeIfSuperfluous(const TalkingUIContainer &container) {
|
|
if (container.isEmpty() && !container.isPermanent()) {
|
|
return removeContainer(container);
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
struct container_ptr_less {
|
|
bool operator()(const std::unique_ptr< TalkingUIContainer > &first,
|
|
const std::unique_ptr< TalkingUIContainer > &second) {
|
|
return *first < *second;
|
|
}
|
|
};
|
|
|
|
void TalkingUI::sortContainers() {
|
|
// Remove all containers from the UI
|
|
for (auto ¤tContainer : m_containers) {
|
|
layout()->removeWidget(currentContainer->getWidget());
|
|
}
|
|
|
|
// Sort the containers
|
|
std::sort(m_containers.begin(), m_containers.end(), container_ptr_less());
|
|
|
|
// Add them again in the order they appear in the vector
|
|
for (auto ¤tContainer : m_containers) {
|
|
layout()->addWidget(currentContainer->getWidget());
|
|
}
|
|
}
|
|
|
|
TalkingUIUser *TalkingUI::findUser(unsigned int userSession) {
|
|
for (auto ¤tContainer : m_containers) {
|
|
TalkingUIEntry *entry = currentContainer->get(userSession, EntryType::USER);
|
|
|
|
if (entry) {
|
|
// We know that it must be a TalkingUIUser since that is what we searched for
|
|
return static_cast< TalkingUIUser * >(entry);
|
|
}
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
void TalkingUI::removeUser(unsigned int userSession) {
|
|
TalkingUIUser *userEntry = findUser(userSession);
|
|
|
|
if (userEntry) {
|
|
// If the user that is going to be deleted is currently selected, clear the selection
|
|
if (isSelected(*userEntry)) {
|
|
setSelection(EmptySelection());
|
|
}
|
|
|
|
TalkingUIContainer *userContainer = userEntry->getContainer();
|
|
|
|
userContainer->removeEntry(userEntry);
|
|
|
|
removeIfSuperfluous(*userContainer);
|
|
|
|
updateUI();
|
|
}
|
|
}
|
|
|
|
void TalkingUI::addListener(const ClientUser *user, const Channel *channel) {
|
|
TalkingUIChannelListener *existingEntry = findListener(user->uiSession, channel->iId);
|
|
|
|
if (!existingEntry) {
|
|
// Only create entry if it doesn't exist yet
|
|
|
|
// First make sure the channel exists
|
|
addChannel(channel);
|
|
|
|
std::unique_ptr< TalkingUIContainer > &channelContainer = m_containers[static_cast< std::size_t >(
|
|
findContainer(static_cast< int >(channel->iId), ContainerType::CHANNEL))];
|
|
|
|
std::unique_ptr< TalkingUIChannelListener > listenerEntry =
|
|
std::make_unique< TalkingUIChannelListener >(*user, *channel);
|
|
|
|
channelContainer->addEntry(std::move(listenerEntry));
|
|
|
|
sortContainers();
|
|
}
|
|
}
|
|
|
|
TalkingUIChannelListener *TalkingUI::findListener(unsigned int userSession, unsigned int channelID) {
|
|
int channelIndex = findContainer(static_cast< int >(channelID), ContainerType::CHANNEL);
|
|
|
|
if (channelIndex >= 0) {
|
|
std::unique_ptr< TalkingUIContainer > &channelContainer =
|
|
m_containers[static_cast< std::size_t >(channelIndex)];
|
|
|
|
TalkingUIEntry *entry = channelContainer->get(userSession, EntryType::LISTENER);
|
|
|
|
if (entry) {
|
|
return static_cast< TalkingUIChannelListener * >(entry);
|
|
}
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
void TalkingUI::removeListener(unsigned int userSession, unsigned int channelID) {
|
|
TalkingUIChannelListener *listenerEntry = findListener(userSession, channelID);
|
|
|
|
if (listenerEntry) {
|
|
// If the user that is going to be deleted is currently selected, clear the selection
|
|
if (isSelected(*listenerEntry)) {
|
|
setSelection(EmptySelection());
|
|
}
|
|
|
|
TalkingUIContainer *userContainer = listenerEntry->getContainer();
|
|
|
|
userContainer->removeEntry(listenerEntry);
|
|
|
|
removeIfSuperfluous(*userContainer);
|
|
|
|
updateUI();
|
|
}
|
|
}
|
|
|
|
void TalkingUI::removeAllListeners() {
|
|
// Find all listener entries
|
|
std::vector< TalkingUIEntry * > entriesToBeRemoved;
|
|
for (auto ¤tContainer : m_containers) {
|
|
for (auto ¤tEntry : currentContainer->getEntries()) {
|
|
if (currentEntry->getType() == EntryType::LISTENER) {
|
|
entriesToBeRemoved.push_back(currentEntry.get());
|
|
}
|
|
}
|
|
}
|
|
|
|
// remove the individual entries
|
|
for (auto currentEntry : entriesToBeRemoved) {
|
|
TalkingUIContainer *container = currentEntry->getContainer();
|
|
|
|
container->removeEntry(currentEntry);
|
|
|
|
removeIfSuperfluous(*container);
|
|
}
|
|
|
|
// if we removed something, update the UI
|
|
if (entriesToBeRemoved.size() > 0) {
|
|
updateUI();
|
|
}
|
|
}
|
|
|
|
void TalkingUI::setupUI() {
|
|
QVBoxLayout *layout = new QVBoxLayout;
|
|
|
|
setLayout(layout);
|
|
|
|
setWindowTitle(QObject::tr("Talking UI"));
|
|
|
|
setAttribute(Qt::WA_ShowWithoutActivating);
|
|
setWindowFlags(Qt::Dialog | Qt::WindowStaysOnTopHint);
|
|
|
|
// Hide the "?" (context help) button in the title bar of the widget as we don't want
|
|
// that due to it taking valuable screen space so that the title can't be displayed
|
|
// properly and as the TalkingUI doesn't provide context help anyways, this is not a big loss.
|
|
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
|
|
|
|
connect(Global::get().mw->qtvUsers->selectionModel(), &QItemSelectionModel::currentChanged, this,
|
|
&TalkingUI::on_mainWindowSelectionChanged);
|
|
}
|
|
|
|
void TalkingUI::setFontSize(MultiStyleWidgetWrapper &widgetWrapper) {
|
|
const double fontFactor = Global::get().s.iTalkingUI_RelativeFontSize / 100.0;
|
|
const int origLineHeight = QFontMetrics(font()).height();
|
|
|
|
if (font().pixelSize() >= 0) {
|
|
// font specified in pixels
|
|
uint32_t pixelSize = static_cast< uint32_t >(std::max(fontFactor * font().pixelSize(), 1.0));
|
|
widgetWrapper.setFontSize(pixelSize, true);
|
|
} else {
|
|
// font specified in points
|
|
uint32_t pointSize = static_cast< uint32_t >(std::max(fontFactor * font().pointSize(), 1.0));
|
|
widgetWrapper.setFontSize(pointSize, false);
|
|
}
|
|
|
|
m_currentLineHeight = static_cast< int >(std::max(origLineHeight * fontFactor, 1.0));
|
|
}
|
|
|
|
void TalkingUI::updateStatusIcons(const ClientUser *user) {
|
|
TalkingUIUser *userEntry = findUser(user->uiSession);
|
|
|
|
if (!userEntry) {
|
|
return;
|
|
}
|
|
|
|
TalkingUIUser::UserStatus status;
|
|
status.muted = user->bMute;
|
|
status.selfMuted = user->bSelfMute;
|
|
status.localMuted = user->bLocalMute;
|
|
status.deafened = user->bDeaf;
|
|
status.selfDeafened = user->bSelfDeaf;
|
|
|
|
userEntry->setStatus(status);
|
|
}
|
|
|
|
void TalkingUI::hideUser(unsigned int session) {
|
|
removeUser(session);
|
|
|
|
updateUI();
|
|
}
|
|
|
|
QString createChannelName(const Channel *chan, bool abbreviateName, int minPrefixChars, int minPostfixChars,
|
|
int idealMaxChars, int parentLevel, const QString &separator,
|
|
const QString &abbreviationIndicator, bool abbreviateCurrentChannel) {
|
|
if (!abbreviateName) {
|
|
return chan->qsName;
|
|
}
|
|
|
|
// Assemble list of relevant channel names (representing the channel hierarchy
|
|
QStringList nameList;
|
|
do {
|
|
nameList << chan->qsName;
|
|
|
|
chan = chan->cParent;
|
|
} while (chan && nameList.size() < (parentLevel + 1));
|
|
|
|
const bool reachedRoot = !chan;
|
|
|
|
// We also want to abbreviate names that nominally have the same amount of characters before and
|
|
// after abbreviation. However as we're typically not using mono-spaced fonts, the abbreviation
|
|
// indicator might still occupy less space than the original text.
|
|
const int abbreviableSize = minPrefixChars + minPostfixChars + abbreviationIndicator.size();
|
|
|
|
// Iterate over all names and check how many of them could be abbreviated
|
|
int totalCharCount = reachedRoot ? separator.size() : 0;
|
|
for (int i = 0; i < nameList.size(); i++) {
|
|
totalCharCount += nameList[i].size();
|
|
|
|
if (i + 1 < nameList.size()) {
|
|
// Account for the separator's size as well
|
|
totalCharCount += separator.size();
|
|
}
|
|
}
|
|
|
|
QString groupName = reachedRoot ? separator : QString();
|
|
|
|
for (int i = nameList.size() - 1; i >= 0; i--) {
|
|
if (totalCharCount > idealMaxChars && nameList[i].size() >= abbreviableSize
|
|
&& (abbreviateCurrentChannel || i != 0)) {
|
|
// Abbreviate the names as much as possible
|
|
groupName += nameList[i].left(minPrefixChars) + abbreviationIndicator + nameList[i].right(minPostfixChars);
|
|
} else {
|
|
groupName += nameList[i];
|
|
}
|
|
|
|
if (i != 0) {
|
|
groupName += separator;
|
|
}
|
|
}
|
|
|
|
return groupName;
|
|
}
|
|
|
|
void TalkingUI::addChannel(const Channel *channel) {
|
|
if (findContainer(static_cast< int >(channel->iId), ContainerType::CHANNEL) < 0) {
|
|
// Create a QGroupBox for this channel
|
|
const QString channelName = createChannelName(
|
|
channel, Global::get().s.bTalkingUI_AbbreviateChannelNames, Global::get().s.iTalkingUI_PrefixCharCount,
|
|
Global::get().s.iTalkingUI_PostfixCharCount, Global::get().s.iTalkingUI_MaxChannelNameLength,
|
|
Global::get().s.iTalkingUI_ChannelHierarchyDepth, Global::get().s.qsHierarchyChannelSeparator,
|
|
Global::get().s.qsTalkingUI_AbbreviationReplacement, Global::get().s.bTalkingUI_AbbreviateCurrentChannel);
|
|
|
|
std::unique_ptr< TalkingUIChannel > channelContainer =
|
|
std::make_unique< TalkingUIChannel >(channel->iId, channelName, *this);
|
|
|
|
QWidget *channelWidget = channelContainer->getWidget();
|
|
|
|
setFontSize(channelContainer->getStylableWidget());
|
|
|
|
layout()->addWidget(channelWidget);
|
|
|
|
m_containers.push_back(std::move(channelContainer));
|
|
}
|
|
}
|
|
|
|
TalkingUIUser *TalkingUI::findOrAddUser(const ClientUser *user) {
|
|
TalkingUIUser *oldUserEntry = findUser(user->uiSession);
|
|
bool nameMatches = true;
|
|
|
|
if (oldUserEntry) {
|
|
// We also verify whether the name for that user matches up (if it is contained in m_entries) in case
|
|
// a user didn't get removed from the map but its ID got reused by a new client.
|
|
|
|
nameMatches = oldUserEntry->getName() == user->qsName;
|
|
|
|
if (!nameMatches) {
|
|
// Hide and remove the stale user
|
|
hideUser(user->uiSession);
|
|
|
|
// reset pointer
|
|
oldUserEntry = nullptr;
|
|
}
|
|
}
|
|
|
|
// Make sure that the user's channel exists in this UI.
|
|
// Note that this has to be done **after** the name check above as
|
|
// the user might have been renamed in which case the abovementioned
|
|
// code block removes the old user entry. If that user was the only
|
|
// client in its channel, the channel gets removed as well. However
|
|
// the code below expects the user's channel to exist.
|
|
addChannel(user->cChannel);
|
|
|
|
if (!oldUserEntry || !nameMatches) {
|
|
// Create an entry for this user
|
|
bool isSelf = Global::get().uiSession == user->uiSession;
|
|
|
|
int channelIndex = findContainer(static_cast< int >(user->cChannel->iId), ContainerType::CHANNEL);
|
|
if (channelIndex < 0) {
|
|
qCritical("TalkingUI::findOrAddUser User's channel does not exist!");
|
|
return nullptr;
|
|
}
|
|
std::unique_ptr< TalkingUIContainer > &channelContainer =
|
|
m_containers[static_cast< std::size_t >(channelIndex)];
|
|
if (!channelContainer) {
|
|
qCritical("TalkingUI::findOrAddUser requesting unknown channel!");
|
|
return nullptr;
|
|
}
|
|
|
|
std::unique_ptr< TalkingUIUser > userEntry = std::make_unique< TalkingUIUser >(*user);
|
|
TalkingUIUser *newUserEntry = userEntry.get();
|
|
|
|
// * 1000 as the setting is in seconds whereas the timer expects milliseconds
|
|
userEntry->setLifeTime(static_cast< unsigned int >(Global::get().s.iTalkingUI_SilentUserLifeTime) * 1000);
|
|
|
|
bool isLocalUserAndLocalUserAlwaysVisible = isSelf && Global::get().s.bTalkingUI_LocalUserStaysVisible;
|
|
bool usersAlwaysVisible = Global::get().s.talkingUI_UsersAlwaysVisible;
|
|
bool isUserAlwaysVisible = usersAlwaysVisible || isLocalUserAndLocalUserAlwaysVisible;
|
|
userEntry->restrictLifetime(!isUserAlwaysVisible);
|
|
|
|
userEntry->setPriority(isSelf ? EntryPriority::HIGH : EntryPriority::DEFAULT);
|
|
|
|
QObject::connect(user, &ClientUser::localVolumeAdjustmentsChanged, this,
|
|
&TalkingUI::on_userLocalVolumeAdjustmentsChanged);
|
|
|
|
// If this user is currently selected, mark him/her as such
|
|
if (Global::get().mw && Global::get().mw->pmModel && Global::get().mw->pmModel->getSelectedUser() == user) {
|
|
setSelection(UserSelection(userEntry->getWidget(), userEntry->getAssociatedUserSession()));
|
|
}
|
|
|
|
// As the font size of the newly created widget did not adapt to any StyleSheet it might have inherited
|
|
// from its parents (the channel box), the size the talking icon is initialized to in the entry's
|
|
// constructor is incorrect. Therefore we have to explicitly update it here.
|
|
userEntry->setIconSize(m_currentLineHeight);
|
|
|
|
// Actually add the user to the respective channel
|
|
channelContainer->addEntry(std::move(userEntry));
|
|
|
|
sortContainers();
|
|
|
|
return newUserEntry;
|
|
} else {
|
|
return oldUserEntry;
|
|
}
|
|
}
|
|
|
|
void TalkingUI::moveUserToChannel(unsigned int userSession, unsigned int channelID) {
|
|
int targetChanIndex = findContainer(static_cast< int >(channelID), ContainerType::CHANNEL);
|
|
|
|
if (targetChanIndex < 0) {
|
|
qCritical("TalkingUI::moveUserToChannel Can't find channel for speaker");
|
|
return;
|
|
}
|
|
|
|
std::unique_ptr< TalkingUIContainer > &targetChannel = m_containers[static_cast< std::size_t >(targetChanIndex)];
|
|
|
|
if (targetChannel->contains(userSession, EntryType::USER)) {
|
|
// The given user is already in the target channel - nothing to do
|
|
return;
|
|
}
|
|
|
|
// Iterate all containers in order to find the one the user is currently in
|
|
TalkingUIUser *userEntry = findUser(userSession);
|
|
|
|
if (userEntry) {
|
|
TalkingUIContainer *oldContainer = userEntry->getContainer();
|
|
|
|
targetChannel->addEntry(oldContainer->removeEntry(userEntry));
|
|
|
|
removeIfSuperfluous(*oldContainer);
|
|
|
|
sortContainers();
|
|
} else {
|
|
qCritical("TalkingUI::moveUserToChannel Unable to locate user");
|
|
return;
|
|
}
|
|
|
|
updateUI();
|
|
}
|
|
|
|
void TalkingUI::updateUI() {
|
|
// Use timer to execute this after all other events have been processed
|
|
QTimer::singleShot(0, [this]() { adjustSize(); });
|
|
}
|
|
|
|
void TalkingUI::setSelection(const TalkingUISelection &selection) {
|
|
if (dynamic_cast< const EmptySelection * >(&selection)) {
|
|
// The selection is set to an empty selection
|
|
if (m_currentSelection) {
|
|
// There currently is a selection -> clear and remove it
|
|
m_currentSelection->discard();
|
|
m_currentSelection.reset();
|
|
}
|
|
} else {
|
|
if (m_currentSelection) {
|
|
if (selection == *m_currentSelection) {
|
|
// Selection hasn't actually changed
|
|
return;
|
|
}
|
|
|
|
// Discard old selection (it'll get deleted on re-assignment below)
|
|
m_currentSelection->discard();
|
|
}
|
|
|
|
// Use the new selection (which at this point we know is not the empty selection)
|
|
m_currentSelection = selection.cloneToHeap();
|
|
|
|
m_currentSelection->apply();
|
|
m_currentSelection->syncToMainWindow();
|
|
}
|
|
}
|
|
|
|
bool TalkingUI::isSelected(const TalkingUIComponent &component) const {
|
|
if (m_currentSelection) {
|
|
return *m_currentSelection == component.getWidget();
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void TalkingUI::mousePressEvent(QMouseEvent *event) {
|
|
bool foundTarget = false;
|
|
|
|
for (auto ¤tContainer : m_containers) {
|
|
QRect containerArea(currentContainer->getWidget()->mapToGlobal({ 0, 0 }),
|
|
currentContainer->getWidget()->size());
|
|
|
|
if (containerArea.contains(event->globalPos())) {
|
|
for (auto ¤tEntry : currentContainer->getEntries()) {
|
|
QRect entryArea(currentEntry->getWidget()->mapToGlobal({ 0, 0 }), currentEntry->getWidget()->size());
|
|
|
|
if (entryArea.contains(event->globalPos())) {
|
|
switch (currentEntry->getType()) {
|
|
case EntryType::USER:
|
|
setSelection(
|
|
UserSelection(currentEntry->getWidget(), currentEntry->getAssociatedUserSession()));
|
|
break;
|
|
case EntryType::LISTENER:
|
|
TalkingUIChannelListener *listenerEntry =
|
|
static_cast< TalkingUIChannelListener * >(currentEntry.get());
|
|
setSelection(ListenerSelection(
|
|
listenerEntry->getWidget(), listenerEntry->getAssociatedUserSession(),
|
|
static_cast< unsigned int >(listenerEntry->getAssociatedChannelID())));
|
|
break;
|
|
}
|
|
|
|
foundTarget = true;
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!foundTarget) {
|
|
// Select channel itself
|
|
setSelection(ChannelSelection(currentContainer->getWidget(),
|
|
static_cast< unsigned int >(currentContainer->getAssociatedChannelID())));
|
|
|
|
foundTarget = true;
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (foundTarget) {
|
|
if (event->button() == Qt::RightButton && Global::get().mw) {
|
|
// If an entry is selected and the right mouse button was clicked, we pretend as if the user had clicked on
|
|
// the client in the MainWindow. For this to work we map the global mouse position to the local coordinate
|
|
// system of the UserView in the MainWindow. The function will use some internal logic to determine the user
|
|
// to invoke the context menu on but if that fails (which in this case it will), it'll fall back to the
|
|
// currently selected item. This item we have updated to the correct one with the setSelection() call above
|
|
// resulting in the proper context menu being shown at the position of the mouse which in this case is in
|
|
// the TalkingUI.
|
|
QMetaObject::invokeMethod(Global::get().mw, "on_qtvUsers_customContextMenuRequested", Qt::QueuedConnection,
|
|
Q_ARG(QPoint, Global::get().mw->qtvUsers->mapFromGlobal(event->globalPos())),
|
|
Q_ARG(bool, false));
|
|
}
|
|
} else {
|
|
// Clear selection
|
|
setSelection(EmptySelection());
|
|
}
|
|
|
|
updateUI();
|
|
}
|
|
|
|
void TalkingUI::setVisible(bool visible) {
|
|
if (visible) {
|
|
adjustSize();
|
|
}
|
|
|
|
QWidget::setVisible(visible);
|
|
}
|
|
|
|
QSize TalkingUI::sizeHint() const {
|
|
// Prefer to occupy at least 10% of the screen's size
|
|
// This aims to be a good compromise between not being in the way and not
|
|
// being too small to being handled properly.
|
|
int width = static_cast< int >(QGuiApplication::screens()[0]->availableSize().width() * 0.1);
|
|
|
|
return { width, 0 };
|
|
}
|
|
|
|
QSize TalkingUI::minimumSizeHint() const {
|
|
return { 0, 0 };
|
|
}
|
|
|
|
void TalkingUI::on_talkingStateChanged() {
|
|
ClientUser *user = qobject_cast< ClientUser * >(sender());
|
|
|
|
if (!user) {
|
|
// If the user that caused this event doesn't exist anymore, it means that it
|
|
// got deleted in the meantime. This in turn means that the user disconnected
|
|
// from the server. In that case it has been removed via on_clientDisconnected
|
|
// already (or shortly will be), so it is safe to silently ignore this case
|
|
// here.
|
|
return;
|
|
}
|
|
|
|
if (!user->cChannel) {
|
|
// If the user doesn't have an associated channel, something's either wrong
|
|
// or that user has just disconnected. In either way, we want to make sure
|
|
// that this user won't stick around in the UI.
|
|
hideUser(user->uiSession);
|
|
return;
|
|
}
|
|
|
|
TalkingUIUser *userEntry = findOrAddUser(user);
|
|
|
|
if (userEntry) {
|
|
userEntry->setTalkingState(user->tsState);
|
|
}
|
|
|
|
updateUI();
|
|
}
|
|
|
|
void TalkingUI::on_mainWindowSelectionChanged(const QModelIndex ¤t, const QModelIndex &previous) {
|
|
Q_UNUSED(previous);
|
|
|
|
// Sync the selection in the MainWindow to the TalkingUI
|
|
if (Global::get().mw && Global::get().mw->pmModel) {
|
|
bool clearSelection = true;
|
|
|
|
const ClientUser *user = Global::get().mw->pmModel->getUser(current);
|
|
const Channel *channel = Global::get().mw->pmModel->getChannel(current);
|
|
|
|
if (Global::get().mw->pmModel->isChannelListener(current)) {
|
|
TalkingUIChannelListener *listenerEntry = findListener(user->uiSession, channel->iId);
|
|
|
|
if (listenerEntry) {
|
|
setSelection(ListenerSelection(listenerEntry->getWidget(), user->uiSession, channel->iId));
|
|
|
|
clearSelection = false;
|
|
}
|
|
} else {
|
|
if (user) {
|
|
TalkingUIUser *userEntry = findUser(user->uiSession);
|
|
|
|
if (userEntry) {
|
|
// Only select the user if there is an actual entry for it in the TalkingUI
|
|
setSelection(UserSelection(userEntry->getWidget(), userEntry->getAssociatedUserSession()));
|
|
|
|
clearSelection = false;
|
|
}
|
|
} else if (!user && channel) {
|
|
// if user != nullptr, the selection is actually a user, but UserModel::getChannel still returns
|
|
// the channel of that user. However we only want to select the channel if the user has indeed
|
|
// selected the channel and not just one of the users in it.
|
|
int index = findContainer(static_cast< int >(channel->iId), ContainerType::CHANNEL);
|
|
|
|
if (index >= 0) {
|
|
// Only select the channel if there is present in the TalkingUI
|
|
std::unique_ptr< TalkingUIContainer > &targetContainer =
|
|
m_containers[static_cast< std::size_t >(index)];
|
|
|
|
setSelection(
|
|
ChannelSelection(targetContainer->getWidget(),
|
|
static_cast< unsigned int >(targetContainer->getAssociatedChannelID())));
|
|
|
|
clearSelection = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (clearSelection) {
|
|
setSelection(EmptySelection());
|
|
}
|
|
}
|
|
}
|
|
|
|
void TalkingUI::on_serverSynchronized() {
|
|
ClientUser *self = ClientUser::get(Global::get().uiSession);
|
|
|
|
assert(self);
|
|
|
|
if (Global::get().s.bTalkingUI_LocalUserStaysVisible) {
|
|
// According to the settings the local user should always be visible and as we
|
|
// can't count on it to change its talking state right after it has connected to
|
|
// a server, we have to add it manually.
|
|
findOrAddUser(self);
|
|
}
|
|
|
|
// According to the settings the all users should always be visible and as we
|
|
// can't count on users to change their talking state right after our user has connected
|
|
// to a server, we have to add them manually.
|
|
if (Global::get().s.talkingUI_UsersAlwaysVisible) {
|
|
for (auto &user : ClientUser::c_qmUsers) {
|
|
findOrAddUser(user);
|
|
}
|
|
}
|
|
// The client may have received add listener messages for the user before the
|
|
// sync was complete. So we do this to ensure that they appear in the
|
|
// TalkingUI. Removing all listeners is probably not necessary but could
|
|
// prevent duplicates appearing in the case of a race.
|
|
removeAllListeners();
|
|
if (Global::get().s.bTalkingUI_ShowLocalListeners) {
|
|
if (self) {
|
|
const QSet< unsigned int > channels =
|
|
Global::get().channelListenerManager->getListenedChannelsForUser(self->uiSession);
|
|
|
|
for (unsigned int currentChannelID : channels) {
|
|
const Channel *channel = Channel::get(currentChannelID);
|
|
|
|
if (channel) {
|
|
addListener(self, channel);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void TalkingUI::on_serverDisconnected() {
|
|
setSelection(EmptySelection());
|
|
|
|
// If we disconnect from a server, we have to clear all our users, channels, and so on
|
|
// Since all entries are owned by their respective containers, we only have to delete
|
|
// all containers. These in turn are managed by smart-pointers and therefore it is
|
|
// enough to let those go out of scope.
|
|
m_containers.clear();
|
|
|
|
updateUI();
|
|
}
|
|
|
|
void TalkingUI::on_channelChanged(QObject *obj) {
|
|
// According to this function's doc, the passed object must be of type ClientUser
|
|
ClientUser *user = static_cast< ClientUser * >(obj);
|
|
|
|
if (!user) {
|
|
return;
|
|
}
|
|
|
|
TalkingUIUser *userEntry = findUser(user->uiSession);
|
|
|
|
if (userEntry) {
|
|
// The user is visible, so we call moveUserToChannel in order to update
|
|
// the channel this particular user is being displayed in.
|
|
// But first we have to make sure there actually exists and entry for
|
|
// the new channel.
|
|
addChannel(user->cChannel);
|
|
moveUserToChannel(user->uiSession, user->cChannel->iId);
|
|
}
|
|
}
|
|
|
|
void TalkingUI::on_settingsChanged() {
|
|
// The settings might have affected the way we have to display the channel names
|
|
// thus we'll update them just in case
|
|
for (auto ¤tContainer : m_containers) {
|
|
// The font size might have changed as well -> update it
|
|
// By the hierarchy in the UI the font-size should propagate to all
|
|
// sub-elements (entries) as well.
|
|
setFontSize(currentContainer->getStylableWidget());
|
|
|
|
if (currentContainer->getType() != ContainerType::CHANNEL) {
|
|
continue;
|
|
}
|
|
|
|
TalkingUIChannel *channelContainer = static_cast< TalkingUIChannel * >(currentContainer.get());
|
|
|
|
const Channel *channel = Channel::get(static_cast< unsigned int >(currentContainer->getAssociatedChannelID()));
|
|
|
|
if (channel) {
|
|
// Update
|
|
channelContainer->setName(createChannelName(
|
|
channel, Global::get().s.bTalkingUI_AbbreviateChannelNames, Global::get().s.iTalkingUI_PrefixCharCount,
|
|
Global::get().s.iTalkingUI_PostfixCharCount, Global::get().s.iTalkingUI_MaxChannelNameLength,
|
|
Global::get().s.iTalkingUI_ChannelHierarchyDepth, Global::get().s.qsHierarchyChannelSeparator,
|
|
Global::get().s.qsTalkingUI_AbbreviationReplacement,
|
|
Global::get().s.bTalkingUI_AbbreviateCurrentChannel));
|
|
} else {
|
|
qCritical("TalkingUI: Can't find channel for stored ID");
|
|
}
|
|
}
|
|
|
|
int silentUserLifeTime = Global::get().s.iTalkingUI_SilentUserLifeTime;
|
|
bool usersAlwaysVisible = Global::get().s.talkingUI_UsersAlwaysVisible;
|
|
// If the font has changed, we have to update the icon size as well
|
|
for (auto ¤tContainer : m_containers) {
|
|
for (auto ¤tEntry : currentContainer->getEntries()) {
|
|
currentEntry->setIconSize(m_currentLineHeight);
|
|
|
|
if (currentEntry->getType() == EntryType::USER) {
|
|
TalkingUIUser *userEntry = static_cast< TalkingUIUser * >(currentEntry.get());
|
|
|
|
// The time that a silent user may stick around might have changed as well
|
|
// * 1000 as the setting is in seconds whereas the timer expects milliseconds
|
|
userEntry->setLifeTime(static_cast< unsigned int >(silentUserLifeTime) * 1000);
|
|
userEntry->restrictLifetime(!usersAlwaysVisible);
|
|
}
|
|
}
|
|
}
|
|
|
|
// According to the settings the all users should always be visible and as we
|
|
// can't count on users to change their talking state right after settings where changed
|
|
// we have to add them manually.
|
|
if (usersAlwaysVisible) {
|
|
for (auto &user : ClientUser::c_qmUsers) {
|
|
findOrAddUser(user);
|
|
}
|
|
}
|
|
|
|
const ClientUser *self = ClientUser::get(Global::get().uiSession);
|
|
|
|
// Whether or not the current user should always be displayed might also have changed,
|
|
// so we'll have to update that as well.
|
|
TalkingUIUser *localUserEntry = findUser(Global::get().uiSession);
|
|
if (localUserEntry) {
|
|
bool localUserAlwaysVisible = usersAlwaysVisible || Global::get().s.bTalkingUI_LocalUserStaysVisible;
|
|
localUserEntry->restrictLifetime(!localUserAlwaysVisible);
|
|
} else {
|
|
if (self && (Global::get().s.bTalkingUI_LocalUserStaysVisible || usersAlwaysVisible)) {
|
|
// Add the local user as it is requested to be displayed
|
|
findOrAddUser(self);
|
|
}
|
|
}
|
|
|
|
|
|
// Furthermore whether or not to display the local user's listeners might have changed -> clear all
|
|
// listeners from the TalkingUI and add them again if appropriate
|
|
removeAllListeners();
|
|
if (Global::get().s.bTalkingUI_ShowLocalListeners) {
|
|
if (self) {
|
|
const QSet< unsigned int > channels =
|
|
Global::get().channelListenerManager->getListenedChannelsForUser(self->uiSession);
|
|
|
|
for (unsigned int currentChannelID : channels) {
|
|
const Channel *channel = Channel::get(currentChannelID);
|
|
|
|
if (channel) {
|
|
addListener(self, channel);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void TalkingUI::on_clientDisconnected(unsigned int userSession) {
|
|
removeUser(userSession);
|
|
}
|
|
|
|
void TalkingUI::on_muteDeafStateChanged() {
|
|
ClientUser *user = qobject_cast< ClientUser * >(sender());
|
|
|
|
if (user) {
|
|
// Update icons for local user only
|
|
updateStatusIcons(user);
|
|
}
|
|
}
|
|
|
|
void TalkingUI::on_userLocalVolumeAdjustmentsChanged(float, float) {
|
|
ClientUser *user = qobject_cast< ClientUser * >(sender());
|
|
|
|
if (user) {
|
|
TalkingUIUser *userEntry = findUser(user->uiSession);
|
|
if (userEntry) {
|
|
userEntry->setDisplayString(UserModel::createDisplayString(*user, false, nullptr));
|
|
}
|
|
}
|
|
}
|
|
|
|
void TalkingUI::on_channelListenerAdded(const ClientUser *user, const Channel *channel) {
|
|
if (user->uiSession == Global::get().uiSession && Global::get().s.bTalkingUI_ShowLocalListeners) {
|
|
addListener(user, channel);
|
|
}
|
|
}
|
|
|
|
void TalkingUI::on_channelListenerRemoved(const ClientUser *user, const Channel *channel) {
|
|
removeListener(user->uiSession, channel->iId);
|
|
}
|
|
|
|
void TalkingUI::on_channelListenerLocalVolumeAdjustmentChanged(unsigned int channelID, float, float) {
|
|
TalkingUIChannelListener *listenerEntry = findListener(Global::get().uiSession, channelID);
|
|
|
|
const Channel *channel = Channel::get(channelID);
|
|
const ClientUser *self = ClientUser::get(Global::get().uiSession);
|
|
|
|
if (listenerEntry && channel && self) {
|
|
listenerEntry->setDisplayString(UserModel::createDisplayString(*self, true, channel));
|
|
}
|
|
}
|