6a9b14369eb33d520e51a39a1b4d4a7ffaa9f85a
[quassel.git] / src / qtui / chatitem.cpp
1 /***************************************************************************
2  *   Copyright (C) 2005-08 by the Quassel Project                          *
3  *   devel@quassel-irc.org                                                 *
4  *                                                                         *
5  *   This program is free software; you can redistribute it and/or modify  *
6  *   it under the terms of the GNU General Public License as published by  *
7  *   the Free Software Foundation; either version 2 of the License, or     *
8  *   (at your option) version 3.                                           *
9  *                                                                         *
10  *   This program is distributed in the hope that it will be useful,       *
11  *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
12  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
13  *   GNU General Public License for more details.                          *
14  *                                                                         *
15  *   You should have received a copy of the GNU General Public License     *
16  *   along with this program; if not, write to the                         *
17  *   Free Software Foundation, Inc.,                                       *
18  *   59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.             *
19  ***************************************************************************/
20
21 #include <QApplication>
22 #include <QClipboard>
23 #include <QDesktopServices>
24 #include <QFontMetrics>
25 #include <QGraphicsSceneMouseEvent>
26 #include <QPainter>
27 #include <QPalette>
28 #include <QTextLayout>
29
30 #include "chatitem.h"
31 #include "chatlinemodel.h"
32 #include "qtui.h"
33
34 ChatItem::ChatItem(ChatLineModel::ColumnType col, QAbstractItemModel *model, QGraphicsItem *parent)
35   : QGraphicsItem(parent),
36     _fontMetrics(0),
37     _layoutData(0),
38     _selectionMode(NoSelection),
39     _selectionStart(-1)
40 {
41   Q_ASSERT(model);
42   QModelIndex index = model->index(row(), col);
43   _fontMetrics = QtUi::style()->fontMetrics(model->data(index, ChatLineModel::FormatRole).value<UiStyle::FormatList>().at(0).second);
44   setAcceptHoverEvents(true);
45   setZValue(20);
46 }
47
48 ChatItem::~ChatItem() {
49   delete _layoutData;
50 }
51
52 QVariant ChatItem::data(int role) const {
53   QModelIndex index = model()->index(row(), column());
54   if(!index.isValid()) {
55     qWarning() << "ChatItem::data(): model index is invalid!" << index;
56     return QVariant();
57   }
58   return model()->data(index, role);
59 }
60
61 qreal ChatItem::setGeometry(qreal w, qreal h) {
62   if(w == _boundingRect.width()) return _boundingRect.height();
63   prepareGeometryChange();
64   _boundingRect.setWidth(w);
65   if(h < 0) h = computeHeight();
66   _boundingRect.setHeight(h);
67   if(haveLayout()) updateLayout();
68   return h;
69 }
70
71 qreal ChatItem::computeHeight() {
72   return fontMetrics()->lineSpacing(); // only contents can be multi-line
73 }
74
75 QTextLayout *ChatItem::createLayout(QTextOption::WrapMode wrapMode, Qt::Alignment alignment) {
76   QTextLayout *layout = new QTextLayout(data(MessageModel::DisplayRole).toString());
77
78   QTextOption option;
79   option.setWrapMode(wrapMode);
80   option.setAlignment(alignment);
81   layout->setTextOption(option);
82
83   QList<QTextLayout::FormatRange> formatRanges
84          = QtUi::style()->toTextLayoutList(data(MessageModel::FormatRole).value<UiStyle::FormatList>(), layout->text().length());
85   layout->setAdditionalFormats(formatRanges);
86   return layout;
87 }
88
89 void ChatItem::setLayout(QTextLayout *layout) {
90   if(!_layoutData)
91     _layoutData = new LayoutData;
92   _layoutData->layout = layout;
93 }
94
95 void ChatItem::updateLayout() {
96   if(!haveLayout())
97     setLayout(createLayout(QTextOption::WrapAnywhere, Qt::AlignLeft));
98
99   layout()->beginLayout();
100   QTextLine line = layout()->createLine();
101   if(line.isValid()) {
102     line.setLineWidth(width());
103     line.setPosition(QPointF(0,0));
104   }
105   layout()->endLayout();
106 }
107
108 void ChatItem::clearLayoutData() {
109   delete _layoutData;
110   _layoutData = 0;
111 }
112
113 // NOTE: This is not the most time-efficient implementation, but it saves space by not caching unnecessary data
114 //       This is a deliberate trade-off. (-> selectFmt creation, data() call)
115 void ChatItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) {
116   Q_UNUSED(option); Q_UNUSED(widget);
117   if(!haveLayout()) updateLayout();
118   painter->setClipRect(boundingRect()); // no idea why QGraphicsItem clipping won't work
119   //if(_selectionMode == FullSelection) {
120     //painter->save();
121     //painter->fillRect(boundingRect(), QApplication::palette().brush(QPalette::Highlight));
122     //painter->restore();
123   //}
124   QVector<QTextLayout::FormatRange> formats;
125   if(_selectionMode != NoSelection) {
126     QTextLayout::FormatRange selectFmt;
127     selectFmt.format.setForeground(QApplication::palette().brush(QPalette::HighlightedText));
128     selectFmt.format.setBackground(QApplication::palette().brush(QPalette::Highlight));
129     if(_selectionMode == PartialSelection) {
130       selectFmt.start = qMin(_selectionStart, _selectionEnd);
131       selectFmt.length = qAbs(_selectionStart - _selectionEnd);
132     } else { // FullSelection
133       selectFmt.start = 0;
134       selectFmt.length = data(MessageModel::DisplayRole).toString().length();
135     }
136     formats.append(selectFmt);
137   }
138   layout()->draw(painter, QPointF(0,0), formats, boundingRect());
139 }
140
141 qint16 ChatItem::posToCursor(const QPointF &pos) {
142   if(pos.y() > height()) return data(MessageModel::DisplayRole).toString().length();
143   if(pos.y() < 0) return 0;
144   if(!haveLayout()) updateLayout();
145   for(int l = layout()->lineCount() - 1; l >= 0; l--) {
146     QTextLine line = layout()->lineAt(l);
147     if(pos.y() >= line.y()) {
148       return line.xToCursor(pos.x(), QTextLine::CursorOnCharacter);
149     }
150   }
151   return 0;
152 }
153
154 void ChatItem::setFullSelection() {
155   if(_selectionMode != FullSelection) {
156     _selectionMode = FullSelection;
157     update();
158   }
159 }
160
161 void ChatItem::clearSelection() {
162   if(_selectionMode != NoSelection) {
163     _selectionMode = NoSelection;
164     update();
165   }
166 }
167
168 void ChatItem::continueSelecting(const QPointF &pos) {
169   _selectionMode = PartialSelection;
170   _selectionEnd = posToCursor(pos);
171   update();
172 }
173
174 QList<QRectF> ChatItem::findWords(const QString &searchWord, Qt::CaseSensitivity caseSensitive) {
175   QList<QRectF> resultList;
176   const QAbstractItemModel *model_ = model();
177   if(!model_)
178     return resultList;
179
180   QString plainText = model_->data(model_->index(row(), column()), MessageModel::DisplayRole).toString();
181   QList<int> indexList;
182   int searchIdx = plainText.indexOf(searchWord, 0, caseSensitive);
183   while(searchIdx != -1) {
184     indexList << searchIdx;
185     searchIdx = plainText.indexOf(searchWord, searchIdx + 1, caseSensitive);
186   }
187
188   if(!haveLayout())
189     updateLayout();
190
191   foreach(int idx, indexList) {
192     QTextLine line = layout()->lineForTextPosition(idx);
193     qreal x = line.cursorToX(idx);
194     qreal width = line.cursorToX(idx + searchWord.count()) - x;
195     qreal height = fontMetrics()->lineSpacing();
196     qreal y = height * line.lineNumber();
197     resultList << QRectF(x, y, width, height);
198   }
199   return resultList;
200 }
201
202
203 void ChatItem::mousePressEvent(QGraphicsSceneMouseEvent *event) {
204   if(event->buttons() == Qt::LeftButton) {
205     if(_selectionMode == NoSelection) {
206       chatScene()->setSelectingItem(this);  // removes earlier selection if exists
207       _selectionStart = _selectionEnd = posToCursor(event->pos());
208       //_selectionMode = PartialSelection;
209     } else {
210       chatScene()->setSelectingItem(0);
211       _selectionMode = NoSelection;
212       update();
213     }
214     event->accept();
215   } else {
216     event->ignore();
217   }
218 }
219
220 void ChatItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event) {
221   if(event->buttons() == Qt::LeftButton) {
222     if(contains(event->pos())) {
223       qint16 end = posToCursor(event->pos());
224       if(end != _selectionEnd) {
225         _selectionEnd = end;
226         if(_selectionStart != _selectionEnd) _selectionMode = PartialSelection;
227         else _selectionMode = NoSelection;
228         update();
229       }
230     } else {
231       setFullSelection();
232       chatScene()->startGlobalSelection(this, event->pos());
233     }
234     event->accept();
235   } else {
236     event->ignore();
237   }
238 }
239
240 void ChatItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) {
241   if(_selectionMode != NoSelection && !event->buttons() & Qt::LeftButton) {
242     _selectionEnd = posToCursor(event->pos());
243     QString selection
244         = data(MessageModel::DisplayRole).toString().mid(qMin(_selectionStart, _selectionEnd), qAbs(_selectionStart - _selectionEnd));
245     chatScene()->putToClipboard(selection);
246     event->accept();
247   } else {
248     event->ignore();
249   }
250 }
251
252 void ChatItem::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) {
253   // FIXME dirty and fast hack to make http:// urls klickable
254
255   QRegExp regex("\\b([hf]t{1,2}ps?://[^\\s]+)\\b");
256   QString str = data(ChatLineModel::DisplayRole).toString();
257   int idx = posToCursor(event->pos());
258   int mi = 0;
259   do {
260     mi = regex.indexIn(str, mi);
261     if(mi < 0) break;
262     if(idx >= mi && idx < mi + regex.matchedLength()) {
263       QDesktopServices::openUrl(QUrl(regex.capturedTexts()[1]));
264       break;
265     }
266     mi += regex.matchedLength();
267   } while(mi >= 0);
268   event->accept();
269 }
270
271 void ChatItem::hoverEnterEvent(QGraphicsSceneHoverEvent *event) {
272   //qDebug() << (void*)this << "entering";
273   event->ignore();
274 }
275
276 void ChatItem::hoverLeaveEvent(QGraphicsSceneHoverEvent *event) {
277   //qDebug() << (void*)this << "leaving";
278   event->ignore();
279 }
280
281 void ChatItem::hoverMoveEvent(QGraphicsSceneHoverEvent *event) {
282   //qDebug() << (void*)this << event->pos();
283   event->ignore();
284 }
285
286 /*************************************************************************************************/
287
288 /*************************************************************************************************/
289
290 void SenderChatItem::updateLayout() {
291   if(!haveLayout()) setLayout(createLayout(QTextOption::WrapAnywhere, Qt::AlignRight));
292   ChatItem::updateLayout();
293 }
294
295 /*************************************************************************************************/
296
297 qreal ContentsChatItem::computeHeight() {
298   int lines = 1;
299   WrapColumnFinder finder(this);
300   while(finder.nextWrapColumn() > 0) lines++;
301   return lines * fontMetrics()->lineSpacing();
302 }
303
304 void ContentsChatItem::updateLayout() {
305   if(!haveLayout()) setLayout(createLayout(QTextOption::WrapAnywhere));
306
307   // Now layout
308   ChatLineModel::WrapList wrapList = data(ChatLineModel::WrapListRole).value<ChatLineModel::WrapList>();
309   if(!wrapList.count()) return; // empty chatitem
310
311   qreal h = 0;
312   WrapColumnFinder finder(this);
313   layout()->beginLayout();
314   forever {
315     QTextLine line = layout()->createLine();
316     if(!line.isValid())
317       break;
318
319     int col = finder.nextWrapColumn();
320     line.setNumColumns(col >= 0 ? col - line.textStart() : layout()->text().length());
321     line.setPosition(QPointF(0, h));
322     h += line.height() + fontMetrics()->leading();
323   }
324   layout()->endLayout();
325 }
326
327
328 /*************************************************************************************************/
329
330 ContentsChatItem::WrapColumnFinder::WrapColumnFinder(ChatItem *_item) : item(_item) {
331   wrapList = item->data(ChatLineModel::WrapListRole).value<ChatLineModel::WrapList>();
332   wordidx = 0;
333   layout = 0;
334   lastwrapcol = 0;
335   lastwrappos = 0;
336   w = 0;
337 }
338
339 ContentsChatItem::WrapColumnFinder::~WrapColumnFinder() {
340   delete layout;
341 }
342
343 qint16 ContentsChatItem::WrapColumnFinder::nextWrapColumn() {
344   while(wordidx < wrapList.count()) {
345     w += wrapList.at(wordidx).width;
346     if(w >= item->width()) {
347       if(lastwrapcol >= wrapList.at(wordidx).start) {
348         // first word, and it doesn't fit
349         if(!line.isValid()) {
350           layout = item->createLayout(QTextOption::NoWrap);
351           layout->beginLayout();
352           line = layout->createLine();
353           line.setLineWidth(item->width());
354           layout->endLayout();
355         }
356         int idx = line.xToCursor(lastwrappos + item->width(), QTextLine::CursorOnCharacter);
357         qreal x = line.cursorToX(idx, QTextLine::Trailing);
358         w = w - wrapList.at(wordidx).width - (x - lastwrappos);
359         lastwrappos = x;
360         lastwrapcol = idx;
361         return idx;
362       }
363       // not the first word, so just wrap before this
364       lastwrapcol = wrapList.at(wordidx).start;
365       lastwrappos = lastwrappos + w - wrapList.at(wordidx).width;
366       w = 0;
367       return lastwrapcol;
368     }
369     w += wrapList.at(wordidx).trailing;
370     wordidx++;
371   }
372   return -1;
373 }