7d52c5a57dfafc96ca6984927f0de74dbaf71d3c
[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 <QFontMetrics>
24 #include <QGraphicsSceneMouseEvent>
25 #include <QPainter>
26 #include <QPalette>
27 #include <QTextLayout>
28
29 #include "chatitem.h"
30 #include "chatlinemodel.h"
31 #include "qtui.h"
32
33 ChatItem::ChatItem(const QPersistentModelIndex &index_, QGraphicsItem *parent) : QGraphicsItem(parent), _index(index_) {
34   _fontMetrics = QtUi::style()->fontMetrics(data(ChatLineModel::FormatRole).value<UiStyle::FormatList>().at(0).second);
35   _layout = 0;
36   _lines = 0;
37   _selectionStart = -1;
38 }
39
40 ChatItem::~ChatItem() {
41   delete _layout;
42 }
43
44 QVariant ChatItem::data(int role) const {
45   if(!_index.isValid()) {
46     qWarning() << "ChatItem::data(): Model index is invalid!" << _index;
47     return QVariant();
48   }
49   return _index.data(role);
50 }
51
52 int ChatItem::setWidth(int w) {
53   if(w == _boundingRect.width()) return _boundingRect.height();
54   prepareGeometryChange();
55   _boundingRect.setWidth(w);
56   int h = heightForWidth(w);
57   _boundingRect.setHeight(h);
58   if(haveLayout()) updateLayout();
59   return h;
60 }
61
62 int ChatItem::heightForWidth(int width) {
63   if(data(ChatLineModel::ColumnTypeRole).toUInt() != ChatLineModel::ContentsColumn)
64     return fontMetrics()->lineSpacing(); // only contents can be multi-line
65
66   _lines = 1;
67   WrapColumnFinder finder(this);
68   while(finder.nextWrapColumn() > 0) _lines++;
69   return _lines * fontMetrics()->lineSpacing();
70 }
71
72 QTextLayout *ChatItem::createLayout(QTextOption::WrapMode wrapMode, Qt::Alignment alignment) {
73   QTextLayout *layout = new QTextLayout(data(MessageModel::DisplayRole).toString());
74
75   QTextOption option;
76   option.setWrapMode(wrapMode);
77   option.setAlignment(alignment);
78   layout->setTextOption(option);
79
80   QList<QTextLayout::FormatRange> formatRanges
81          = QtUi::style()->toTextLayoutList(data(MessageModel::FormatRole).value<UiStyle::FormatList>(), layout->text().length());
82   layout->setAdditionalFormats(formatRanges);
83   return layout;
84 }
85
86 void ChatItem::updateLayout() {
87   switch(data(ChatLineModel::ColumnTypeRole).toUInt()) {
88     case ChatLineModel::TimestampColumn:
89       if(!haveLayout()) _layout = createLayout(QTextOption::WrapAnywhere, Qt::AlignLeft);
90       // fallthrough
91     case ChatLineModel::SenderColumn:
92       if(!haveLayout()) _layout = createLayout(QTextOption::WrapAnywhere, Qt::AlignRight);
93       _layout->beginLayout();
94       {
95         QTextLine line = _layout->createLine();
96         if(line.isValid()) {
97           line.setLineWidth(width());
98           line.setPosition(QPointF(0, fontMetrics()->leading()));
99         }
100         _layout->endLayout();
101       }
102       break;
103     case ChatLineModel::ContentsColumn: {
104       if(!haveLayout()) _layout = createLayout(QTextOption::WrapAnywhere);
105
106       // Now layout
107       ChatLineModel::WrapList wrapList = data(ChatLineModel::WrapListRole).value<ChatLineModel::WrapList>();
108       if(!wrapList.count()) return; // empty chatitem
109       int wordidx = 0;
110       ChatLineModel::Word word = wrapList.at(0);
111
112       qreal h = 0;
113       WrapColumnFinder finder(this);
114       _layout->beginLayout();
115       forever {
116         QTextLine line = _layout->createLine();
117         if (!line.isValid())
118           break;
119
120         int col = finder.nextWrapColumn();
121         line.setNumColumns(col >= 0 ? col - line.textStart() : _layout->text().length());
122
123         h += fontMetrics()->leading();
124         line.setPosition(QPointF(0, h));
125         h += line.height();
126       }
127       _layout->endLayout();
128     }
129     break;
130   }
131 }
132
133 void ChatItem::clearLayout() {
134   delete _layout;
135   _layout = 0;
136 }
137
138 void ChatItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) {
139   Q_UNUSED(option); Q_UNUSED(widget);
140   if(!haveLayout()) updateLayout();
141   _layout->draw(painter, QPointF(0,0), QVector<QTextLayout::FormatRange>(), boundingRect());
142 }
143
144 int ChatItem::posToCursor(const QPointF &pos) {
145   if(pos.y() > height()) return data(MessageModel::DisplayRole).toString().length();
146   if(pos.y() < 0) return 0;
147   if(!haveLayout()) updateLayout();
148   for(int l = _layout->lineCount() - 1; l >= 0; l--) {
149     QTextLine line = _layout->lineAt(l);
150     if(pos.y() >= line.y()) {
151       return line.xToCursor(pos.x(), QTextLine::CursorOnCharacter);
152     }
153   }
154   return 0;
155 }
156
157 void ChatItem::clearSelection() {
158   if(_selectionStart >= 0) {
159     QList<QTextLayout::FormatRange> formats = _layout->additionalFormats();
160     formats.removeLast();
161     _layout->setAdditionalFormats(formats);
162     _selectionStart = -1;
163     updateLayout();
164     update();
165   }
166 }
167
168 void ChatItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event) {
169   int selectionEnd = posToCursor(event->pos());
170   QList<QTextLayout::FormatRange> formats = _layout->additionalFormats();
171   formats.last().start = qMin(_selectionStart, selectionEnd);
172   formats.last().length = qMax(_selectionStart, selectionEnd) - formats.last().start;
173   _layout->setAdditionalFormats(formats);
174   updateLayout();
175   update();
176 }
177
178 void ChatItem::mousePressEvent(QGraphicsSceneMouseEvent *event) {
179   if(event->buttons() & Qt::LeftButton) {
180     if(!haveLayout()) updateLayout();
181     chatScene()->setSelectingItem(this);
182     _selectionStart = posToCursor(event->pos());
183     QList<QTextLayout::FormatRange> formats = _layout->additionalFormats();
184     QTextLayout::FormatRange selectFmt;
185     QPalette pal = QApplication::palette();
186     selectFmt.format.setForeground(pal.brush(QPalette::HighlightedText));
187     selectFmt.format.setBackground(pal.brush(QPalette::Highlight));
188     selectFmt.length = 0;
189     formats.append(selectFmt);
190     _layout->setAdditionalFormats(formats);
191     updateLayout();
192     update();
193     event->accept();
194   } else {
195     event->ignore();
196   }
197 }
198
199 void ChatItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) {
200   if(_selectionStart >= 0) {
201     int selectionEnd = posToCursor(event->pos());
202     QString selection
203         = data(MessageModel::DisplayRole).toString().mid(qMin(_selectionStart, selectionEnd), qAbs(_selectionStart - selectionEnd));
204     QApplication::clipboard()->setText(selection, QClipboard::Clipboard);  // TODO configure where selections should go
205     event->accept();
206   } else {
207     event->ignore();
208   }
209 }
210
211 /*************************************************************************************************/
212
213 ChatItem::WrapColumnFinder::WrapColumnFinder(ChatItem *_item) : item(_item) {
214   wrapList = item->data(ChatLineModel::WrapListRole).value<ChatLineModel::WrapList>();
215   wordidx = 0;
216   layout = 0;
217   lastwrapcol = 0;
218   lastwrappos = 0;
219   w = 0;
220 }
221
222 ChatItem::WrapColumnFinder::~WrapColumnFinder() {
223   delete layout;
224 }
225
226 int ChatItem::WrapColumnFinder::nextWrapColumn() {
227   while(wordidx < wrapList.count()) {
228     w += wrapList.at(wordidx).width;
229     if(w >= item->width()) {
230       if(lastwrapcol >= wrapList.at(wordidx).start) {
231         // first word, and it doesn't fit
232         if(!line.isValid()) {
233           layout = item->createLayout(QTextOption::NoWrap);
234           layout->beginLayout();
235           line = layout->createLine();
236           line.setLineWidth(item->width());
237           layout->endLayout();
238         }
239         int idx = line.xToCursor(lastwrappos + item->width(), QTextLine::CursorOnCharacter);
240         qreal x = line.cursorToX(idx, QTextLine::Trailing);
241         w = w - wrapList.at(wordidx).width - (x - lastwrappos);
242         lastwrappos = x;
243         lastwrapcol = idx;
244         return idx;
245       }
246       // not the first word, so just wrap before this
247       lastwrapcol = wrapList.at(wordidx).start;
248       lastwrappos = lastwrappos + w - wrapList.at(wordidx).width;
249       w = 0;
250       return lastwrapcol;
251     }
252     w += wrapList.at(wordidx).trailing;
253     wordidx++;
254   }
255   return -1;
256 }