Ok this is the major rework of quassel we've all been waiting for. For the actual...
[quassel.git] / src / qtgui / chatline.cpp
1 /***************************************************************************
2  *   Copyright (C) 2005-07 by The Quassel IRC Development Team             *
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) any later version.                                   *
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 "chatline.h"
22
23 //!\brief Construct a ChatLine object from a message.
24 /**
25  * \param m   The message to be layouted and rendered
26  * \param net The network name
27  * \param buf The buffer name
28  */
29 ChatLine::ChatLine(Message m) {
30   hght = 0;
31   //networkName = m.buffer.network();
32   //bufferName = m.buffer.buffer();
33   msg = m;
34   selectionMode = None;
35   formatMsg(msg);
36 }
37
38 ChatLine::~ChatLine() {
39
40 }
41
42 void ChatLine::formatMsg(Message msg) {
43   QTextOption tsOption, senderOption, textOption;
44   styledTimeStamp = Style::formattedToStyled(msg.formattedTimeStamp());
45   styledSender = Style::formattedToStyled(msg.formattedSender());
46   styledText = Style::formattedToStyled(msg.formattedText());
47   precomputeLine();
48 }
49
50 QList<ChatLine::FormatRange> ChatLine::calcFormatRanges(const Style::StyledString &fs, QTextLayout::FormatRange additional) {
51   QList<FormatRange> ranges;
52   QList<QTextLayout::FormatRange> formats = fs.formats;
53   formats.append(additional);
54   int cur = -1;
55   FormatRange range, lastrange;
56   for(int i = 0; i < fs.text.length(); i++) {
57     QTextCharFormat format;
58     foreach(QTextLayout::FormatRange f, formats) {
59       if(i >= f.start && i < f.start + f.length) format.merge(f.format);
60     }
61     if(cur < 0) {
62       range.start = 0; range.length = 1; range.format= format;
63       cur = 0;
64     } else {
65       if(format == range.format) range.length++;
66       else {
67         QFontMetrics metrics(range.format.font());
68         range.height = metrics.lineSpacing();
69         ranges.append(range);
70         range.start = i; range.length = 1; range.format = format;
71         cur++;
72       }
73     }
74   }
75   if(cur >= 0) {
76     QFontMetrics metrics(range.format.font());
77     range.height = metrics.lineSpacing();
78     ranges.append(range);
79   }
80   return ranges;
81 }
82
83 void ChatLine::setSelection(SelectionMode mode, int start, int end) {
84   selectionMode = mode;
85   //tsFormat.clear(); senderFormat.clear(); textFormat.clear();
86   QPalette pal = QApplication::palette();
87   QTextLayout::FormatRange tsSel, senderSel, textSel;
88   switch (mode) {
89     case None:
90       tsFormat = calcFormatRanges(styledTimeStamp);
91       senderFormat = calcFormatRanges(styledSender);
92       textFormat = calcFormatRanges(styledText);
93       break;
94     case Partial:
95       selectionStart = qMin(start, end); selectionEnd = qMax(start, end);
96       textSel.format.setForeground(pal.brush(QPalette::HighlightedText));
97       textSel.format.setBackground(pal.brush(QPalette::Highlight));
98       textSel.start = selectionStart;
99       textSel.length = selectionEnd - selectionStart;
100       //textFormat.append(textSel);
101       textFormat = calcFormatRanges(styledText, textSel);
102       foreach(FormatRange fr, textFormat);
103       break;
104     case Full:
105       tsSel.format.setForeground(pal.brush(QPalette::HighlightedText));
106       tsSel.format.setBackground(pal.brush(QPalette::Highlight));
107       tsSel.start = 0; tsSel.length = styledTimeStamp.text.length();
108       tsFormat = calcFormatRanges(styledTimeStamp, tsSel);
109       senderSel.format.setForeground(pal.brush(QPalette::HighlightedText));
110       senderSel.format.setBackground(pal.brush(QPalette::Highlight));
111       senderSel.start = 0; senderSel.length = styledSender.text.length();
112       senderFormat = calcFormatRanges(styledSender, senderSel);
113       textSel.format.setForeground(pal.brush(QPalette::HighlightedText));
114       textSel.format.setBackground(pal.brush(QPalette::Highlight));
115       textSel.start = 0; textSel.length = styledText.text.length();
116       textFormat = calcFormatRanges(styledText, textSel);
117       break;
118   }
119 }
120
121 uint ChatLine::msgId() const {
122   return msg.buffer().uid();
123 }
124
125 BufferInfo ChatLine::bufferInfo() const {
126   return msg.buffer();
127 }
128
129 QDateTime ChatLine::timeStamp() const {
130   return msg.timeStamp();
131 }
132
133 QString ChatLine::sender() const {
134   return styledSender.text;
135 }
136
137 QString ChatLine::text() const {
138   return styledText.text;
139 }
140
141 bool ChatLine::isUrl(int c) const {
142   if(c < 0 || c >= charUrlIdx.count()) return false;;
143   return charUrlIdx[c] >= 0;
144 }
145
146 QUrl ChatLine::getUrl(int c) const {
147   if(c < 0 || c >= charUrlIdx.count()) return QUrl();
148   int i = charUrlIdx[c];
149   if(i >= 0) return styledText.urls[i].url;
150   else return QUrl();
151 }
152
153 //!\brief Return the cursor position for the given coordinate pos.
154 /**
155  * \param pos The position relative to the ChatLine
156  * \return The cursor position, [or -3 for invalid,] or -2 for timestamp, or -1 for sender
157  */
158 int ChatLine::posToCursor(QPointF pos) {
159   if(pos.x() < tsWidth + (int)Style::sepTsSender()/2) return -2;
160   qreal textStart = tsWidth + Style::sepTsSender() + senderWidth + Style::sepSenderText();
161   if(pos.x() < textStart) return -1;
162   int x = (int)(pos.x() - textStart);
163   for(int l = lineLayouts.count() - 1; l >=0; l--) {
164     LineLayout line = lineLayouts[l];
165     if(pos.y() >= line.y) {
166       int offset = charPos[line.start]; x += offset;
167       for(int i = line.start + line.length - 1; i >= line.start; i--) {
168         if((charPos[i] + charPos[i+1])/2 <= x) return i+1; // FIXME: Optimize this!
169       }
170       return line.start;
171     }
172   }
173   return 0;
174 }
175
176 void ChatLine::precomputeLine() {
177   tsFormat = calcFormatRanges(styledTimeStamp);
178   senderFormat = calcFormatRanges(styledSender);
179   textFormat = calcFormatRanges(styledText);
180
181   minHeight = 0;
182   foreach(FormatRange fr, tsFormat) minHeight = qMax(minHeight, fr.height);
183   foreach(FormatRange fr, senderFormat) minHeight = qMax(minHeight, fr.height);
184
185   words.clear();
186   charPos.resize(styledText.text.length() + 1);
187   charHeights.resize(styledText.text.length());
188   charUrlIdx.fill(-1, styledText.text.length());
189   for(int i = 0; i < styledText.urls.count(); i++) {
190     Style::UrlInfo url = styledText.urls[i];
191     for(int j = url.start; j < url.end; j++) charUrlIdx[j] = i;
192   }
193   if(!textFormat.count()) return;
194   int idx = 0; int cnt = 0; int w = 0;
195   QFontMetrics metrics(textFormat[0].format.font());
196   Word wr;
197   wr.start = -1; wr.trailing = -1;
198   for(int i = 0; i < styledText.text.length(); ) {
199     charPos[i] = w; charHeights[i] = textFormat[idx].height;
200     w += metrics.charWidth(styledText.text, i);
201     if(!styledText.text[i].isSpace()) {
202       if(wr.trailing >= 0) {
203         // new word after space
204         words.append(wr);
205         wr.start = -1;
206       }
207       if(wr.start < 0) {
208         wr.start = i; wr.length = 1; wr.trailing = -1; wr.height = textFormat[idx].height;
209       } else {
210         wr.length++; wr.height = qMax(wr.height, textFormat[idx].height);
211       }
212     } else {
213       if(wr.start < 0) {
214         wr.start = i; wr.length = 0; wr.trailing = 1; wr.height = 0;
215       } else {
216         wr.trailing++;
217       }
218     }
219     if(++i < styledText.text.length() && ++cnt >= textFormat[idx].length) {
220       cnt = 0; idx++;
221       Q_ASSERT(idx < textFormat.count());
222       metrics = QFontMetrics(textFormat[idx].format.font());
223     }
224   }
225   charPos[styledText.text.length()] = w;
226   if(wr.start >= 0) words.append(wr);
227 }
228
229 qreal ChatLine::layout(qreal tsw, qreal senderw, qreal textw) {
230   tsWidth = tsw; senderWidth = senderw; textWidth = textw;
231   if(textw <= 0) return minHeight;
232   lineLayouts.clear(); LineLayout line;
233   int h = 0;
234   int offset = 0; int numWords = 0;
235   line.y = 0;
236   line.start = 0;
237   line.height = minHeight;  // first line needs room for ts and sender
238   for(uint i = 0; i < (uint)words.count(); i++) {
239     int lastpos = charPos[words[i].start + words[i].length]; // We use charPos[lastchar + 1], 'coz last char needs to fit
240     if(lastpos - offset <= textw) {
241       line.height = qMax(line.height, words[i].height);
242       line.length = words[i].start + words[i].length - line.start;
243       numWords++;
244     } else {
245       // we need to wrap!
246       if(numWords > 0) {
247         // ok, we had some words before, so store the layout and start a new line
248         h += line.height;
249         line.length = words[i-1].start + words[i-1].length - line.start;
250         lineLayouts.append(line);
251         line.y += line.height;
252         line.start = words[i].start;
253         line.height = words[i].height;
254         offset = charPos[words[i].start];
255       }
256       numWords = 1;
257       // check if the word fits into the current line
258       if(lastpos - offset <= textw) {
259         line.length = words[i].length;
260       } else {
261         // we need to break a word in the middle
262         int border = (int)textw + offset; // save some additions
263         line.start = words[i].start;
264         line.length = 1;
265         line.height = charHeights[line.start];
266         int j = line.start + 1;
267         for(int l = 1; l < words[i].length; j++, l++) {
268           if(charPos[j+1] < border) {
269             line.length++;
270             line.height = qMax(line.height, charHeights[j]);
271             continue;
272           } else {
273             h += line.height;
274             lineLayouts.append(line);
275             line.y += line.height;
276             line.start = j;
277             line.height = charHeights[j];
278             line.length = 1;
279             offset = charPos[j];
280             border = (int)textw + offset;
281           }
282         }
283       }
284     }
285   }
286   h += line.height;
287   if(numWords > 0) {
288     lineLayouts.append(line);
289   }
290   hght = h;
291   return hght;
292 }
293
294 //!\brief Draw ChatLine on the given QPainter at the given position.
295 void ChatLine::draw(QPainter *p, const QPointF &pos) {
296   QPalette pal = QApplication::palette();
297
298   if(selectionMode == Full) {
299     p->setPen(Qt::NoPen);
300     p->setBrush(pal.brush(QPalette::Highlight));
301     p->drawRect(QRectF(pos, QSizeF(tsWidth + Style::sepTsSender() + senderWidth + Style::sepSenderText() + textWidth, height())));
302   } else if(selectionMode == Partial) {
303
304   } /*
305   p->setClipRect(QRectF(pos, QSizeF(tsWidth, height())));
306   tsLayout.draw(p, pos, tsFormat);
307   p->setClipRect(QRectF(pos + QPointF(tsWidth + Style::sepTsSender(), 0), QSizeF(senderWidth, height())));
308   senderLayout.draw(p, pos + QPointF(tsWidth + Style::sepTsSender(), 0), senderFormat);
309   p->setClipping(false);
310   textLayout.draw(p, pos + QPointF(tsWidth + Style::sepTsSender() + senderWidth + Style::sepSenderText(), 0), textFormat);
311     */
312   //p->setClipRect(QRectF(pos, QSizeF(tsWidth, 15)));
313   //p->drawRect(QRectF(pos, QSizeF(tsWidth, minHeight)));
314   p->setBackgroundMode(Qt::OpaqueMode);
315   QPointF tp = pos;
316   QRectF rect(pos, QSizeF(tsWidth, minHeight));
317   QRectF brect;
318   foreach(FormatRange fr, tsFormat) {
319     p->setFont(fr.format.font());
320     p->setPen(QPen(fr.format.foreground(), 0)); p->setBackground(fr.format.background());
321     p->drawText(rect, Qt::AlignLeft|Qt::TextSingleLine, styledTimeStamp.text.mid(fr.start, fr.length), &brect);
322     rect.setLeft(brect.right());
323   }
324   rect = QRectF(pos + QPointF(tsWidth + Style::sepTsSender(), 0), QSizeF(senderWidth, minHeight));
325   for(int i = senderFormat.count() - 1; i >= 0; i--) {
326     FormatRange fr = senderFormat[i];
327     p->setFont(fr.format.font()); p->setPen(QPen(fr.format.foreground(), 0)); p->setBackground(fr.format.background());
328     p->drawText(rect, Qt::AlignRight|Qt::TextSingleLine, styledSender.text.mid(fr.start, fr.length), &brect);
329     rect.setRight(brect.left());
330   }
331   QPointF tpos = pos + QPointF(tsWidth + Style::sepTsSender() + senderWidth + Style::sepSenderText(), 0);
332   qreal h = 0; int l = 0;
333   rect = QRectF(tpos + QPointF(0, h), QSizeF(textWidth, lineLayouts[l].height));
334   int offset = 0;
335   foreach(FormatRange fr, textFormat) {
336     if(l >= lineLayouts.count()) break;
337     p->setFont(fr.format.font()); p->setPen(QPen(fr.format.foreground(), 0)); p->setBackground(fr.format.background());
338     int start, end, frend, llend;
339     do {
340       frend = fr.start + fr.length;
341       if(frend <= lineLayouts[l].start) break;
342       llend = lineLayouts[l].start + lineLayouts[l].length;
343       start = qMax(fr.start, lineLayouts[l].start); end = qMin(frend, llend);
344       rect.setLeft(tpos.x() + charPos[start] - offset);
345       p->drawText(rect, Qt::AlignLeft|Qt::TextSingleLine, styledText.text.mid(start, end - start), &brect);
346       if(llend <= end) {
347         h += lineLayouts[l].height;
348         l++;
349         if(l < lineLayouts.count()) {
350           rect = QRectF(tpos + QPointF(0, h), QSizeF(textWidth, lineLayouts[l].height));
351           offset = charPos[lineLayouts[l].start];
352         }
353       }
354     } while(end < frend && l < lineLayouts.count());
355   }
356 }