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