950 lines
34 KiB
C++
950 lines
34 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 "Log.h"
|
|
|
|
#include "Accessibility.h"
|
|
#include "AudioOutput.h"
|
|
#include "AudioOutputSample.h"
|
|
#include "AudioOutputToken.h"
|
|
#include "Channel.h"
|
|
#include "MainWindow.h"
|
|
#include "NetworkConfig.h"
|
|
#include "RichTextEditor.h"
|
|
#include "Screen.h"
|
|
#include "ServerHandler.h"
|
|
#ifndef USE_NO_TTS
|
|
# include "TextToSpeech.h"
|
|
#endif
|
|
#include "Utils.h"
|
|
#include "VolumeAdjustment.h"
|
|
#include "Global.h"
|
|
|
|
#include <QSignalBlocker>
|
|
#include <QtCore/QMutexLocker>
|
|
#include <QtGui/QImageWriter>
|
|
#include <QtGui/QScreen>
|
|
#include <QtGui/QTextBlock>
|
|
#include <QtGui/QTextDocumentFragment>
|
|
#include <QtNetwork/QNetworkReply>
|
|
#include <QtWidgets/QDesktopWidget>
|
|
|
|
const QString LogConfig::name = QLatin1String("LogConfig");
|
|
|
|
static ConfigWidget *LogConfigDialogNew(Settings &st) {
|
|
return new LogConfig(st);
|
|
}
|
|
|
|
static ConfigRegistrar registrarLog(4000, LogConfigDialogNew);
|
|
|
|
LogConfig::LogConfig(Settings &st) : ConfigWidget(st) {
|
|
setupUi(this);
|
|
|
|
#ifdef USE_NO_TTS
|
|
qgbTTS->setDisabled(true);
|
|
qsTTSVolume->setDisabled(true);
|
|
#endif
|
|
|
|
qtwMessages->header()->setSectionResizeMode(ColMessage, QHeaderView::Stretch);
|
|
qtwMessages->header()->setSectionResizeMode(ColConsole, QHeaderView::ResizeToContents);
|
|
qtwMessages->header()->setSectionResizeMode(ColNotification, QHeaderView::ResizeToContents);
|
|
qtwMessages->header()->setSectionResizeMode(ColHighlight, QHeaderView::ResizeToContents);
|
|
qtwMessages->header()->setSectionResizeMode(ColTTS, QHeaderView::ResizeToContents);
|
|
qtwMessages->header()->setSectionResizeMode(ColMessageLimit, QHeaderView::ResizeToContents);
|
|
qtwMessages->header()->setSectionResizeMode(ColStaticSound, QHeaderView::ResizeToContents);
|
|
|
|
qtwMessages->headerItem()->setData(ColMessage, Qt::AccessibleTextRole, tr("Message type"));
|
|
qtwMessages->headerItem()->setData(ColConsole, Qt::AccessibleTextRole, tr("Log message to console checkbox"));
|
|
qtwMessages->headerItem()->setData(ColNotification, Qt::AccessibleTextRole,
|
|
tr("Display pop-up notification for message checkbox"));
|
|
qtwMessages->headerItem()->setData(ColHighlight, Qt::AccessibleTextRole,
|
|
tr("Highlight window for message checkbox"));
|
|
qtwMessages->headerItem()->setData(ColTTS, Qt::AccessibleTextRole,
|
|
tr("Read message using text to speech checkbox"));
|
|
qtwMessages->headerItem()->setData(ColMessageLimit, Qt::AccessibleTextRole,
|
|
tr("Limit message notification if user count is high checkbox"));
|
|
qtwMessages->headerItem()->setData(ColStaticSound, Qt::AccessibleTextRole,
|
|
tr("Play sound file for message checkbox"));
|
|
qtwMessages->headerItem()->setData(ColStaticSoundPath, Qt::AccessibleTextRole, tr("Path to sound file"));
|
|
|
|
// Add a "All messages" entry
|
|
allMessagesItem = new QTreeWidgetItem(qtwMessages);
|
|
allMessagesItem->setText(ColMessage, QObject::tr("All messages"));
|
|
allMessagesItem->setCheckState(ColConsole, Qt::Unchecked);
|
|
allMessagesItem->setToolTip(ColConsole, QObject::tr("Toggle console for all events"));
|
|
allMessagesItem->setCheckState(ColNotification, Qt::Unchecked);
|
|
allMessagesItem->setToolTip(ColNotification, QObject::tr("Toggle pop-up notifications for all events"));
|
|
allMessagesItem->setCheckState(ColHighlight, Qt::Unchecked);
|
|
allMessagesItem->setToolTip(ColHighlight, QObject::tr("Toggle window highlight (if not active) for all events"));
|
|
allMessagesItem->setCheckState(ColMessageLimit, Qt::Unchecked);
|
|
allMessagesItem->setToolTip(ColMessageLimit, tr("Click here to toggle message limiting for all events - If using "
|
|
"this option be sure to change the user limit below."));
|
|
allMessagesItem->setCheckState(ColStaticSound, Qt::Unchecked);
|
|
allMessagesItem->setToolTip(ColStaticSound, QObject::tr("Click here to toggle sound notifications for all events"));
|
|
#ifndef USE_NO_TTS
|
|
allMessagesItem->setCheckState(ColTTS, Qt::Unchecked);
|
|
allMessagesItem->setToolTip(ColTTS, QObject::tr("Toggle Text-to-Speech for all events"));
|
|
#endif
|
|
|
|
QTreeWidgetItem *twi;
|
|
for (int i = Log::firstMsgType; i <= Log::lastMsgType; ++i) {
|
|
Log::MsgType t = Log::msgOrder[i];
|
|
const QString messageName = Global::get().l->msgName(t);
|
|
|
|
twi = new QTreeWidgetItem(qtwMessages);
|
|
twi->setData(ColMessage, Qt::UserRole, static_cast< int >(t));
|
|
twi->setText(ColMessage, messageName);
|
|
twi->setCheckState(ColConsole, Qt::Unchecked);
|
|
twi->setCheckState(ColNotification, Qt::Unchecked);
|
|
twi->setCheckState(ColHighlight, Qt::Unchecked);
|
|
twi->setCheckState(ColMessageLimit, Qt::Unchecked);
|
|
twi->setCheckState(ColStaticSound, Qt::Unchecked);
|
|
|
|
twi->setToolTip(ColConsole, tr("Toggle console for %1 events").arg(messageName));
|
|
twi->setToolTip(ColNotification, tr("Toggle pop-up notifications for %1 events").arg(messageName));
|
|
twi->setToolTip(ColHighlight, tr("Toggle window highlight (if not active) for %1 events").arg(messageName));
|
|
twi->setToolTip(ColMessageLimit, tr("Toggle message limiting behavior for %1 events ").arg(messageName));
|
|
twi->setToolTip(ColStaticSound, tr("Click here to toggle sound notification for %1 events").arg(messageName));
|
|
twi->setToolTip(ColStaticSoundPath, tr("Path to sound file used for sound notifications in the case of %1 "
|
|
"events<br />Single click to play<br />Double-click to change")
|
|
.arg(messageName));
|
|
|
|
twi->setWhatsThis(ColConsole, tr("Click here to toggle console output for %1 events.<br />If checked, this "
|
|
"option makes Mumble output all %1 events in its message log.")
|
|
.arg(messageName));
|
|
twi->setWhatsThis(ColNotification,
|
|
tr("Click here to toggle pop-up notifications for %1 events.<br />If checked, a notification "
|
|
"pop-up will be created by Mumble for every %1 event.")
|
|
.arg(messageName));
|
|
twi->setWhatsThis(ColHighlight, tr("Click here to toggle window highlight for %1 events.<br />If checked, "
|
|
"Mumble's window will be highlighted for every %1 event, if not active.")
|
|
.arg(messageName));
|
|
twi->setWhatsThis(
|
|
ColMessageLimit,
|
|
tr("Click here to toggle limiting for %1 events.<br />If checked, notifications for this event type "
|
|
"will not be played when the user count on the server exceeds the set threshold.")
|
|
.arg(messageName));
|
|
twi->setWhatsThis(ColStaticSound, tr("Click here to toggle sound notification for %1 events.<br />If checked, "
|
|
"Mumble uses a sound file predefined by you to indicate %1 events. Sound "
|
|
"files and Text-To-Speech cannot be used at the same time.")
|
|
.arg(messageName));
|
|
twi->setWhatsThis(ColStaticSoundPath,
|
|
tr("Path to sound file used for sound notifications in the case of %1 events.<br />Single "
|
|
"click to play<br />Double-click to change<br />Ensure that sound notifications for these "
|
|
"events are enabled or this field will not have any effect.")
|
|
.arg(messageName));
|
|
#ifndef USE_NO_TTS
|
|
twi->setCheckState(ColTTS, Qt::Unchecked);
|
|
twi->setToolTip(ColTTS, tr("Toggle Text-To-Speech for %1 events").arg(messageName));
|
|
twi->setWhatsThis(
|
|
ColTTS,
|
|
tr("Click here to toggle Text-To-Speech for %1 events.<br />If checked, Mumble uses Text-To-Speech to read "
|
|
"%1 events out loud to you. Text-To-Speech is also able to read the contents of the event which is not "
|
|
"true for sound files. Text-To-Speech and sound files cannot be used at the same time.")
|
|
.arg(messageName));
|
|
#endif
|
|
}
|
|
}
|
|
|
|
void LogConfig::updateSelectAllButtons() {
|
|
QList< QTreeWidgetItem * > qlItems = qtwMessages->findItems(QString(), Qt::MatchContains);
|
|
bool allConsoleChecked = true;
|
|
bool allNotificationChecked = true;
|
|
bool allHighlightChecked = true;
|
|
#ifndef USE_NO_TTS
|
|
bool allTTSChecked = true;
|
|
#endif
|
|
bool allSoundChecked = true;
|
|
bool allLimitChecked = true;
|
|
foreach (QTreeWidgetItem *i, qlItems) {
|
|
if (i == allMessagesItem) {
|
|
continue;
|
|
}
|
|
|
|
if (i->checkState(ColConsole) != Qt::Checked) {
|
|
allConsoleChecked = false;
|
|
}
|
|
if (i->checkState(ColNotification) != Qt::Checked) {
|
|
allNotificationChecked = false;
|
|
}
|
|
if (i->checkState(ColHighlight) != Qt::Checked) {
|
|
allHighlightChecked = false;
|
|
}
|
|
#ifndef USE_NO_TTS
|
|
if (i->checkState(ColTTS) != Qt::Checked) {
|
|
allTTSChecked = false;
|
|
}
|
|
#endif
|
|
if (i->checkState(ColMessageLimit) != Qt::Checked) {
|
|
allLimitChecked = false;
|
|
}
|
|
if (i->checkState(ColStaticSound) != Qt::Checked) {
|
|
allSoundChecked = false;
|
|
}
|
|
|
|
if (!allConsoleChecked && !allNotificationChecked && !allHighlightChecked && !allSoundChecked) {
|
|
#ifndef USE_NO_TTS
|
|
if (!allTTSChecked) {
|
|
break;
|
|
}
|
|
#else
|
|
break;
|
|
#endif
|
|
}
|
|
}
|
|
|
|
const QSignalBlocker blocker(qtwMessages);
|
|
allMessagesItem->setCheckState(ColConsole, allConsoleChecked ? Qt::Checked : Qt::Unchecked);
|
|
allMessagesItem->setCheckState(ColNotification, allNotificationChecked ? Qt::Checked : Qt::Unchecked);
|
|
allMessagesItem->setCheckState(ColHighlight, allHighlightChecked ? Qt::Checked : Qt::Unchecked);
|
|
#ifndef USE_NO_TTS
|
|
allMessagesItem->setCheckState(ColTTS, allTTSChecked ? Qt::Checked : Qt::Unchecked);
|
|
#endif
|
|
allMessagesItem->setCheckState(ColMessageLimit, allLimitChecked ? Qt::Checked : Qt::Unchecked);
|
|
allMessagesItem->setCheckState(ColStaticSound, allSoundChecked ? Qt::Checked : Qt::Unchecked);
|
|
}
|
|
|
|
QString LogConfig::title() const {
|
|
return windowTitle();
|
|
}
|
|
|
|
const QString &LogConfig::getName() const {
|
|
return LogConfig::name;
|
|
}
|
|
|
|
QIcon LogConfig::icon() const {
|
|
return QIcon(QLatin1String("skin:config_msgs.png"));
|
|
}
|
|
|
|
void LogConfig::load(const Settings &r) {
|
|
QList< QTreeWidgetItem * > qlItems = qtwMessages->findItems(QString(), Qt::MatchContains);
|
|
|
|
foreach (QTreeWidgetItem *i, qlItems) {
|
|
if (i == allMessagesItem) {
|
|
continue;
|
|
}
|
|
Log::MsgType mt = static_cast< Log::MsgType >(i->data(ColMessage, Qt::UserRole).toInt());
|
|
Settings::MessageLog ml = static_cast< Settings::MessageLog >(r.qmMessages.value(mt));
|
|
|
|
i->setCheckState(ColConsole, (ml & Settings::LogConsole) ? Qt::Checked : Qt::Unchecked);
|
|
i->setCheckState(ColNotification, (ml & Settings::LogBalloon) ? Qt::Checked : Qt::Unchecked);
|
|
i->setCheckState(ColHighlight, (ml & Settings::LogHighlight) ? Qt::Checked : Qt::Unchecked);
|
|
#ifndef USE_NO_TTS
|
|
i->setCheckState(ColTTS, (ml & Settings::LogTTS) ? Qt::Checked : Qt::Unchecked);
|
|
#endif
|
|
i->setCheckState(ColMessageLimit, (ml & Settings::LogMessageLimit) ? Qt::Checked : Qt::Unchecked);
|
|
i->setCheckState(ColStaticSound, (ml & Settings::LogSoundfile) ? Qt::Checked : Qt::Unchecked);
|
|
i->setText(ColStaticSoundPath, r.qmMessageSounds.value(mt));
|
|
}
|
|
|
|
qsbMaxBlocks->setValue(r.iMaxLogBlocks);
|
|
qcb24HourClock->setChecked(r.bLog24HourClock);
|
|
qsbChatMessageMargins->setValue(r.iChatMessageMargins);
|
|
|
|
#ifdef USE_NO_TTS
|
|
qtwMessages->hideColumn(ColTTS);
|
|
qsTTSVolume->hide();
|
|
qlTTSVolume->hide();
|
|
#else
|
|
loadSlider(qsTTSVolume, r.iTTSVolume);
|
|
qsbThreshold->setValue(r.iTTSThreshold);
|
|
qcbReadBackOwn->setChecked(r.bTTSMessageReadBack);
|
|
qcbNoScope->setChecked(r.bTTSNoScope);
|
|
qcbNoAuthor->setChecked(r.bTTSNoAuthor);
|
|
qcbEnableTTS->setChecked(r.bTTS);
|
|
|
|
#endif
|
|
loadSlider(qsNotificationVolume, VolumeAdjustment::toIntegerDBAdjustment(r.notificationVolume));
|
|
loadSlider(qsCueVolume, VolumeAdjustment::toIntegerDBAdjustment(r.cueVolume));
|
|
qcbWhisperFriends->setChecked(r.bWhisperFriends);
|
|
qsbMessageLimitUsers->setValue(r.iMessageLimitUserThreshold);
|
|
}
|
|
|
|
void LogConfig::save() const {
|
|
QList< QTreeWidgetItem * > qlItems = qtwMessages->findItems(QString(), Qt::MatchContains);
|
|
foreach (QTreeWidgetItem *i, qlItems) {
|
|
if (i == allMessagesItem) {
|
|
continue;
|
|
}
|
|
Log::MsgType mt = static_cast< Log::MsgType >(i->data(ColMessage, Qt::UserRole).toInt());
|
|
|
|
int v = 0;
|
|
if (i->checkState(ColConsole) == Qt::Checked)
|
|
v |= Settings::LogConsole;
|
|
if (i->checkState(ColNotification) == Qt::Checked)
|
|
v |= Settings::LogBalloon;
|
|
if (i->checkState(ColHighlight) == Qt::Checked)
|
|
v |= Settings::LogHighlight;
|
|
#ifndef USE_NO_TTS
|
|
if (i->checkState(ColTTS) == Qt::Checked)
|
|
v |= Settings::LogTTS;
|
|
#endif
|
|
if (i->checkState(ColMessageLimit) == Qt::Checked) {
|
|
v |= Settings::LogMessageLimit;
|
|
}
|
|
if (i->checkState(ColStaticSound) == Qt::Checked)
|
|
v |= Settings::LogSoundfile;
|
|
s.qmMessages[mt] = static_cast< unsigned int >(v);
|
|
s.qmMessageSounds[mt] = i->text(ColStaticSoundPath);
|
|
}
|
|
s.iMaxLogBlocks = qsbMaxBlocks->value();
|
|
s.bLog24HourClock = qcb24HourClock->isChecked();
|
|
s.iChatMessageMargins = qsbChatMessageMargins->value();
|
|
|
|
#ifndef USE_NO_TTS
|
|
s.iTTSVolume = qsTTSVolume->value();
|
|
s.iTTSThreshold = qsbThreshold->value();
|
|
s.bTTSMessageReadBack = qcbReadBackOwn->isChecked();
|
|
s.bTTSNoScope = qcbNoScope->isChecked();
|
|
s.bTTSNoAuthor = qcbNoAuthor->isChecked();
|
|
s.bTTS = qcbEnableTTS->isChecked();
|
|
#endif
|
|
s.notificationVolume = VolumeAdjustment::toFactor(qsNotificationVolume->value());
|
|
s.cueVolume = VolumeAdjustment::toFactor(qsCueVolume->value());
|
|
s.bWhisperFriends = qcbWhisperFriends->isChecked();
|
|
s.iMessageLimitUserThreshold = qsbMessageLimitUsers->value();
|
|
}
|
|
|
|
void LogConfig::accept() const {
|
|
#ifndef USE_NO_TTS
|
|
Global::get().l->tts->setVolume(s.iTTSVolume);
|
|
#endif
|
|
Global::get().mw->qteLog->document()->setMaximumBlockCount(s.iMaxLogBlocks);
|
|
}
|
|
|
|
void LogConfig::on_qtwMessages_itemChanged(QTreeWidgetItem *i, int column) {
|
|
if (i->isSelected() && i != allMessagesItem) {
|
|
switch (column) {
|
|
case ColTTS:
|
|
if (i->checkState(ColTTS))
|
|
i->setCheckState(ColStaticSound, Qt::Unchecked);
|
|
break;
|
|
case ColStaticSound:
|
|
if (i->checkState(ColStaticSound)) {
|
|
i->setCheckState(ColTTS, Qt::Unchecked);
|
|
if (i->text(ColStaticSoundPath).isEmpty())
|
|
browseForAudioFile();
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (i != allMessagesItem) {
|
|
updateSelectAllButtons();
|
|
} else {
|
|
// Suppress signals on the TreeWidget
|
|
const QSignalBlocker blocker(qtwMessages);
|
|
// Select / Unselect all entries of that column
|
|
QList< QTreeWidgetItem * > qlItems = qtwMessages->findItems(QString(), Qt::MatchContains);
|
|
foreach (QTreeWidgetItem *item, qlItems) {
|
|
if (item != allMessagesItem) {
|
|
item->setCheckState(column, allMessagesItem->checkState(column));
|
|
}
|
|
}
|
|
}
|
|
|
|
if (column != ColMessage && column != ColStaticSoundPath) {
|
|
i->setData(column, Qt::AccessibleDescriptionRole,
|
|
i->checkState(column) == Qt::Checked ? tr("checked") : tr("unchecked"));
|
|
}
|
|
}
|
|
|
|
void LogConfig::on_qtwMessages_itemClicked(QTreeWidgetItem *item, int column) {
|
|
if (item && item != allMessagesItem && column == ColStaticSoundPath) {
|
|
AudioOutputPtr ao = Global::get().ao;
|
|
if (ao) {
|
|
if (!ao->playSample(item->text(ColStaticSoundPath), Global::get().s.notificationVolume)) {
|
|
browseForAudioFile();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void LogConfig::on_qtwMessages_itemDoubleClicked(QTreeWidgetItem *item, int column) {
|
|
if (item && item != allMessagesItem && column == ColStaticSoundPath)
|
|
browseForAudioFile();
|
|
}
|
|
|
|
void LogConfig::browseForAudioFile() {
|
|
QTreeWidgetItem *i = qtwMessages->selectedItems()[0];
|
|
QString defaultpath(i->text(ColStaticSoundPath));
|
|
QString file = AudioOutputSample::browseForSndfile(defaultpath);
|
|
if (!file.isEmpty()) {
|
|
i->setText(ColStaticSoundPath, file);
|
|
i->setCheckState(ColStaticSound, Qt::Checked);
|
|
}
|
|
}
|
|
|
|
void LogConfig::on_qsNotificationVolume_valueChanged(int value) {
|
|
qsbNotificationVolume->setValue(value);
|
|
Mumble::Accessibility::setSliderSemanticValue(qsNotificationVolume,
|
|
QString("%1 %2").arg(value).arg(tr("decibels")));
|
|
}
|
|
|
|
void LogConfig::on_qsCueVolume_valueChanged(int value) {
|
|
qsbCueVolume->setValue(value);
|
|
Mumble::Accessibility::setSliderSemanticValue(qsCueVolume, QString("%1 %2").arg(value).arg(tr("decibels")));
|
|
}
|
|
|
|
void LogConfig::on_qsTTSVolume_valueChanged(int value) {
|
|
qsbTTSVolume->setValue(value);
|
|
Mumble::Accessibility::setSliderSemanticValue(qsTTSVolume, Mumble::Accessibility::SliderMode::READ_PERCENT, "%");
|
|
}
|
|
|
|
void LogConfig::on_qsbNotificationVolume_valueChanged(int value) {
|
|
qsNotificationVolume->setValue(value);
|
|
}
|
|
|
|
void LogConfig::on_qsbCueVolume_valueChanged(int value) {
|
|
qsCueVolume->setValue(value);
|
|
}
|
|
|
|
void LogConfig::on_qsbTTSVolume_valueChanged(int value) {
|
|
qsTTSVolume->setValue(value);
|
|
}
|
|
|
|
|
|
QMutex Log::qmDeferredLogs;
|
|
QVector< LogMessage > Log::qvDeferredLogs;
|
|
|
|
|
|
Log::Log(QObject *p) : QObject(p) {
|
|
qRegisterMetaType< Log::MsgType >();
|
|
|
|
#ifndef USE_NO_TTS
|
|
tts = new TextToSpeech(this);
|
|
tts->setVolume(Global::get().s.iTTSVolume);
|
|
#endif
|
|
uiLastId = 0;
|
|
qdDate = QDate::currentDate();
|
|
}
|
|
|
|
// Display order in settingsscreen, allows to insert new events without breaking config-compatibility with older
|
|
// versions
|
|
const Log::MsgType Log::msgOrder[] = { DebugInfo,
|
|
CriticalError,
|
|
Warning,
|
|
Information,
|
|
ServerConnected,
|
|
ServerDisconnected,
|
|
UserJoin,
|
|
UserLeave,
|
|
ChannelListeningAdd,
|
|
ChannelListeningRemove,
|
|
Recording,
|
|
YouKicked,
|
|
UserKicked,
|
|
UserRenamed,
|
|
SelfMute,
|
|
SelfUnmute,
|
|
SelfDeaf,
|
|
SelfUndeaf,
|
|
OtherSelfMute,
|
|
YouMuted,
|
|
YouMutedOther,
|
|
OtherMutedOther,
|
|
SelfChannelJoin,
|
|
SelfChannelJoinOther,
|
|
ChannelJoin,
|
|
ChannelLeave,
|
|
ChannelJoinConnect,
|
|
ChannelLeaveDisconnect,
|
|
PermissionDenied,
|
|
TextMessage,
|
|
PrivateTextMessage,
|
|
PluginMessage };
|
|
|
|
const char *Log::msgNames[] = { QT_TRANSLATE_NOOP("Log", "Debug"),
|
|
QT_TRANSLATE_NOOP("Log", "Critical"),
|
|
QT_TRANSLATE_NOOP("Log", "Warning"),
|
|
QT_TRANSLATE_NOOP("Log", "Information"),
|
|
QT_TRANSLATE_NOOP("Log", "Server connected"),
|
|
QT_TRANSLATE_NOOP("Log", "Server disconnected"),
|
|
QT_TRANSLATE_NOOP("Log", "User joined server"),
|
|
QT_TRANSLATE_NOOP("Log", "User left server"),
|
|
QT_TRANSLATE_NOOP("Log", "User recording state changed"),
|
|
QT_TRANSLATE_NOOP("Log", "User kicked (you or by you)"),
|
|
QT_TRANSLATE_NOOP("Log", "User kicked"),
|
|
QT_TRANSLATE_NOOP("Log", "You self-muted"),
|
|
QT_TRANSLATE_NOOP("Log", "Other self-muted/deafened"),
|
|
QT_TRANSLATE_NOOP("Log", "User muted (you)"),
|
|
QT_TRANSLATE_NOOP("Log", "User muted (by you)"),
|
|
QT_TRANSLATE_NOOP("Log", "User muted (other)"),
|
|
QT_TRANSLATE_NOOP("Log", "User joined channel"),
|
|
QT_TRANSLATE_NOOP("Log", "User left channel"),
|
|
QT_TRANSLATE_NOOP("Log", "Permission denied"),
|
|
QT_TRANSLATE_NOOP("Log", "Text message"),
|
|
QT_TRANSLATE_NOOP("Log", "You self-unmuted"),
|
|
QT_TRANSLATE_NOOP("Log", "You self-deafened"),
|
|
QT_TRANSLATE_NOOP("Log", "You self-undeafened"),
|
|
QT_TRANSLATE_NOOP("Log", "User renamed"),
|
|
QT_TRANSLATE_NOOP("Log", "You joined channel"),
|
|
QT_TRANSLATE_NOOP("Log", "You joined channel (moved)"),
|
|
QT_TRANSLATE_NOOP("Log", "User connected and entered channel"),
|
|
QT_TRANSLATE_NOOP("Log", "User left channel and disconnected"),
|
|
QT_TRANSLATE_NOOP("Log", "Private text message"),
|
|
QT_TRANSLATE_NOOP("Log", "User started listening to channel"),
|
|
QT_TRANSLATE_NOOP("Log", "User stopped listening to channel"),
|
|
QT_TRANSLATE_NOOP("Log", "Plugin message") };
|
|
|
|
QString Log::msgName(MsgType t) const {
|
|
return tr(msgNames[t]);
|
|
}
|
|
|
|
const char *Log::colorClasses[] = { "time", "server", "privilege" };
|
|
|
|
const QStringList Log::allowedSchemes() {
|
|
QStringList qslAllowedSchemeNames;
|
|
qslAllowedSchemeNames << QLatin1String("mumble");
|
|
qslAllowedSchemeNames << QLatin1String("http");
|
|
qslAllowedSchemeNames << QLatin1String("https");
|
|
qslAllowedSchemeNames << QLatin1String("gemini");
|
|
qslAllowedSchemeNames << QLatin1String("ftp");
|
|
qslAllowedSchemeNames << QLatin1String("clientid");
|
|
qslAllowedSchemeNames << QLatin1String("channelid");
|
|
qslAllowedSchemeNames << QLatin1String("spotify");
|
|
qslAllowedSchemeNames << QLatin1String("steam");
|
|
qslAllowedSchemeNames << QLatin1String("irc");
|
|
qslAllowedSchemeNames << QLatin1String("gg"); // Gadu-Gadu http://gg.pl - Polish instant messenger
|
|
qslAllowedSchemeNames << QLatin1String("mailto");
|
|
qslAllowedSchemeNames << QLatin1String("xmpp");
|
|
qslAllowedSchemeNames << QLatin1String("skype");
|
|
qslAllowedSchemeNames << QLatin1String("rtmp"); // http://en.wikipedia.org/wiki/Real_Time_Messaging_Protocol
|
|
qslAllowedSchemeNames << QLatin1String("magnet"); // https://en.wikipedia.org/wiki/Magnet_URI_scheme
|
|
|
|
return qslAllowedSchemeNames;
|
|
}
|
|
|
|
QString Log::msgColor(const QString &text, LogColorType t) {
|
|
QString classname;
|
|
|
|
return QString::fromLatin1("<span class='log-%1'>%2</span>").arg(QString::fromLatin1(colorClasses[t])).arg(text);
|
|
}
|
|
|
|
QString Log::formatChannel(::Channel *c) {
|
|
return QString::fromLatin1("<a href='channelid://id.%1/%3' class='log-channel'>%2</a>")
|
|
.arg(c->iId)
|
|
.arg(c->qsName.toHtmlEscaped())
|
|
.arg(QString::fromLatin1(Global::get().sh->qbaDigest.toBase64()));
|
|
}
|
|
|
|
void Log::logOrDefer(Log::MsgType mt, const QString &console, const QString &terse, bool ownMessage,
|
|
const QString &overrideTTS, bool ignoreTTS) {
|
|
if (Global::get().l) {
|
|
// log directly as it seems the log-UI has been set-up already
|
|
Global::get().l->log(mt, console, terse, ownMessage, overrideTTS, ignoreTTS);
|
|
} else {
|
|
// defer the log
|
|
QMutexLocker mLock(&Log::qmDeferredLogs);
|
|
|
|
qvDeferredLogs.append(LogMessage(mt, console, terse, ownMessage, overrideTTS, ignoreTTS));
|
|
}
|
|
}
|
|
|
|
QString Log::formatClientUser(ClientUser *cu, LogColorType t, const QString &displayName) {
|
|
QString className;
|
|
if (t == Log::Target) {
|
|
className = QString::fromLatin1("target");
|
|
} else if (t == Log::Source) {
|
|
className = QString::fromLatin1("source");
|
|
}
|
|
|
|
if (cu) {
|
|
QString name = (displayName.isNull() ? cu->qsName : displayName).toHtmlEscaped();
|
|
if (cu->qsHash.isEmpty()) {
|
|
return QString::fromLatin1("<a href='clientid://id.%2/%4' class='log-user log-%1'>%3</a>")
|
|
.arg(className)
|
|
.arg(cu->uiSession)
|
|
.arg(name)
|
|
.arg(QString::fromLatin1(Global::get().sh->qbaDigest.toBase64()));
|
|
} else {
|
|
return QString::fromLatin1("<a href='clientid://%2' class='log-user log-%1'>%3</a>")
|
|
.arg(className)
|
|
.arg(cu->qsHash)
|
|
.arg(name);
|
|
}
|
|
} else {
|
|
return QString::fromLatin1("<span class='log-server log-%1'>%2</span>").arg(className).arg(tr("the server"));
|
|
}
|
|
}
|
|
|
|
void Log::setIgnore(MsgType t, int ignore) {
|
|
qmIgnore.insert(t, ignore);
|
|
}
|
|
|
|
void Log::clearIgnore() {
|
|
qmIgnore.clear();
|
|
}
|
|
|
|
QString Log::imageToImg(const QByteArray &format, const QByteArray &image) {
|
|
QString fmt = QLatin1String(format);
|
|
|
|
if (fmt.isEmpty())
|
|
fmt = QLatin1String("qt");
|
|
|
|
QByteArray rawbase = image.toBase64();
|
|
QByteArray encoded;
|
|
int i = 0;
|
|
int begin = 0, end = 0;
|
|
do {
|
|
begin = i * 72;
|
|
end = begin + 72;
|
|
|
|
encoded.append(QUrl::toPercentEncoding(QLatin1String(rawbase.mid(begin, 72))));
|
|
if (end < rawbase.length())
|
|
encoded.append('\n');
|
|
|
|
++i;
|
|
} while (end < rawbase.length());
|
|
|
|
return QString::fromLatin1("<img src=\"data:image/%1;base64,%2\" />").arg(fmt).arg(QLatin1String(encoded));
|
|
}
|
|
|
|
QString Log::imageToImg(QImage img, int maxSize) {
|
|
constexpr int MAX_WIDTH = 600;
|
|
constexpr int MAX_HEIGHT = 400;
|
|
|
|
if ((img.width() > MAX_WIDTH) || (img.height() > MAX_HEIGHT)) {
|
|
img = img.scaled(MAX_WIDTH, MAX_HEIGHT, Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
|
}
|
|
|
|
int quality = 100;
|
|
QByteArray format = "JPEG";
|
|
|
|
QByteArray qba;
|
|
QString result;
|
|
while (quality > 0) {
|
|
qba.clear();
|
|
QBuffer qb(&qba);
|
|
qb.open(QIODevice::WriteOnly);
|
|
|
|
QImageWriter imgwrite(&qb, format);
|
|
imgwrite.setQuality(quality);
|
|
imgwrite.write(img);
|
|
result = imageToImg(format, qba);
|
|
if (result.length() < maxSize || maxSize == 0) {
|
|
return result;
|
|
}
|
|
quality -= 10;
|
|
}
|
|
return QString();
|
|
}
|
|
|
|
QString Log::validHtml(const QString &html, QTextCursor *tc) {
|
|
LogDocument qtd;
|
|
|
|
QRectF qr = Mumble::Screen::screenFromWidget(*Global::get().mw)->availableGeometry();
|
|
qtd.setTextWidth(qr.width() / 2);
|
|
qtd.setDefaultStyleSheet(qApp->styleSheet());
|
|
|
|
// Call documentLayout on our LogDocument to ensure
|
|
// it has a layout backing it. With a layout set on
|
|
// the document, it will attempt to load all the
|
|
// resources it contains as soon as we call setHtml(),
|
|
// allowing our validation checks for things such as
|
|
// data URL images to run.
|
|
(void) qtd.documentLayout();
|
|
qtd.setHtml(html);
|
|
|
|
QStringList qslAllowed = allowedSchemes();
|
|
for (QTextBlock qtb = qtd.begin(); qtb != qtd.end(); qtb = qtb.next()) {
|
|
for (QTextBlock::iterator qtbi = qtb.begin(); qtbi != qtb.end(); ++qtbi) {
|
|
const QTextFragment &qtf = qtbi.fragment();
|
|
QTextCharFormat qcf = qtf.charFormat();
|
|
if (!qcf.anchorHref().isEmpty()) {
|
|
QUrl url(qcf.anchorHref());
|
|
if (!url.isValid() || !qslAllowed.contains(url.scheme())) {
|
|
QTextCharFormat qcfn = QTextCharFormat();
|
|
QTextCursor qtc(&qtd);
|
|
qtc.setPosition(qtf.position(), QTextCursor::MoveAnchor);
|
|
qtc.setPosition(qtf.position() + qtf.length(), QTextCursor::KeepAnchor);
|
|
qtc.setCharFormat(qcfn);
|
|
qtbi = qtb.begin();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
qtd.adjustSize();
|
|
QSizeF s = qtd.size();
|
|
|
|
if (!s.isValid()) {
|
|
QString errorInvalidSizeMessage = tr("[[ Invalid size ]]");
|
|
if (tc) {
|
|
tc->insertText(errorInvalidSizeMessage);
|
|
return QString();
|
|
} else {
|
|
return errorInvalidSizeMessage;
|
|
}
|
|
}
|
|
|
|
int messageSize = static_cast< int >(s.width() * s.height());
|
|
int allowedSize = 2048 * 2048;
|
|
|
|
if (messageSize > allowedSize) {
|
|
QString errorSizeMessage = tr("[[ Text object too large to display ]]");
|
|
if (tc) {
|
|
tc->insertText(errorSizeMessage);
|
|
return QString();
|
|
} else {
|
|
return errorSizeMessage;
|
|
}
|
|
}
|
|
|
|
if (tc) {
|
|
QTextCursor tcNew(&qtd);
|
|
tcNew.movePosition(QTextCursor::End, QTextCursor::KeepAnchor);
|
|
tc->insertFragment(tcNew.selection());
|
|
return QString();
|
|
} else {
|
|
return qtd.toHtml();
|
|
}
|
|
}
|
|
|
|
void Log::log(MsgType mt, const QString &console, const QString &terse, bool ownMessage, const QString &overrideTTS,
|
|
bool ignoreTTS) {
|
|
QDateTime dt = QDateTime::currentDateTime();
|
|
|
|
int ignore = qmIgnore.value(mt);
|
|
if (ignore) {
|
|
ignore--;
|
|
qmIgnore.insert(mt, ignore);
|
|
return;
|
|
}
|
|
|
|
QString plain = QTextDocumentFragment::fromHtml(console).toPlainText();
|
|
|
|
quint32 flags = Global::get().s.qmMessages.value(mt);
|
|
|
|
// Message output on console
|
|
if ((flags & Settings::LogConsole)) {
|
|
QTextCursor tc = Global::get().mw->qteLog->textCursor();
|
|
|
|
tc.movePosition(QTextCursor::End);
|
|
|
|
// We copy the value from the settings in order to make sure that
|
|
// we use the same margin everywhere while in this method (even if
|
|
// the setting might change in that time).
|
|
const int msgMargin = Global::get().s.iChatMessageMargins;
|
|
|
|
QTextFrameFormat qttf;
|
|
qttf.setTopMargin(0);
|
|
qttf.setBottomMargin(msgMargin);
|
|
|
|
LogTextBrowser *tlog = Global::get().mw->qteLog;
|
|
const int oldscrollvalue = tlog->getLogScroll();
|
|
// Restore the previous scroll position after inserting a new message
|
|
// if the message was not sent by the user AND the chat log is not
|
|
// scrolled all the way down.
|
|
const bool restoreScroll = !(ownMessage || tlog->isScrolledToBottom());
|
|
|
|
// A newline is inserted after each frame, but this spaces out the
|
|
// log entries too much, so the line height is set to zero to reduce
|
|
// the space between log entries. This line height is only set for the
|
|
// blank lines between entries, not for entries themselves.
|
|
//
|
|
// NOTE: All further log entries must go in a new text frame.
|
|
// Otherwise, they will not display correctly as a result of having
|
|
// line height equal to 0 for the current block.
|
|
QTextBlockFormat bf = tc.blockFormat();
|
|
bf.setLineHeight(0, QTextBlockFormat::FixedHeight);
|
|
bf.setTopMargin(0);
|
|
bf.setBottomMargin(0);
|
|
|
|
// Set the line height of the leading blank line to zero
|
|
tc.setBlockFormat(bf);
|
|
|
|
if (qdDate != dt.date()) {
|
|
qdDate = dt.date();
|
|
tc.insertFrame(qttf);
|
|
tc.insertHtml(
|
|
tr("[Date changed to %1]\n").arg(QLocale().toString(qdDate, QLocale::ShortFormat).toHtmlEscaped()));
|
|
tc.movePosition(QTextCursor::End);
|
|
tc.setBlockFormat(bf);
|
|
}
|
|
|
|
// Convert CRLF to unix-style LF and old mac-style LF (single \r) to unix-style as well
|
|
QString fixedNLPlain =
|
|
plain.replace(QLatin1String("\r\n"), QLatin1String("\n")).replace(QLatin1String("\r"), QLatin1String("\n"));
|
|
|
|
if (fixedNLPlain.contains(QRegExp(QLatin1String("\\n[ \\t]*$")))) {
|
|
// If the message ends with one or more blank lines (or lines only containing whitespace)
|
|
// paint a border around the message to make clear that it contains invisible parts.
|
|
// The beginning of the message is clear anyway (the date and potentially the "To XY" part)
|
|
// so we don't have to care about that.
|
|
qttf.setBorder(1);
|
|
qttf.setPadding(2);
|
|
qttf.setBorderStyle(QTextFrameFormat::BorderStyle_Dashed);
|
|
}
|
|
|
|
tc.insertFrame(qttf);
|
|
|
|
const QString timeString =
|
|
dt.time().toString(QLatin1String(Global::get().s.bLog24HourClock ? "HH:mm:ss" : "hh:mm:ss AP"));
|
|
tc.insertHtml(Log::msgColor(QString::fromLatin1("[%1] ").arg(timeString.toHtmlEscaped()), Log::Time));
|
|
|
|
validHtml(console, &tc);
|
|
tc.movePosition(QTextCursor::End);
|
|
Global::get().mw->qteLog->setTextCursor(tc);
|
|
|
|
// Set the line height of the trailing blank line to zero
|
|
tc.setBlockFormat(bf);
|
|
|
|
if (restoreScroll) {
|
|
tlog->setLogScroll(oldscrollvalue);
|
|
}
|
|
}
|
|
|
|
if (!ownMessage) {
|
|
if (!(Global::get().mw->isActiveWindow() && Global::get().mw->qdwLog->isVisible())) {
|
|
// Message notification with window highlight
|
|
if (flags & Settings::LogHighlight) {
|
|
QApplication::alert(Global::get().mw);
|
|
}
|
|
|
|
// Message notification with balloon tooltips
|
|
if (flags & Settings::LogBalloon) {
|
|
// Replace any instances of a "Object Replacement Character" from QTextDocumentFragment::toPlainText
|
|
postNotification(mt, plain.replace("\xEF\xBF\xBC", tr("[embedded content]")));
|
|
}
|
|
}
|
|
|
|
// Don't make any noise if we are self deafened (Unless it is the sound for activating self deaf)
|
|
if (Global::get().s.bDeaf && mt != Log::SelfDeaf) {
|
|
return;
|
|
}
|
|
|
|
// Message notification with static sounds
|
|
int connectedUsers = 0;
|
|
{
|
|
QReadLocker lock(&ClientUser::c_qrwlUsers);
|
|
connectedUsers = ClientUser::c_qmUsers.size();
|
|
}
|
|
if ((flags & Settings::LogSoundfile)
|
|
&& !(flags & Settings::LogMessageLimit && connectedUsers > Global::get().s.iMessageLimitUserThreshold)) {
|
|
QString sSound = Global::get().s.qmMessageSounds.value(mt);
|
|
AudioOutputPtr ao = Global::get().ao;
|
|
|
|
if (!ao || !ao->playSample(sSound, Global::get().s.notificationVolume)) {
|
|
qWarning() << "Sound file" << sSound << "is not a valid audio file, fallback to TTS.";
|
|
flags ^= Settings::LogSoundfile | Settings::LogTTS; // Fallback to TTS
|
|
}
|
|
}
|
|
} else if (!Global::get().s.bTTSMessageReadBack) {
|
|
return;
|
|
}
|
|
|
|
// Message notification with Text-To-Speech
|
|
if (Global::get().s.bDeaf || !Global::get().s.bTTS || !(flags & Settings::LogTTS) || ignoreTTS) {
|
|
return;
|
|
}
|
|
|
|
// If overrideTTS is a valid string use its contents as message
|
|
if (!overrideTTS.isNull()) {
|
|
plain = overrideTTS;
|
|
}
|
|
|
|
// Apply simplifications to spoken text
|
|
QRegExp identifyURL(QLatin1String("[a-z-]+://[^ <]*"), Qt::CaseInsensitive, QRegExp::RegExp2);
|
|
|
|
QStringList qslAllowed = allowedSchemes();
|
|
|
|
int pos = 0;
|
|
while ((pos = identifyURL.indexIn(plain, pos)) != -1) {
|
|
QUrl url(identifyURL.cap(0).toLower());
|
|
int len = identifyURL.matchedLength();
|
|
if (url.isValid() && qslAllowed.contains(url.scheme())) {
|
|
// Replace it appropriately
|
|
QString replacement;
|
|
QString host = url.host().replace(QRegExp(QLatin1String("^www.")), QString());
|
|
|
|
if (url.scheme() == QLatin1String("http") || url.scheme() == QLatin1String("https"))
|
|
replacement = tr("link to %1").arg(host);
|
|
else if (url.scheme() == QLatin1String("ftp"))
|
|
replacement = tr("FTP link to %1").arg(host);
|
|
else if (url.scheme() == QLatin1String("clientid"))
|
|
replacement = tr("player link");
|
|
else if (url.scheme() == QLatin1String("channelid"))
|
|
replacement = tr("channel link");
|
|
else
|
|
replacement = tr("%1 link").arg(url.scheme());
|
|
|
|
plain.replace(pos, len, replacement);
|
|
} else {
|
|
pos += len;
|
|
}
|
|
}
|
|
|
|
#ifndef USE_NO_TTS
|
|
// TTS threshold limiter.
|
|
if (plain.length() <= Global::get().s.iTTSThreshold)
|
|
tts->say(plain);
|
|
else if ((!terse.isEmpty()) && (terse.length() <= Global::get().s.iTTSThreshold))
|
|
tts->say(terse);
|
|
#else
|
|
// Mark as unused
|
|
Q_UNUSED(terse);
|
|
#endif
|
|
}
|
|
|
|
void Log::processDeferredLogs() {
|
|
QMutexLocker mLocker(&Log::qmDeferredLogs);
|
|
|
|
while (!qvDeferredLogs.isEmpty()) {
|
|
LogMessage msg = qvDeferredLogs.takeFirst();
|
|
|
|
log(msg.mt, msg.console, msg.terse, msg.ownMessage, msg.overrideTTS, msg.ignoreTTS);
|
|
}
|
|
}
|
|
|
|
// Post a notification using the MainWindow's QSystemTrayIcon.
|
|
void Log::postQtNotification(MsgType mt, const QString &plain) {
|
|
if (Global::get().mw->qstiIcon->isSystemTrayAvailable() && Global::get().mw->qstiIcon->supportsMessages()) {
|
|
QSystemTrayIcon::MessageIcon msgIcon;
|
|
switch (mt) {
|
|
case DebugInfo:
|
|
case CriticalError:
|
|
msgIcon = QSystemTrayIcon::Critical;
|
|
break;
|
|
case Warning:
|
|
msgIcon = QSystemTrayIcon::Warning;
|
|
break;
|
|
default:
|
|
msgIcon = QSystemTrayIcon::Information;
|
|
break;
|
|
}
|
|
Global::get().mw->qstiIcon->showMessage(msgName(mt), plain, msgIcon);
|
|
}
|
|
}
|
|
|
|
LogMessage::LogMessage(Log::MsgType mt, const QString &console, const QString &terse, bool ownMessage,
|
|
const QString &overrideTTS, bool ignoreTTS)
|
|
: mt(mt), console(console), terse(terse), ownMessage(ownMessage), overrideTTS(overrideTTS), ignoreTTS(ignoreTTS) {
|
|
}
|
|
|
|
LogDocument::LogDocument(QObject *p) : QTextDocument(p) {
|
|
}
|
|
|
|
QVariant LogDocument::loadResource(int type, const QUrl &url) {
|
|
// Ignore requests for all external resources
|
|
// that aren't images. We don't support any of them.
|
|
if (type != QTextDocument::ImageResource) {
|
|
addResource(type, url, QByteArray());
|
|
return QByteArray();
|
|
}
|
|
|
|
// Only accept data URLs, not external resources
|
|
if (url.isValid() && url.scheme() == QLatin1String("data")) {
|
|
return QTextDocument::loadResource(type, url);
|
|
}
|
|
|
|
QImage qi(1, 1, QImage::Format_Mono);
|
|
addResource(type, url, qi);
|
|
|
|
return qi;
|
|
}
|