03a9d1f6c86cd47d585d06a53fbcde2200511c99
[quassel.git] / gui / chatwidget.cpp
1 /***************************************************************************
2  *   Copyright (C) 2005-07 by The Quassel 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 "util.h"
22 #include "style.h"
23 #include "chatwidget.h"
24 #include <QtGui>
25 #include <QtCore>
26
27 ChatWidget::ChatWidget(QWidget *parent) : QScrollArea(parent) {
28   setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
29   setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn);
30   setAlignment(Qt::AlignLeft | Qt::AlignTop);
31
32 }
33
34 void ChatWidget::init(QString netname, QString bufname, ChatWidgetContents *contentsWidget) {
35   networkName = netname;
36   bufferName = bufname;
37   //setAlignment(Qt::AlignBottom);
38   contents = contentsWidget;
39   setWidget(contents);
40   //setWidgetResizable(true);
41   //contents->setWidth(contents->sizeHint().width());
42   contents->setFocusProxy(this);
43   //contents->show();
44   //setAlignment(Qt::AlignBottom);
45 }
46
47 ChatWidget::~ChatWidget() {
48
49 }
50
51 void ChatWidget::clear() {
52   //contents->clear();
53 }
54
55 void ChatWidget::appendMsg(Message msg) {
56   contents->appendMsg(msg);
57   //qDebug() << "appending" << msg.text;
58
59 }
60
61 void ChatWidget::resizeEvent(QResizeEvent *event) {
62   //qDebug() << bufferName << isVisible() << event->size();
63   contents->setWidth(event->size().width());
64   //setAlignment(Qt::AlignBottom);
65   QScrollArea::resizeEvent(event);
66 }
67
68 /*************************************************************************************/
69
70 ChatWidgetContents::ChatWidgetContents(QString net, QString buf, QWidget *parent) : QWidget(parent) {
71   networkName = net;
72   bufferName = buf;
73   layoutTimer = new QTimer(this);
74   layoutTimer->setSingleShot(true);
75   connect(layoutTimer, SIGNAL(timeout()), this, SLOT(triggerLayout()));
76
77   setBackgroundRole(QPalette::Base);
78   setFont(QFont("Fixed"));
79
80   ycoords.append(0);
81   tsWidth = 90;
82   senderWidth = 100;
83   //textWidth = 400;
84   computePositions();
85   //setFixedWidth((int)(tsWidth + senderWidth + textWidth + 20));
86   setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
87   setMouseTracking(true);
88   mouseMode = Normal;
89   selectionMode = NoSelection;
90
91   doLayout = false;
92 }
93
94 ChatWidgetContents::~ChatWidgetContents() {
95   delete layoutTimer;
96   foreach(ChatLine *l, lines) {
97     delete l;
98   }
99 }
100
101 QSize ChatWidgetContents::sizeHint() const {
102   //qDebug() << size();
103   return size();
104 }
105
106 void ChatWidgetContents::paintEvent(QPaintEvent *event) {
107   QPainter painter(this);
108   qreal top = event->rect().top();
109   qreal bot = top + event->rect().height();
110   int idx = yToLineIdx(top);
111   if(idx < 0) return;
112   for(int i = idx; i < lines.count() ; i++) {
113     lines[i]->draw(&painter, QPointF(0, ycoords[i]));
114     if(ycoords[i+1] > bot) return;
115   }
116 }
117
118 void ChatWidgetContents::appendMsg(Message msg) {
119   ChatLine *line = new ChatLine(msg, networkName, bufferName);
120   qreal h = line->layout(tsWidth, senderWidth, textWidth);
121   ycoords.append(h + ycoords[ycoords.count() - 1]);
122   setFixedHeight((int)ycoords[ycoords.count()-1]);
123   lines.append(line);
124   update();
125   return;
126
127 }
128
129 void ChatWidgetContents::clear() {
130
131
132 }
133
134 //!\brief Computes the different x position vars for given tsWidth and senderWidth.
135 void ChatWidgetContents::computePositions() {
136   senderX = tsWidth + Style::sepTsSender();
137   textX = senderX + senderWidth + Style::sepSenderText();
138   tsGrabPos = tsWidth + (int)Style::sepTsSender()/2;
139   senderGrabPos = senderX + senderWidth + (int)Style::sepSenderText()/2;
140   textWidth = size().width() - textX;
141 }
142
143 void ChatWidgetContents::setWidth(qreal w) {
144   textWidth = (int)w - (Style::sepTsSender() + Style::sepSenderText()) - tsWidth - senderWidth;
145   setFixedWidth((int)w);
146   layout();
147 }
148
149 //!\brief Trigger layout (used by layoutTimer only).
150 /** This method is triggered by the layoutTimer. Layout the widget if it has been postponed earlier.
151  */
152 void ChatWidgetContents::triggerLayout() {
153   layout(true);
154 }
155
156 //!\brief Layout the widget.
157 /** The contents of the widget is re-layouted completely. Since this could take a while if the widget
158  * is huge, we don't want to trigger the layout procedure too often (causing layout calls to pile up).
159  * We use a timer that ensures layouting is only done if the last one has finished.
160  */
161 void ChatWidgetContents::layout(bool timer) {
162   if(layoutTimer->isActive()) {
163     // Layouting too fast. We set a flag though, so that a final layout is done when the timer runs out.
164     doLayout = true;
165     return;
166   }
167   if(timer && !doLayout) return; // Only check doLayout if we have been triggered by the timer!
168   qreal y = 0;
169   for(int i = 0; i < lines.count(); i++) {
170     qreal h = lines[i]->layout(tsWidth, senderWidth, textWidth);
171     ycoords[i+1] = h + ycoords[i];
172   }
173   setFixedHeight((int)ycoords[ycoords.count()-1]);
174   update();
175   doLayout = false; // Clear previous layout requests
176   layoutTimer->start(50); // Minimum time until we start the next layout
177 }
178
179 int ChatWidgetContents::yToLineIdx(qreal y) {
180   if(y >= ycoords[ycoords.count()-1]) ycoords.count()-1;
181   if(ycoords.count() <= 1) return 0;
182   int uidx = 0;
183   int oidx = ycoords.count() - 1;
184   int idx;
185   while(1) {
186     if(uidx == oidx - 1) return uidx;
187     idx = (uidx + oidx) / 2;
188     if(ycoords[idx] > y) oidx = idx;
189     else uidx = idx;
190   }
191 }
192
193 void ChatWidgetContents::mousePressEvent(QMouseEvent *event) {
194   if(event->button() == Qt::LeftButton) {
195     dragStartPos = event->pos();
196     dragStartMode = Normal;
197     switch(mouseMode) {
198       case Normal:
199         if(mousePos == OverTsSep) {
200           dragStartMode = DragTsSep;
201           setCursor(Qt::ClosedHandCursor);
202         } else if(mousePos == OverTextSep) {
203           dragStartMode = DragTextSep;
204           setCursor(Qt::ClosedHandCursor);
205         } else {
206           dragStartLine = yToLineIdx(event->pos().y());
207           dragStartCursor = lines[dragStartLine]->posToCursor(QPointF(event->pos().x(), event->pos().y()-ycoords[dragStartLine]));
208         }
209         mouseMode = Pressed;
210         break;
211     }
212   }
213 }
214
215 void ChatWidgetContents::mouseReleaseEvent(QMouseEvent *event) {
216   if(event->button() == Qt::LeftButton) {
217     dragStartPos = QPoint();
218     if(mousePos == OverTsSep || mousePos == OverTextSep) setCursor(Qt::OpenHandCursor);
219     else setCursor(Qt::ArrowCursor);
220
221     switch(mouseMode) {
222       case Pressed:
223         mouseMode = Normal;
224         clearSelection();
225         break;
226       case MarkText:
227         mouseMode = Normal;
228         selectionMode = TextSelected;
229         selectionLine = dragStartLine;
230         selectionStart = qMin(dragStartCursor, curCursor);
231         selectionEnd = qMax(dragStartCursor, curCursor);
232         // TODO Make X11SelectionMode configurable!
233         QApplication::clipboard()->setText(selectionToString());
234         break;
235       case MarkLines:
236         mouseMode = Normal;
237         selectionMode = LinesSelected;
238         selectionStart = qMin(dragStartLine, curLine);
239         selectionEnd = qMax(dragStartLine, curLine);
240         // TODO Make X11SelectionMode configurable!
241         QApplication::clipboard()->setText(selectionToString());
242         break;
243       default:
244         mouseMode = Normal;
245     }
246   }
247 }
248
249 //!\brief React to mouse movements over the ChatWidget.
250 /** This is called by Qt whenever the mouse moves. Here we have to do most of the mouse handling,
251  * such as changing column widths, marking text or initiating drag & drop.
252  */
253 void ChatWidgetContents::mouseMoveEvent(QMouseEvent *event) {
254   // Set some basic properties of the current position
255   int x = event->pos().x();
256   int y = event->pos().y();
257   MousePos oldpos = mousePos;
258   if(x >= tsGrabPos - 3 && x <= tsGrabPos + 3) mousePos = OverTsSep;
259   else if(x >= senderGrabPos - 3 && x <= senderGrabPos + 3) mousePos = OverTextSep;
260   else mousePos = None;
261
262   // Pass 1: Do whatever we can before switching mouse mode (if at all).
263   switch(mouseMode) {
264     // No special mode. Set mouse cursor if appropriate.
265     case Normal:
266       if(oldpos != mousePos) {
267         if(mousePos == OverTsSep || mousePos == OverTextSep) setCursor(Qt::OpenHandCursor);
268         else setCursor(Qt::ArrowCursor);
269       }
270       break;
271     // Left button pressed. Might initiate marking or drag & drop if we moved past the drag distance.
272     case Pressed:
273       if(!dragStartPos.isNull() && (dragStartPos - event->pos()).manhattanLength() >= QApplication::startDragDistance()) {
274         // Moving a column separator?
275         if(dragStartMode == DragTsSep) mouseMode = DragTsSep;
276         else if(dragStartMode == DragTextSep) mouseMode = DragTextSep;
277         // Nope. Check if we are over a selection to start drag & drop.
278         else if(dragStartMode == Normal) {
279           bool dragdrop = false;
280           if(selectionMode == TextSelected) {
281             int l = yToLineIdx(y);
282             if(selectionLine == l) {
283               int p = lines[l]->posToCursor(QPointF(x, y - ycoords[l]));
284               if(p >= selectionStart && p <= selectionEnd) dragdrop = true;
285             }
286           } else if(selectionMode == LinesSelected) {
287             int l = yToLineIdx(y);
288             if(l >= selectionStart && l <= selectionEnd) dragdrop = true;
289           }
290           // Ok, so just start drag & drop if appropriate.
291           if(dragdrop) {
292             QDrag *drag = new QDrag(this);
293             QMimeData *mimeData = new QMimeData;
294             mimeData->setText(selectionToString());
295             drag->setMimeData(mimeData);
296             drag->start();
297             mouseMode = Normal;
298           // Otherwise, clear the selection and start text marking!
299           } else {
300             clearSelection();
301             if(dragStartCursor < 0) { mouseMode = MarkLines; curLine = -1; }
302             else mouseMode = MarkText;
303           }
304         }
305       }
306       break;
307     case DragTsSep:
308       break;
309     case DragTextSep:
310       break;
311   }
312   // Pass 2: Some mouse modes need work after being set...
313   if(mouseMode == DragTsSep && x < size().width() - Style::sepSenderText() - senderWidth - 10) {
314     // Drag first column separator
315     int foo = Style::sepTsSender()/2;
316     tsWidth = qMax(x, foo) - foo;
317     computePositions();
318     layout();
319   } else if(mouseMode == DragTextSep && x < size().width() - 10) {
320     // Drag second column separator
321     int foo = tsWidth + Style::sepTsSender() + Style::sepSenderText()/2;
322     senderWidth = qMax(x, foo) - foo;
323     computePositions();
324     layout();
325   } else if(mouseMode == MarkText) {
326     // Change currently marked text
327     curLine = yToLineIdx(y);
328     int c = lines[curLine]->posToCursor(QPointF(x, y - ycoords[curLine]));
329     if(curLine == dragStartLine && c >= 0) {
330       if(c != curCursor) {
331         curCursor = c;
332         lines[curLine]->setSelection(ChatLine::Partial, dragStartCursor, c);
333         update();
334       }
335     } else {
336       mouseMode = MarkLines;
337       selectionStart = qMin(curLine, dragStartLine); selectionEnd = qMax(curLine, dragStartLine);
338       for(int i = selectionStart; i <= selectionEnd; i++) lines[i]->setSelection(ChatLine::Full);
339       update();
340     }
341   } else if(mouseMode == MarkLines) {
342     // Line marking
343     int l = yToLineIdx(y);
344     if(l != curLine) {
345       selectionStart = qMin(l, dragStartLine); selectionEnd = qMax(l, dragStartLine);
346       if(curLine >= 0 && curLine < selectionStart) {
347         for(int i = curLine; i < selectionStart; i++) lines[i]->setSelection(ChatLine::None);
348       } else if(curLine > selectionEnd) {
349         for(int i = selectionEnd+1; i <= curLine; i++) lines[i]->setSelection(ChatLine::None);
350       } else if(selectionStart < curLine && l < curLine) {
351           for(int i = selectionStart; i < curLine; i++) lines[i]->setSelection(ChatLine::Full);
352       } else if(curLine < selectionEnd && l > curLine) {
353         for(int i = curLine+1; i <= selectionEnd; i++) lines[i]->setSelection(ChatLine::Full);
354       }
355       curLine = l;
356       update();
357     }
358   }
359 }
360
361 //!\brief Clear current text selection.
362 void ChatWidgetContents::clearSelection() {
363   if(selectionMode == TextSelected) {
364     lines[selectionLine]->setSelection(ChatLine::None);
365   } else if(selectionMode == LinesSelected) {
366     for(int i = selectionStart; i <= selectionEnd; i++) {
367       lines[i]->setSelection(ChatLine::None);
368     }
369   }
370   selectionMode = NoSelection;
371   update();
372 }
373
374 //!\brief Convert current selection to human-readable string.
375 QString ChatWidgetContents::selectionToString() {
376   //TODO Make selection format configurable!
377   if(selectionMode == NoSelection) return "";
378   if(selectionMode == LinesSelected) {
379     QString result;
380     for(int l = selectionStart; l <= selectionEnd; l++) {
381       result += QString("[%1] %2 %3\n").arg(lines[l]->getTimeStamp().toLocalTime().toString("hh:mm:ss"))
382           .arg(lines[l]->getSender()).arg(lines[l]->getText());
383     }
384     return result;
385   }
386   // selectionMode == TextSelected
387   return lines[selectionLine]->getText().mid(selectionStart, selectionEnd - selectionStart);
388 }
389
390 /************************************************************************************/
391
392 //!\brief Construct a ChatLine object from a message.
393 /**
394  * \param m The message to be layouted and rendered
395  * \param net The network name
396  * \param buf The buffer name
397  */ 
398 ChatLine::ChatLine(Message m, QString net, QString buf) : QObject() {
399   hght = 0;
400   networkName = net;
401   bufferName = buf;
402   msg = m;
403   selectionMode = None;
404   formatMsg(msg);
405
406 }
407
408 ChatLine::~ChatLine() {
409
410 }
411
412 void ChatLine::formatMsg(Message msg) {
413   QString user = userFromMask(msg.sender);
414   QString host = hostFromMask(msg.sender);
415   QString nick = nickFromMask(msg.sender);
416   QString text = Style::mircToInternal(msg.text);
417
418   QString c = tr("%DT[%1]").arg(msg.timeStamp.toLocalTime().toString("hh:mm:ss"));
419   QString s, t;
420   switch(msg.type) {
421     case Message::Plain:
422       s = tr("%DS<%1>").arg(nick); t = tr("%D0%1").arg(text); break;
423     case Message::Server:
424       s = tr("%Ds*"); t = tr("%Ds%1").arg(text); break;
425     case Message::Error:
426       s = tr("%De*"); t = tr("%De%1").arg(text); break;
427     case Message::Join:
428       s = tr("%Dj-->"); t = tr("%Dj%DN%1%DN %DH(%2@%3)%DH has joined %DC%4%DC").arg(nick, user, host, bufferName); break;
429     case Message::Part:
430       s = tr("%Dp<--"); t = tr("%Dp%DN%1%DN %DH(%2@%3)%DH has left %DC%4%DC").arg(nick, user, host, bufferName);
431       if(!text.isEmpty()) t = QString("%1 (%2)").arg(t).arg(text);
432       break;
433     case Message::Quit:
434       s = tr("%Dq<--"); t = tr("%Dq%DN%1%DN %DH(%2@%3)%DH has quit").arg(nick, user, host);
435       if(!text.isEmpty()) t = QString("%1 (%2)").arg(t).arg(text);
436       break;
437     case Message::Kick:
438       { s = tr("%Dk<-*");
439         QString victim = text.section(" ", 0, 0);
440         //if(victim == ui.ownNick->currentText()) victim = tr("you");
441         QString kickmsg = text.section(" ", 1);
442         t = tr("%Dk%DN%1%DN has kicked %DN%2%DN from %DC%3%DC").arg(nick).arg(victim).arg(bufferName);
443         if(!kickmsg.isEmpty()) t = QString("%1 (%2)").arg(t).arg(kickmsg);
444       }
445       break;
446     case Message::Nick:
447       s = tr("%Dr<->");
448       if(nick == msg.text) t = tr("You are now known as %DN%1%DN").arg(msg.text);
449       else t = tr("%DN%1%DN is now known as %DN%2%DN").arg(nick, msg.text);
450       break;
451     case Message::Mode:
452       s = tr("%Dm***");
453       if(nick.isEmpty()) t = tr("User mode: %DM%1%DM").arg(msg.text);
454       else t = tr("Mode %DM%1%DM by %DN%2%DN").arg(msg.text, nick);
455       break;
456     default:
457       s = tr("%De%1").arg(msg.sender);
458       t = tr("%De[%1]").arg(msg.text);
459   }
460   QTextOption tsOption, senderOption, textOption;
461   tsFormatted = Style::internalToFormatted(c);
462   senderFormatted = Style::internalToFormatted(s);
463   textFormatted = Style::internalToFormatted(t);
464   tsLayout.setText(tsFormatted.text); tsLayout.setAdditionalFormats(tsFormatted.formats);
465   tsOption.setWrapMode(QTextOption::NoWrap);
466   tsLayout.setTextOption(tsOption);
467   senderLayout.setText(senderFormatted.text); senderLayout.setAdditionalFormats(senderFormatted.formats);
468   senderOption.setAlignment(Qt::AlignRight); senderOption.setWrapMode(QTextOption::ManualWrap);
469   senderLayout.setTextOption(senderOption);
470   textLayout.setText(textFormatted.text); textLayout.setAdditionalFormats(textFormatted.formats);
471   textOption.setWrapMode(QTextOption::WrapAnywhere); // seems to do what we want, apparently
472   textLayout.setTextOption(textOption);
473 }
474
475 //!\brief Return the cursor position for the given coordinate pos.
476 /**
477  * \param pos The position relative to the ChatLine
478  * \return The cursor position, or -2 for timestamp, or -1 for sender
479  */
480 int ChatLine::posToCursor(QPointF pos) {
481   if(pos.x() < tsWidth + (int)Style::sepTsSender()/2) return -2;
482   qreal textStart = tsWidth + Style::sepTsSender() + senderWidth + Style::sepSenderText();
483   if(pos.x() < textStart) return -1;
484   for(int l = textLayout.lineCount() - 1; l >=0; l--) {
485     QTextLine line = textLayout.lineAt(l);
486     if(pos.y() >= line.position().y()) {
487       int p = line.xToCursor(pos.x() - textStart, QTextLine::CursorOnCharacter);
488       return p;
489     }
490   }
491 }
492
493 void ChatLine::setSelection(SelectionMode mode, int start, int end) {
494   selectionMode = mode;
495   tsFormat.clear(); senderFormat.clear(); textFormat.clear();
496   QPalette pal = QApplication::palette();
497   QTextLayout::FormatRange tsSel, senderSel, textSel;
498   switch (mode) {
499     case None:
500       break;
501     case Partial:
502       selectionStart = qMin(start, end); selectionEnd = qMax(start, end);
503       textSel.format.setForeground(pal.brush(QPalette::HighlightedText));
504       textSel.format.setBackground(pal.brush(QPalette::Highlight));
505       textSel.start = selectionStart;
506       textSel.length = selectionEnd - selectionStart;
507       textFormat.append(textSel);
508       break;
509     case Full:
510       tsSel.format.setForeground(pal.brush(QPalette::HighlightedText));
511       tsSel.start = 0; tsSel.length = tsLayout.text().length(); tsFormat.append(tsSel);
512       senderSel.format.setForeground(pal.brush(QPalette::HighlightedText));
513       senderSel.start = 0; senderSel.length = senderLayout.text().length(); senderFormat.append(senderSel);
514       textSel.format.setForeground(pal.brush(QPalette::HighlightedText));
515       textSel.start = 0; textSel.length = textLayout.text().length(); textFormat.append(textSel);
516       break;
517   }
518 }
519
520 QDateTime ChatLine::getTimeStamp() {
521   return msg.timeStamp;
522 }
523
524 QString ChatLine::getSender() {
525   return senderLayout.text();
526 }
527
528 QString ChatLine::getText() {
529   return textLayout.text();
530 }
531
532 qreal ChatLine::layout(qreal tsw, qreal senderw, qreal textw) {
533   tsWidth = tsw; senderWidth = senderw; textWidth = textw;
534   QTextLine tl;
535   tsLayout.beginLayout();
536   tl = tsLayout.createLine();
537   tl.setLineWidth(tsWidth);
538   tl.setPosition(QPointF(0, 0));
539   tsLayout.endLayout();
540
541   senderLayout.beginLayout();
542   tl = senderLayout.createLine();
543   tl.setLineWidth(senderWidth);
544   tl.setPosition(QPointF(0, 0));
545   senderLayout.endLayout();
546
547   qreal h = 0;
548   textLayout.beginLayout();
549   while(1) {
550     tl = textLayout.createLine();
551     if(!tl.isValid()) break;
552     tl.setLineWidth(textWidth);
553     tl.setPosition(QPointF(0, h));
554     h += tl.height();
555   }
556   textLayout.endLayout();
557   hght = h;
558   return h;
559 }
560
561 //!\brief Draw ChatLine on the given QPainter at the given position.
562 void ChatLine::draw(QPainter *p, const QPointF &pos) {
563   QPalette pal = QApplication::palette();
564   if(selectionMode == Full) {
565     p->setPen(Qt::NoPen);
566     p->setBrush(pal.brush(QPalette::Highlight));
567     p->drawRect(QRectF(pos, QSizeF(tsWidth + Style::sepTsSender() + senderWidth + Style::sepSenderText() + textWidth, height())));
568   } else if(selectionMode == Partial) {
569
570   }
571   p->setClipRect(QRectF(pos, QSizeF(tsWidth, height())));
572   tsLayout.draw(p, pos, tsFormat);
573   p->setClipRect(QRectF(pos + QPointF(tsWidth + Style::sepTsSender(), 0), QSizeF(senderWidth, height())));
574   senderLayout.draw(p, pos + QPointF(tsWidth + Style::sepTsSender(), 0), senderFormat);
575   p->setClipping(false);
576   textLayout.draw(p, pos + QPointF(tsWidth + Style::sepTsSender() + senderWidth + Style::sepSenderText(), 0), textFormat);
577 }