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 "CustomElements.h"
9
10 #include "ClientUser.h"
11 #include "MainWindow.h"
12 #include "Log.h"
13
14 // 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.
15 #include "Global.h"
16
LogTextBrowser(QWidget * p)17 LogTextBrowser::LogTextBrowser(QWidget *p) : QTextBrowser(p) {}
18
resizeEvent(QResizeEvent * e)19 void LogTextBrowser::resizeEvent(QResizeEvent *e) {
20 scrollLogToBottom();
21 QTextBrowser::resizeEvent(e);
22 }
23
event(QEvent * e)24 bool LogTextBrowser::event(QEvent *e) {
25 if (e->type() == LogDocumentResourceAddedEvent::Type) {
26 scrollLogToBottom();
27 }
28 return QTextBrowser::event(e);
29 }
30
getLogScroll()31 int LogTextBrowser::getLogScroll() {
32 return verticalScrollBar()->value();
33 }
34
getLogScrollMaximum()35 int LogTextBrowser::getLogScrollMaximum() {
36 return verticalScrollBar()->maximum();
37 }
38
setLogScroll(int scroll_pos)39 void LogTextBrowser::setLogScroll(int scroll_pos) {
40 verticalScrollBar()->setValue(scroll_pos);
41 }
42
scrollLogToBottom()43 void LogTextBrowser::scrollLogToBottom() {
44 verticalScrollBar()->setValue(verticalScrollBar()->maximum());
45 }
46
47
focusInEvent(QFocusEvent * qfe)48 void ChatbarTextEdit::focusInEvent(QFocusEvent *qfe) {
49 inFocus(true);
50 QTextEdit::focusInEvent(qfe);
51 }
52
focusOutEvent(QFocusEvent * qfe)53 void ChatbarTextEdit::focusOutEvent(QFocusEvent *qfe) {
54 inFocus(false);
55 QTextEdit::focusOutEvent(qfe);
56 }
57
inFocus(bool focus)58 void ChatbarTextEdit::inFocus(bool focus) {
59 if (focus) {
60 if (bDefaultVisible) {
61 QFont f = font();
62 f.setItalic(false);
63 setFont(f);
64 setPlainText(QString());
65 bDefaultVisible = false;
66 }
67 } else {
68 if (toPlainText().trimmed().isEmpty() || bDefaultVisible) {
69 QFont f = font();
70 f.setItalic(true);
71 setFont(f);
72 setHtml(qsDefaultText);
73 bDefaultVisible = true;
74 } else {
75 bDefaultVisible = false;
76 }
77 }
78 }
79
contextMenuEvent(QContextMenuEvent * qcme)80 void ChatbarTextEdit::contextMenuEvent(QContextMenuEvent *qcme) {
81 QMenu *menu = createStandardContextMenu();
82
83 QAction *action = new QAction(tr("Paste and &Send") + QLatin1Char('\t'), menu);
84 action->setEnabled(!QApplication::clipboard()->text().isEmpty());
85 connect(action, SIGNAL(triggered()), this, SLOT(pasteAndSend_triggered()));
86 if (menu->actions().count() > 6)
87 menu->insertAction(menu->actions()[6], action);
88 else
89 menu->addAction(action);
90
91 menu->exec(qcme->globalPos());
92 delete menu;
93 }
94
dropEvent(QDropEvent * evt)95 void ChatbarTextEdit::dropEvent(QDropEvent *evt) {
96 inFocus(true);
97 QTextEdit::dropEvent(evt);
98 }
99
ChatbarTextEdit(QWidget * p)100 ChatbarTextEdit::ChatbarTextEdit(QWidget *p) : QTextEdit(p), iHistoryIndex(-1) {
101 setWordWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere);
102 setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
103 setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
104 setMinimumHeight(0);
105 connect(this, SIGNAL(textChanged()), SLOT(doResize()));
106
107 bDefaultVisible = true;
108 setDefaultText(tr("<center>Type chat message here</center>"));
109 }
110
minimumSizeHint() const111 QSize ChatbarTextEdit::minimumSizeHint() const {
112 return QSize(0, fontMetrics().height());
113 }
114
sizeHint() const115 QSize ChatbarTextEdit::sizeHint() const {
116 QSize sh = QTextEdit::sizeHint();
117 const int minHeight = minimumSizeHint().height();
118 const int documentHeight = document()->documentLayout()->documentSize().height();
119 sh.setHeight(std::max(minHeight, documentHeight));
120 const_cast<ChatbarTextEdit *>(this)->setMaximumHeight(sh.height());
121 return sh;
122 }
123
resizeEvent(QResizeEvent * e)124 void ChatbarTextEdit::resizeEvent(QResizeEvent *e) {
125 QTextEdit::resizeEvent(e);
126 QTimer::singleShot(0, this, SLOT(doScrollbar()));
127 }
128
doResize()129 void ChatbarTextEdit::doResize() {
130 updateGeometry();
131 QTimer::singleShot(0, this, SLOT(doScrollbar()));
132 }
133
doScrollbar()134 void ChatbarTextEdit::doScrollbar() {
135 setVerticalScrollBarPolicy(sizeHint().height() > height() ? Qt::ScrollBarAlwaysOn : Qt::ScrollBarAlwaysOff);
136 ensureCursorVisible();
137 }
138
setDefaultText(const QString & new_default,bool force)139 void ChatbarTextEdit::setDefaultText(const QString &new_default, bool force) {
140 qsDefaultText = new_default;
141
142 if (bDefaultVisible || force) {
143 QFont f = font();
144 f.setItalic(true);
145 setFont(f);
146 setHtml(qsDefaultText);
147 bDefaultVisible = true;
148 }
149 }
150
event(QEvent * evt)151 bool ChatbarTextEdit::event(QEvent *evt) {
152 if (evt->type() == QEvent::ShortcutOverride) {
153 return false;
154 }
155
156 if (evt->type() == QEvent::KeyPress) {
157 QKeyEvent *kev = static_cast<QKeyEvent*>(evt);
158 if ((kev->key() == Qt::Key_Enter || kev->key() == Qt::Key_Return) && !(kev->modifiers() & Qt::ShiftModifier)) {
159 const QString msg = toPlainText();
160 if (!msg.isEmpty()) {
161 addToHistory(msg);
162 emit entered(msg);
163 }
164 return true;
165 }
166 if (kev->key() == Qt::Key_Tab) {
167 emit tabPressed();
168 return true;
169 } else if (kev->key() == Qt::Key_Backtab) {
170 emit backtabPressed();
171 return true;
172 } else if (kev->key() == Qt::Key_Space && kev->modifiers() == Qt::ControlModifier) {
173 emit ctrlSpacePressed();
174 return true;
175 } else if (kev->key() == Qt::Key_Up && kev->modifiers() == Qt::ControlModifier) {
176 historyUp();
177 return true;
178 } else if (kev->key() == Qt::Key_Down && kev->modifiers() == Qt::ControlModifier) {
179 historyDown();
180 return true;
181 }
182 }
183 return QTextEdit::event(evt);
184 }
185
186 /**
187 * The bar will try to complete the username, if the nickname
188 * is already complete it will try to find a longer match. If
189 * there is none it will cycle the nicknames alphabetically.
190 * Nothing is done on mismatch.
191 */
completeAtCursor()192 unsigned int ChatbarTextEdit::completeAtCursor() {
193 // Get an alphabetically sorted list of usernames
194 unsigned int id = 0;
195
196 QList<QString> qlsUsernames;
197
198 if (ClientUser::c_qmUsers.empty()) return id;
199 foreach(ClientUser *usr, ClientUser::c_qmUsers) {
200 qlsUsernames.append(usr->qsName);
201 }
202 qSort(qlsUsernames);
203
204 QString target = QString();
205 QTextCursor tc = textCursor();
206
207 if (toPlainText().isEmpty() || tc.position() == 0) {
208 target = qlsUsernames.first();
209 tc.insertText(target);
210 } else {
211 bool bBaseIsName = false;
212 int iend = tc.position();
213 int istart = toPlainText().lastIndexOf(QLatin1Char(' '), iend - 1) + 1;
214 QString base = toPlainText().mid(istart, iend - istart);
215 tc.setPosition(istart);
216 tc.setPosition(iend, QTextCursor::KeepAnchor);
217
218 if (qlsUsernames.last() == base) {
219 bBaseIsName = true;
220 target = qlsUsernames.first();
221 } else {
222 if (qlsUsernames.contains(base)) {
223 // Prevent to complete to what's already there
224 while (qlsUsernames.takeFirst() != base) {}
225 bBaseIsName = true;
226 }
227
228 foreach(QString name, qlsUsernames) {
229 if (name.startsWith(base, Qt::CaseInsensitive)) {
230 target = name;
231 break;
232 }
233 }
234 }
235
236 if (bBaseIsName && target.isEmpty() && !qlsUsernames.empty()) {
237 // If autocomplete failed and base was a name get the next one
238 target = qlsUsernames.first();
239 }
240
241 if (!target.isEmpty()) {
242 tc.insertText(target);
243 }
244 }
245
246 if (!target.isEmpty()) {
247 setTextCursor(tc);
248
249 foreach(ClientUser *usr, ClientUser::c_qmUsers) {
250 if (usr->qsName == target) {
251 id = usr->uiSession;
252 break;
253 }
254 }
255 }
256 return id;
257 }
258
addToHistory(const QString & str)259 void ChatbarTextEdit::addToHistory(const QString &str) {
260 iHistoryIndex = -1;
261 qslHistory.push_front(str);
262 if (qslHistory.length() > MAX_HISTORY) {
263 qslHistory.pop_back();
264 }
265 }
266
historyUp()267 void ChatbarTextEdit::historyUp() {
268 if (qslHistory.length() == 0)
269 return;
270
271 if (iHistoryIndex == -1) {
272 qsHistoryTemp = toPlainText();
273 }
274
275 if (iHistoryIndex < qslHistory.length() - 1) {
276 setPlainText(qslHistory[++iHistoryIndex]);
277 moveCursor(QTextCursor::End);
278 }
279 }
280
historyDown()281 void ChatbarTextEdit::historyDown() {
282 if (iHistoryIndex < 0) {
283 return;
284 } else if (iHistoryIndex == 0) {
285 setPlainText(qsHistoryTemp);
286 iHistoryIndex--;
287 } else {
288 setPlainText(qslHistory[--iHistoryIndex]);
289 }
290 moveCursor(QTextCursor::End);
291 }
292
pasteAndSend_triggered()293 void ChatbarTextEdit::pasteAndSend_triggered() {
294 paste();
295 addToHistory(toPlainText());
296 emit entered(toPlainText());
297 }
298
DockTitleBar()299 DockTitleBar::DockTitleBar() : QLabel(tr("Drag here")) {
300 setAlignment(Qt::AlignCenter);
301 setEnabled(false);
302 qtTick = new QTimer(this);
303 qtTick->setSingleShot(true);
304 connect(qtTick, SIGNAL(timeout()), this, SLOT(tick()));
305 size = newsize = 0;
306 }
307
sizeHint() const308 QSize DockTitleBar::sizeHint() const {
309 return minimumSizeHint();
310 }
311
minimumSizeHint() const312 QSize DockTitleBar::minimumSizeHint() const {
313 return QSize(size,size);
314 }
315
eventFilter(QObject *,QEvent * evt)316 bool DockTitleBar::eventFilter(QObject *, QEvent *evt) {
317 QDockWidget *qdw = qobject_cast<QDockWidget*>(parentWidget());
318
319 if (! this->isEnabled())
320 return false;
321
322 switch (evt->type()) {
323 case QEvent::Leave:
324 case QEvent::Enter:
325 case QEvent::MouseMove:
326 case QEvent::MouseButtonRelease: {
327 newsize = 0;
328 QPoint p = qdw->mapFromGlobal(QCursor::pos());
329 if ((p.x() >= iroundf(static_cast<float>(qdw->width()) * 0.1f + 0.5f)) && (p.x() < iroundf(static_cast<float>(qdw->width()) * 0.9f + 0.5f)) && (p.y() >= 0) && (p.y() < 15))
330 newsize = 15;
331 if (newsize > 0 && !qtTick->isActive())
332 qtTick->start(500);
333 else if ((newsize == size) && qtTick->isActive())
334 qtTick->stop();
335 else if (newsize == 0)
336 tick();
337 }
338 default:
339 break;
340 }
341
342 return false;
343 }
344
tick()345 void DockTitleBar::tick() {
346 QDockWidget *qdw = qobject_cast<QDockWidget*>(parentWidget());
347
348 if (newsize == size)
349 return;
350
351 size = newsize;
352 qdw->setTitleBarWidget(this);
353 }
354