1 /***************************************************************************
2 * *
3 * This program is free software; you can redistribute it and/or modify *
4 * it under the terms of the GNU General Public License as published by *
5 * the Free Software Foundation; either version 3 of the License, or *
6 * (at your option) any later version. *
7 * *
8 ***************************************************************************/
9
10 #include "ChatEdit.h"
11 #include "WulforUtil.h"
12
13 #include "dcpp/HashManager.h"
14
15 #include <QCompleter>
16 #include <QKeyEvent>
17 #include <QScrollBar>
18 #include <QTextBlock>
19 #include <QUrl>
20 #include <QFileInfo>
21 #include <QDir>
22 #include <QMimeData>
23
ChatEdit(QWidget * parent)24 ChatEdit::ChatEdit(QWidget *parent) : QTextEdit(parent), cc(NULL)
25 {
26 setMinimumHeight(10);
27
28 setWordWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere);
29 setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
30 setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
31
32 connect(this, SIGNAL(textChanged()), this, SLOT(recalculateGeometry()));
33 }
34
~ChatEdit()35 ChatEdit::~ChatEdit()
36 {}
37
setCompleter(QCompleter * completer,UserListModel * model)38 void ChatEdit::setCompleter(QCompleter *completer, UserListModel *model)
39 {
40 if (cc)
41 QObject::disconnect(cc, 0, this, 0);
42
43 cc = completer;
44
45 if (!cc || !model)
46 return;
47
48 cc->setWidget(this);
49 cc->setWrapAround(false);
50 cc->setCaseSensitivity(Qt::CaseInsensitive);
51 cc->setCompletionMode(QCompleter::PopupCompletion);
52
53 cc_model = model;
54
55 QObject::connect(cc, SIGNAL(activated(const QModelIndex&)),
56 this, SLOT(insertCompletion(const QModelIndex&)));
57 }
58
minimumSizeHint() const59 QSize ChatEdit::minimumSizeHint() const{
60 QSize sh = QTextEdit::minimumSizeHint();
61 sh.setHeight(fontMetrics().height() + 1);
62 sh += QSize(0, QFrame::lineWidth() * 2);
63 return sh;
64 }
65
sizeHint() const66 QSize ChatEdit::sizeHint() const{
67 QSize sh = QTextEdit::sizeHint();
68 sh.setHeight(int(document()->documentLayout()->documentSize().height()));
69 sh += QSize(0, QFrame::lineWidth() * 2);
70 ((QTextEdit*)this)->setMaximumHeight(sh.height());
71 return sh;
72 }
73
insertCompletion(const QModelIndex & index)74 void ChatEdit::insertCompletion(const QModelIndex & index)
75 {
76 if (cc->widget() != this || !index.isValid())
77 return;
78
79 QString nick = cc->completionModel()->index(index.row(), index.column()).data().toString();
80 int begin = textCursor().position() - cc->completionPrefix().length();
81
82 insertToPos(nick, begin);
83 }
84
insertToPos(const QString & completeText,int begin)85 void ChatEdit::insertToPos(const QString & completeText, int begin)
86 {
87 if (completeText.isEmpty())
88 return;
89
90 if (begin < 0)
91 begin = 0;
92
93 QTextCursor cursor = textCursor();
94 int end = cursor.position();
95 cursor.setPosition(begin);
96 cursor.setPosition(end, QTextCursor::KeepAnchor);
97
98 if (!begin)
99 cursor.insertText(completeText + ": ");
100 else
101 cursor.insertText(completeText + " ");
102
103 setTextCursor(cursor);
104 }
105
textUnderCursor() const106 QString ChatEdit::textUnderCursor() const
107 {
108 QTextCursor cursor = textCursor();
109
110 int curpos = cursor.position();
111 QString text = cursor.block().text().left(curpos);
112
113 QStringList wordList = text.split(QRegExp("\\s"));
114
115 if (wordList.isEmpty())
116 return QString();
117
118 return wordList.last();
119 }
120
focusInEvent(QFocusEvent * e)121 void ChatEdit::focusInEvent(QFocusEvent *e)
122 {
123 if (cc)
124 cc->setWidget(this);
125
126 QTextEdit::focusInEvent(e);
127 }
128
keyPressEvent(QKeyEvent * e)129 void ChatEdit::keyPressEvent(QKeyEvent *e)
130 {
131 const bool ctrlOrShift = e->modifiers() & (Qt::ControlModifier | Qt::ShiftModifier);
132 bool hasModifier = (e->modifiers() != Qt::NoModifier) &&
133 (e->modifiers() != Qt::KeypadModifier) &&
134 !ctrlOrShift;
135
136 if (e->key() == Qt::Key_Tab) {
137 if (!toPlainText().isEmpty()) {
138 if (cc && cc->popup()->isVisible()) {
139 int row = cc->popup()->currentIndex().row() + 1;
140 if (cc->completionModel()->rowCount() == row)
141 row = 0;
142 cc->popup()->setCurrentIndex(cc->completionModel()->index(row, 0));
143 }
144 e->accept();
145 } else {
146 e->ignore();
147 }
148 return;
149 }
150
151 if (cc && cc->popup()->isVisible()) {
152 switch (e->key()) {
153 case Qt::Key_Enter:
154 case Qt::Key_Return:
155 case Qt::Key_Escape:
156 case Qt::Key_Backtab:
157 e->ignore();
158 return;
159 default:
160 break;
161 }
162 }
163
164 if (!cc || !cc->popup()->isVisible() || !hasModifier)
165 QTextEdit::keyPressEvent(e);
166
167 if (ctrlOrShift && e->text().isEmpty())
168 return;
169
170 if (cc->popup()->isVisible() && (hasModifier || e->text().isEmpty())) {
171 cc->popup()->hide();
172 return;
173 }
174
175 if (cc->popup()->isVisible())
176 complete();
177 }
178
keyReleaseEvent(QKeyEvent * e)179 void ChatEdit::keyReleaseEvent(QKeyEvent *e)
180 {
181 bool hasModifier = (e->modifiers() != Qt::NoModifier);
182
183 switch (e->key()) {
184 case Qt::Key_Tab:
185 if (cc && !hasModifier && !cc->popup()->isVisible())
186 complete();
187
188 case Qt::Key_Enter:
189 case Qt::Key_Return:
190 e->ignore();
191 return;
192 default:
193 break;
194 }
195 }
196
complete()197 void ChatEdit::complete()
198 {
199 QString completionPrefix = textUnderCursor();
200
201 if (completionPrefix.isEmpty()) {
202 if (cc->popup()->isVisible())
203 cc->popup()->hide();
204
205 return;
206 }
207
208 if (!cc->popup()->isVisible() || completionPrefix.length() < cc->completionPrefix().length()) {
209 QString pattern = QString("(\\[.*\\])?%1.*").arg( QRegExp::escape(completionPrefix) );
210 QStringList nicks = cc_model->findItems(pattern, Qt::MatchRegExp, 0);
211
212 if (nicks.isEmpty())
213 return;
214
215 if (nicks.count() == 1) {
216 insertToPos(nicks.last(), textCursor().position() - completionPrefix.length());
217 return;
218 }
219
220 NickCompletionModel *tmpModel = new NickCompletionModel(nicks, cc);
221 cc->setModel(tmpModel);
222 }
223
224 if (completionPrefix != cc->completionPrefix()) {
225 cc->setCompletionPrefix(completionPrefix);
226 cc->popup()->setCurrentIndex(cc->completionModel()->index(0, 0));
227 }
228
229 QRect cr = cursorRect();
230 cr.setWidth(cc->popup()->sizeHintForColumn(0)
231 + cc->popup()->verticalScrollBar()->sizeHint().width());
232
233 cc->complete(cr);
234 }
235
dragMoveEvent(QDragMoveEvent * event)236 void ChatEdit::dragMoveEvent(QDragMoveEvent *event) {
237 event->accept();
238 }
239
dragEnterEvent(QDragEnterEvent * e)240 void ChatEdit::dragEnterEvent(QDragEnterEvent *e)
241 {
242 if (e->mimeData()->hasUrls() || e->mimeData()->hasText()) {
243 e->acceptProposedAction();
244 } else {
245 e->ignore();
246 }
247 }
248
dropEvent(QDropEvent * e)249 void ChatEdit::dropEvent(QDropEvent *e)
250 {
251 if (e->mimeData()->hasUrls()) {
252
253 e->setDropAction(Qt::IgnoreAction);
254
255 QStringList fileNames;
256 for (const auto url : e->mimeData()->urls()) {
257 QString urlStr = url.toString();
258 if (url.scheme().toLower() == "file") {
259 QFileInfo fi( url.toLocalFile() );
260 QString str = QDir::toNativeSeparators( fi.absoluteFilePath() );
261
262 if ( fi.exists() && fi.isFile() && !str.isEmpty() ) {
263 const TTHValue *tth = HashManager::getInstance()->getFileTTHif(str.toStdString());
264 if ( !tth ) {
265 str = QDir::toNativeSeparators( fi.canonicalFilePath() ); // try to follow symlinks
266 tth = HashManager::getInstance()->getFileTTHif(str.toStdString());
267 }
268 if (tth)
269 urlStr = WulforUtil::getInstance()->makeMagnet(fi.fileName(), fi.size(), _q(tth->toBase32()));
270 }
271 };
272
273 if (!urlStr.isEmpty())
274 fileNames << urlStr;
275 }
276
277 if (!fileNames.isEmpty()) {
278
279 QString dropText = (fileNames.count() == 1) ? fileNames.last() : "\n" + fileNames.join("\n");
280
281 QMimeData mime;
282 mime.setText(dropText);
283 QDropEvent drop(e->pos(), Qt::CopyAction, &mime, e->mouseButtons(),
284 e->keyboardModifiers(), e->type());
285
286 QTextEdit::dropEvent(&drop);
287 return;
288 }
289 }
290 QTextEdit::dropEvent(e);
291 }
292
updateScrollBar()293 void ChatEdit::updateScrollBar(){
294 setVerticalScrollBarPolicy(sizeHint().height() > height() ? Qt::ScrollBarAlwaysOn : Qt::ScrollBarAlwaysOff);
295 ensureCursorVisible();
296 }
297