1 // Copyright 2005-2019 The Mumble Developers. All rights reserved.
2 // Use of this source code is governed by a BSD-style license
3 // that can be found in the LICENSE file at the root of the
4 // Mumble source tree or at <https://www.mumble.info/LICENSE>.
5
6 #include "mumble_pch.hpp"
7
8 #include "LCD.h"
9
10 #include "ClientUser.h"
11 #include "Channel.h"
12 #include "Message.h"
13 #include "ServerHandler.h"
14
15 // 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.
16 #include "Global.h"
17
18 QList<LCDEngineNew> *LCDEngineRegistrar::qlInitializers;
19
LCDEngineRegistrar(LCDEngineNew cons)20 LCDEngineRegistrar::LCDEngineRegistrar(LCDEngineNew cons) {
21 if (! qlInitializers)
22 qlInitializers = new QList<LCDEngineNew>();
23 n = cons;
24 qlInitializers->append(n);
25 }
26
~LCDEngineRegistrar()27 LCDEngineRegistrar::~LCDEngineRegistrar() {
28 qlInitializers->removeAll(n);
29 if (qlInitializers->isEmpty()) {
30 delete qlInitializers;
31 qlInitializers = NULL;
32 }
33 }
34
LCDConfigDialogNew(Settings & st)35 static ConfigWidget *LCDConfigDialogNew(Settings &st) {
36 return new LCDConfig(st);
37 }
38
39 class LCDDeviceManager : public DeferInit {
40 protected:
41 ConfigRegistrar *crLCD;
42 public:
43 QList<LCDEngine *> qlEngines;
44 QList<LCDDevice *> qlDevices;
45 void initialize();
46 void destroy();
47 };
48
initialize()49 void LCDDeviceManager::initialize() {
50 if (LCDEngineRegistrar::qlInitializers) {
51 foreach(LCDEngineNew engine, *LCDEngineRegistrar::qlInitializers) {
52 LCDEngine *e = engine();
53 qlEngines.append(e);
54
55 foreach(LCDDevice *d, e->devices()) {
56 qlDevices << d;
57 }
58 }
59 }
60 if (qlDevices.count() > 0) {
61 crLCD = new ConfigRegistrar(5900, LCDConfigDialogNew);
62 } else {
63 crLCD = NULL;
64 }
65 }
66
destroy()67 void LCDDeviceManager::destroy() {
68 qlDevices.clear();
69 foreach(LCDEngine *e, qlEngines) {
70 delete e;
71 }
72 delete crLCD;
73 }
74
75 static LCDDeviceManager devmgr;
76
77 /* --- */
78
79
LCDConfig(Settings & st)80 LCDConfig::LCDConfig(Settings &st) : ConfigWidget(st) {
81 setupUi(this);
82
83 QTreeWidgetItem *qtwi;
84 foreach(LCDDevice *d, devmgr.qlDevices) {
85 qtwi = new QTreeWidgetItem(qtwDevices);
86
87 qtwi->setFlags(Qt::ItemIsEnabled |Qt::ItemIsUserCheckable);
88
89 qtwi->setText(0, d->name());
90 qtwi->setToolTip(0, Qt::escape(d->name()));
91
92 QSize lcdsize = d->size();
93 QString qsSize = QString::fromLatin1("%1x%2").arg(lcdsize.width()).arg(lcdsize.height());
94 qtwi->setText(1, qsSize);
95 qtwi->setToolTip(1, qsSize);
96
97 qtwi->setCheckState(2, Qt::Unchecked);
98 qtwi->setToolTip(2, tr("Enable this device"));
99 }
100 }
101
title() const102 QString LCDConfig::title() const {
103 return tr("LCD");
104 }
105
icon() const106 QIcon LCDConfig::icon() const {
107 return QIcon(QLatin1String("skin:config_lcd.png"));
108 }
109
load(const Settings & r)110 void LCDConfig::load(const Settings &r) {
111 QList<QTreeWidgetItem *> qlItems = qtwDevices->findItems(QString(), Qt::MatchContains);
112 foreach(QTreeWidgetItem *qtwi, qlItems) {
113 QString qsName = qtwi->text(0);
114 bool enabled = r.qmLCDDevices.contains(qsName) ? r.qmLCDDevices.value(qsName) : true;
115 qtwi->setCheckState(2, enabled ? Qt::Checked : Qt::Unchecked);
116 }
117
118 loadSlider(qsMinColWidth, r.iLCDUserViewMinColWidth);
119 loadSlider(qsSplitterWidth, r.iLCDUserViewSplitterWidth);
120 }
121
save() const122 void LCDConfig::save() const {
123 QList<QTreeWidgetItem *> qlItems = qtwDevices->findItems(QString(), Qt::MatchContains);
124
125 foreach(QTreeWidgetItem *qtwi, qlItems) {
126 QString qsName = qtwi->text(0);
127 s.qmLCDDevices.insert(qsName, qtwi->checkState(2) == Qt::Checked);
128 }
129
130 s.iLCDUserViewMinColWidth = qsMinColWidth->value();
131 s.iLCDUserViewSplitterWidth = qsSplitterWidth->value();
132 }
133
accept() const134 void LCDConfig::accept() const {
135 foreach(LCDDevice *d, devmgr.qlDevices) {
136 bool enabled = s.qmLCDDevices.value(d->name());
137 d->setEnabled(enabled);
138 }
139 g.lcd->updateUserView();
140 }
141
on_qsMinColWidth_valueChanged(int v)142 void LCDConfig::on_qsMinColWidth_valueChanged(int v) {
143 qlMinColWidth->setText(QString::number(v));
144 }
145
on_qsSplitterWidth_valueChanged(int v)146 void LCDConfig::on_qsSplitterWidth_valueChanged(int v) {
147 qlSplitterWidth->setText(QString::number(v));
148 }
149
150 /* --- */
151
LCD()152 LCD::LCD() : QObject() {
153
154 #ifdef Q_OS_MAC
155 qfNormal.setStyleStrategy(QFont::NoAntialias);
156 qfNormal.setKerning(false);
157 qfNormal.setPointSize(10);
158 qfNormal.setFixedPitch(true);
159 qfNormal.setFamily(QString::fromLatin1("Andale Mono"));
160 #else
161 qfNormal = QFont(QString::fromLatin1("Arial"), 7);
162 #endif
163
164 qfItalic = qfNormal;
165 qfItalic.setItalic(true);
166
167 qfBold = qfNormal;
168 qfBold.setWeight(QFont::Black);
169
170 qfItalicBold = qfBold;
171 qfItalic.setItalic(true);
172
173 QFontMetrics qfm(qfNormal);
174
175 iFontHeight = 10;
176
177 initBuffers();
178
179 iFrameIndex = 0;
180
181 qtTimer = new QTimer(this);
182 connect(qtTimer, SIGNAL(timeout()), this, SLOT(tick()));
183
184 foreach(LCDDevice *d, devmgr.qlDevices) {
185 bool enabled = g.s.qmLCDDevices.contains(d->name()) ? g.s.qmLCDDevices.value(d->name()) : true;
186 d->setEnabled(enabled);
187 }
188 qiLogo = QIcon(QLatin1String("skin:mumble.svg")).pixmap(48,48).toImage().convertToFormat(QImage::Format_MonoLSB);
189
190 #if QT_VERSION >= 0x050600 && QT_VERSION <= 0x050601
191 // Don't invert the logo image when using Qt 5.6.
192 // See mumble-voip/mumble#2429
193 #else
194 qiLogo.invertPixels();
195 #endif
196
197 updateUserView();
198 }
199
tick()200 void LCD::tick() {
201 iFrameIndex ++;
202 updateUserView();
203 }
204
initBuffers()205 void LCD::initBuffers() {
206 foreach(LCDDevice *d, devmgr.qlDevices) {
207 QSize size = d->size();
208 if (! qhImageBuffers.contains(size)) {
209 size_t buflen = (size.width() * size.height()) / 8;
210 qhImageBuffers[size] = new unsigned char[buflen];
211 qhImages[size] = new QImage(qhImageBuffers[size], size.width(), size.height(), QImage::Format_MonoLSB);
212 }
213 }
214 }
215
destroyBuffers()216 void LCD::destroyBuffers() {
217 foreach(QImage *img, qhImages)
218 delete img;
219 qhImages.clear();
220
221 foreach(unsigned char *buf, qhImageBuffers)
222 delete [] buf;
223 qhImageBuffers.clear();
224 }
225
226 struct ListEntry {
227 QString qsString;
228 bool bBold;
229 bool bItalic;
ListEntryListEntry230 ListEntry(const QString &qs, bool bB, bool bI) : qsString(qs), bBold(bB), bItalic(bI) {};
231 };
232
entriesSort(const ListEntry & a,const ListEntry & b)233 static bool entriesSort(const ListEntry &a, const ListEntry &b) {
234 return a.qsString < b.qsString;
235 }
236
updateUserView()237 void LCD::updateUserView() {
238 if (qhImages.count() == 0)
239 return;
240
241 QStringList qslTalking;
242 User *me = g.uiSession ? ClientUser::get(g.uiSession) : NULL;
243 Channel *home = me ? me->cChannel : NULL;
244 bool alert = false;
245
246 foreach(const QSize &size, qhImages.keys()) {
247 QImage *img = qhImages.value(size);
248 QPainter painter(img);
249 painter.setRenderHints(QPainter::Antialiasing | QPainter::TextAntialiasing, false);
250
251 #if QT_VERSION >= 0x050600 && QT_VERSION <= 0x050601
252 // Use Qt::white instead of Qt::color1 on Qt 5.6.
253 // See mumble-voip/mumble#2429
254 painter.setPen(Qt::white);
255 #else
256 painter.setPen(Qt::color1);
257 #endif
258
259 painter.setFont(qfNormal);
260
261 img->fill(Qt::color0);
262
263 if (! me) {
264 qmNew.clear();
265 qmOld.clear();
266 qmSpeaking.clear();
267 qmNameCache.clear();
268 painter.drawImage(0, 0, qiLogo);
269 painter.drawText(60, 20, tr("Not connected"));
270 continue;
271 }
272
273 foreach(User *p, me->cChannel->qlUsers) {
274 if (! qmNew.contains(p->uiSession)) {
275 qmNew.insert(p->uiSession, Timer());
276 qmNameCache.insert(p->uiSession, p->qsName);
277 qmOld.remove(p->uiSession);
278 }
279 }
280
281 foreach(unsigned int session, qmNew.keys()) {
282 User *p = ClientUser::get(session);
283 if (!p || (p->cChannel != me->cChannel)) {
284 qmNew.remove(session);
285 qmOld.insert(session, Timer());
286 }
287 }
288
289 QMap<unsigned int, Timer> old;
290
291 foreach(unsigned int session, qmOld.keys()) {
292 Timer t = qmOld.value(session);
293 if (t.elapsed() > 3000000) {
294 qmNameCache.remove(session);
295 } else {
296 old.insert(session, qmOld.value(session));
297 }
298 }
299 qmOld = old;
300
301 QList<struct ListEntry> entries;
302 entries << ListEntry(QString::fromLatin1("[%1:%2]").arg(me->cChannel->qsName).arg(me->cChannel->qlUsers.count()), false, false);
303
304 bool hasnew = false;
305
306 QMap<unsigned int, Timer> speaking;
307
308 foreach(Channel *c, home->allLinks()) {
309 foreach(User *p, c->qlUsers) {
310 ClientUser *u = static_cast<ClientUser *>(p);
311 bool bTalk = (u->tsState != Settings::Passive);
312 if (bTalk) {
313 speaking.insert(p->uiSession, Timer());
314 } else if (qmSpeaking.contains(p->uiSession)) {
315 Timer t = qmSpeaking.value(p->uiSession);
316 if (t.elapsed() > 1000000)
317 qmSpeaking.remove(p->uiSession);
318 else {
319 speaking.insert(p->uiSession, t);
320 bTalk = true;
321 }
322 }
323 if (bTalk) {
324 alert = true;
325 entries << ListEntry(p->qsName, true, (p->cChannel != me->cChannel));
326 } else if (c == me->cChannel) {
327 if (qmNew.value(p->uiSession).elapsed() < 3000000) {
328 entries << ListEntry(QLatin1String("+") + p->qsName, false, false);
329 hasnew = true;
330 }
331 }
332 }
333 }
334 qmSpeaking = speaking;
335
336 foreach(unsigned int session, qmOld.keys()) {
337 entries << ListEntry(QLatin1String("-") + qmNameCache.value(session), false, false);
338 }
339
340 if (! qmOld.isEmpty() || hasnew || ! qmSpeaking.isEmpty())
341 qtTimer->start(500);
342 else
343 qtTimer->stop();
344
345 qSort(++ entries.begin(), entries.end(), entriesSort);
346
347 const int iWidth = size.width();
348 const int iHeight = size.height();
349 const int iUsersPerColumn = iHeight / iFontHeight;
350 const int iSplitterWidth = g.s.iLCDUserViewSplitterWidth;
351 const int iUserColumns = (entries.count() + iUsersPerColumn - 1) / iUsersPerColumn;
352
353 int iColumns = iUserColumns;
354 int iColumnWidth = 1;
355
356 while (iColumns >= 1) {
357 iColumnWidth = (iWidth - (iColumns-1)*iSplitterWidth) / iColumns;
358 if (iColumnWidth >= g.s.iLCDUserViewMinColWidth)
359 break;
360 --iColumns;
361 }
362
363 int row = 0, col = 0;
364
365
366 foreach(const ListEntry &le, entries) {
367 if (row >= iUsersPerColumn) {
368 row = 0;
369 ++col;
370 }
371 if (col > iColumns)
372 break;
373
374 if (! le.qsString.isEmpty()) {
375 if (le.bBold && le.bItalic)
376 painter.setFont(qfItalicBold);
377 else if (le.bBold)
378 painter.setFont(qfBold);
379 else if (le.bItalic)
380 painter.setFont(qfItalic);
381 else
382 painter.setFont(qfNormal);
383 painter.drawText(QRect(col * (iColumnWidth + iSplitterWidth),
384 row * iFontHeight, iColumnWidth, iFontHeight+2), Qt::AlignLeft, le.qsString);
385 }
386 ++row;
387 }
388 }
389
390 foreach(LCDDevice *d, devmgr.qlDevices) {
391 QImage *img = qhImages[d->size()];
392 if (! img)
393 continue;
394 d->blitImage(img, alert);
395 }
396 }
397
~LCD()398 LCD::~LCD() {
399 destroyBuffers();
400 }
401
hasDevices()402 bool LCD::hasDevices() {
403 return (! devmgr.qlDevices.isEmpty());
404 }
405
406 /* --- */
407
LCDEngine()408 LCDEngine::LCDEngine() : QObject() {
409 }
410
~LCDEngine()411 LCDEngine::~LCDEngine() {
412 foreach(LCDDevice *lcd, qlDevices)
413 delete lcd;
414 }
415
LCDDevice()416 LCDDevice::LCDDevice() {
417 }
418
~LCDDevice()419 LCDDevice::~LCDDevice() {
420 }
421
422 /* --- */
423
qHash(const QSize & size)424 uint qHash(const QSize &size) {
425 return ((size.width() & 0xffff) << 16) | (size.height() & 0xffff);
426 }
427