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