// Copyright 2005-2019 The Mumble Developers. All rights reserved. // Use of this source code is governed by a BSD-style license // that can be found in the LICENSE file at the root of the // Mumble source tree or at . #include "mumble_pch.hpp" #include "LCD.h" #include "ClientUser.h" #include "Channel.h" #include "Message.h" #include "ServerHandler.h" // We define a global macro called 'g'. This can lead to issues when included code uses 'g' as a type or parameter name (like protobuf 3.7 does). As such, for now, we have to make this our last include. #include "Global.h" QList *LCDEngineRegistrar::qlInitializers; LCDEngineRegistrar::LCDEngineRegistrar(LCDEngineNew cons) { if (! qlInitializers) qlInitializers = new QList(); n = cons; qlInitializers->append(n); } LCDEngineRegistrar::~LCDEngineRegistrar() { qlInitializers->removeAll(n); if (qlInitializers->isEmpty()) { delete qlInitializers; qlInitializers = NULL; } } static ConfigWidget *LCDConfigDialogNew(Settings &st) { return new LCDConfig(st); } class LCDDeviceManager : public DeferInit { protected: ConfigRegistrar *crLCD; public: QList qlEngines; QList qlDevices; void initialize(); void destroy(); }; void LCDDeviceManager::initialize() { if (LCDEngineRegistrar::qlInitializers) { foreach(LCDEngineNew engine, *LCDEngineRegistrar::qlInitializers) { LCDEngine *e = engine(); qlEngines.append(e); foreach(LCDDevice *d, e->devices()) { qlDevices << d; } } } if (qlDevices.count() > 0) { crLCD = new ConfigRegistrar(5900, LCDConfigDialogNew); } else { crLCD = NULL; } } void LCDDeviceManager::destroy() { qlDevices.clear(); foreach(LCDEngine *e, qlEngines) { delete e; } delete crLCD; } static LCDDeviceManager devmgr; /* --- */ LCDConfig::LCDConfig(Settings &st) : ConfigWidget(st) { setupUi(this); QTreeWidgetItem *qtwi; foreach(LCDDevice *d, devmgr.qlDevices) { qtwi = new QTreeWidgetItem(qtwDevices); qtwi->setFlags(Qt::ItemIsEnabled |Qt::ItemIsUserCheckable); qtwi->setText(0, d->name()); qtwi->setToolTip(0, Qt::escape(d->name())); QSize lcdsize = d->size(); QString qsSize = QString::fromLatin1("%1x%2").arg(lcdsize.width()).arg(lcdsize.height()); qtwi->setText(1, qsSize); qtwi->setToolTip(1, qsSize); qtwi->setCheckState(2, Qt::Unchecked); qtwi->setToolTip(2, tr("Enable this device")); } } QString LCDConfig::title() const { return tr("LCD"); } QIcon LCDConfig::icon() const { return QIcon(QLatin1String("skin:config_lcd.png")); } void LCDConfig::load(const Settings &r) { QList qlItems = qtwDevices->findItems(QString(), Qt::MatchContains); foreach(QTreeWidgetItem *qtwi, qlItems) { QString qsName = qtwi->text(0); bool enabled = r.qmLCDDevices.contains(qsName) ? r.qmLCDDevices.value(qsName) : true; qtwi->setCheckState(2, enabled ? Qt::Checked : Qt::Unchecked); } loadSlider(qsMinColWidth, r.iLCDUserViewMinColWidth); loadSlider(qsSplitterWidth, r.iLCDUserViewSplitterWidth); } void LCDConfig::save() const { QList qlItems = qtwDevices->findItems(QString(), Qt::MatchContains); foreach(QTreeWidgetItem *qtwi, qlItems) { QString qsName = qtwi->text(0); s.qmLCDDevices.insert(qsName, qtwi->checkState(2) == Qt::Checked); } s.iLCDUserViewMinColWidth = qsMinColWidth->value(); s.iLCDUserViewSplitterWidth = qsSplitterWidth->value(); } void LCDConfig::accept() const { foreach(LCDDevice *d, devmgr.qlDevices) { bool enabled = s.qmLCDDevices.value(d->name()); d->setEnabled(enabled); } g.lcd->updateUserView(); } void LCDConfig::on_qsMinColWidth_valueChanged(int v) { qlMinColWidth->setText(QString::number(v)); } void LCDConfig::on_qsSplitterWidth_valueChanged(int v) { qlSplitterWidth->setText(QString::number(v)); } /* --- */ LCD::LCD() : QObject() { #ifdef Q_OS_MAC qfNormal.setStyleStrategy(QFont::NoAntialias); qfNormal.setKerning(false); qfNormal.setPointSize(10); qfNormal.setFixedPitch(true); qfNormal.setFamily(QString::fromLatin1("Andale Mono")); #else qfNormal = QFont(QString::fromLatin1("Arial"), 7); #endif qfItalic = qfNormal; qfItalic.setItalic(true); qfBold = qfNormal; qfBold.setWeight(QFont::Black); qfItalicBold = qfBold; qfItalic.setItalic(true); QFontMetrics qfm(qfNormal); iFontHeight = 10; initBuffers(); iFrameIndex = 0; qtTimer = new QTimer(this); connect(qtTimer, SIGNAL(timeout()), this, SLOT(tick())); foreach(LCDDevice *d, devmgr.qlDevices) { bool enabled = g.s.qmLCDDevices.contains(d->name()) ? g.s.qmLCDDevices.value(d->name()) : true; d->setEnabled(enabled); } qiLogo = QIcon(QLatin1String("skin:mumble.svg")).pixmap(48,48).toImage().convertToFormat(QImage::Format_MonoLSB); #if QT_VERSION >= 0x050600 && QT_VERSION <= 0x050601 // Don't invert the logo image when using Qt 5.6. // See mumble-voip/mumble#2429 #else qiLogo.invertPixels(); #endif updateUserView(); } void LCD::tick() { iFrameIndex ++; updateUserView(); } void LCD::initBuffers() { foreach(LCDDevice *d, devmgr.qlDevices) { QSize size = d->size(); if (! qhImageBuffers.contains(size)) { size_t buflen = (size.width() * size.height()) / 8; qhImageBuffers[size] = new unsigned char[buflen]; qhImages[size] = new QImage(qhImageBuffers[size], size.width(), size.height(), QImage::Format_MonoLSB); } } } void LCD::destroyBuffers() { foreach(QImage *img, qhImages) delete img; qhImages.clear(); foreach(unsigned char *buf, qhImageBuffers) delete [] buf; qhImageBuffers.clear(); } struct ListEntry { QString qsString; bool bBold; bool bItalic; ListEntry(const QString &qs, bool bB, bool bI) : qsString(qs), bBold(bB), bItalic(bI) {}; }; static bool entriesSort(const ListEntry &a, const ListEntry &b) { return a.qsString < b.qsString; } void LCD::updateUserView() { if (qhImages.count() == 0) return; QStringList qslTalking; User *me = g.uiSession ? ClientUser::get(g.uiSession) : NULL; Channel *home = me ? me->cChannel : NULL; bool alert = false; foreach(const QSize &size, qhImages.keys()) { QImage *img = qhImages.value(size); QPainter painter(img); painter.setRenderHints(QPainter::Antialiasing | QPainter::TextAntialiasing, false); #if QT_VERSION >= 0x050600 && QT_VERSION <= 0x050601 // Use Qt::white instead of Qt::color1 on Qt 5.6. // See mumble-voip/mumble#2429 painter.setPen(Qt::white); #else painter.setPen(Qt::color1); #endif painter.setFont(qfNormal); img->fill(Qt::color0); if (! me) { qmNew.clear(); qmOld.clear(); qmSpeaking.clear(); qmNameCache.clear(); painter.drawImage(0, 0, qiLogo); painter.drawText(60, 20, tr("Not connected")); continue; } foreach(User *p, me->cChannel->qlUsers) { if (! qmNew.contains(p->uiSession)) { qmNew.insert(p->uiSession, Timer()); qmNameCache.insert(p->uiSession, p->qsName); qmOld.remove(p->uiSession); } } foreach(unsigned int session, qmNew.keys()) { User *p = ClientUser::get(session); if (!p || (p->cChannel != me->cChannel)) { qmNew.remove(session); qmOld.insert(session, Timer()); } } QMap old; foreach(unsigned int session, qmOld.keys()) { Timer t = qmOld.value(session); if (t.elapsed() > 3000000) { qmNameCache.remove(session); } else { old.insert(session, qmOld.value(session)); } } qmOld = old; QList entries; entries << ListEntry(QString::fromLatin1("[%1:%2]").arg(me->cChannel->qsName).arg(me->cChannel->qlUsers.count()), false, false); bool hasnew = false; QMap speaking; foreach(Channel *c, home->allLinks()) { foreach(User *p, c->qlUsers) { ClientUser *u = static_cast(p); bool bTalk = (u->tsState != Settings::Passive); if (bTalk) { speaking.insert(p->uiSession, Timer()); } else if (qmSpeaking.contains(p->uiSession)) { Timer t = qmSpeaking.value(p->uiSession); if (t.elapsed() > 1000000) qmSpeaking.remove(p->uiSession); else { speaking.insert(p->uiSession, t); bTalk = true; } } if (bTalk) { alert = true; entries << ListEntry(p->qsName, true, (p->cChannel != me->cChannel)); } else if (c == me->cChannel) { if (qmNew.value(p->uiSession).elapsed() < 3000000) { entries << ListEntry(QLatin1String("+") + p->qsName, false, false); hasnew = true; } } } } qmSpeaking = speaking; foreach(unsigned int session, qmOld.keys()) { entries << ListEntry(QLatin1String("-") + qmNameCache.value(session), false, false); } if (! qmOld.isEmpty() || hasnew || ! qmSpeaking.isEmpty()) qtTimer->start(500); else qtTimer->stop(); qSort(++ entries.begin(), entries.end(), entriesSort); const int iWidth = size.width(); const int iHeight = size.height(); const int iUsersPerColumn = iHeight / iFontHeight; const int iSplitterWidth = g.s.iLCDUserViewSplitterWidth; const int iUserColumns = (entries.count() + iUsersPerColumn - 1) / iUsersPerColumn; int iColumns = iUserColumns; int iColumnWidth = 1; while (iColumns >= 1) { iColumnWidth = (iWidth - (iColumns-1)*iSplitterWidth) / iColumns; if (iColumnWidth >= g.s.iLCDUserViewMinColWidth) break; --iColumns; } int row = 0, col = 0; foreach(const ListEntry &le, entries) { if (row >= iUsersPerColumn) { row = 0; ++col; } if (col > iColumns) break; if (! le.qsString.isEmpty()) { if (le.bBold && le.bItalic) painter.setFont(qfItalicBold); else if (le.bBold) painter.setFont(qfBold); else if (le.bItalic) painter.setFont(qfItalic); else painter.setFont(qfNormal); painter.drawText(QRect(col * (iColumnWidth + iSplitterWidth), row * iFontHeight, iColumnWidth, iFontHeight+2), Qt::AlignLeft, le.qsString); } ++row; } } foreach(LCDDevice *d, devmgr.qlDevices) { QImage *img = qhImages[d->size()]; if (! img) continue; d->blitImage(img, alert); } } LCD::~LCD() { destroyBuffers(); } bool LCD::hasDevices() { return (! devmgr.qlDevices.isEmpty()); } /* --- */ LCDEngine::LCDEngine() : QObject() { } LCDEngine::~LCDEngine() { foreach(LCDDevice *lcd, qlDevices) delete lcd; } LCDDevice::LCDDevice() { } LCDDevice::~LCDDevice() { } /* --- */ uint qHash(const QSize &size) { return ((size.width() & 0xffff) << 16) | (size.height() & 0xffff); }