Added getNetworkId(UserId user, const QString &network) to make the transition to...
[quassel.git] / src / qtgui / 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 "chatline.h"
25
26
27 ChatWidget::ChatWidget(QWidget *parent) : QAbstractScrollArea(parent) {
28   scrollTimer = new QTimer(this);
29   scrollTimer->setSingleShot(false);
30   scrollTimer->setInterval(100);
31   setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding);
32   setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
33   setMinimumSize(QSize(400,400));
34   setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
35   setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn);
36   bottomLine = -1;
37   height = 0;
38   ycoords.append(0);
39   pointerPosition = QPoint(0,0);
40   connect(verticalScrollBar(), SIGNAL(actionTriggered(int)), this, SLOT(scrollBarAction(int)));
41   connect(verticalScrollBar(), SIGNAL(valueChanged(int)), this, SLOT(scrollBarValChanged(int)));
42 }
43
44 void ChatWidget::init(QString netname, QString bufname) {
45   networkName = netname;
46   bufferName = bufname;
47   setBackgroundRole(QPalette::Base);
48   setFont(QFont("Fixed"));
49   tsWidth = 90;
50   senderWidth = 100;
51   computePositions();
52   adjustScrollBar();
53   verticalScrollBar()->setValue(verticalScrollBar()->maximum());
54   //verticalScrollBar()->setPageStep(viewport()->height());
55   //verticalScrollBar()->setSingleStep(20);
56   //verticalScrollBar()->setMinimum(0);
57   //verticalScrollBar()->setMaximum((int)height - verticalScrollBar()->pageStep());
58
59   setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
60   setMouseTracking(true);
61   mouseMode = Normal;
62   selectionMode = NoSelection;
63   connect(scrollTimer, SIGNAL(timeout()), this, SLOT(handleScrollTimer()));
64 }
65
66 ChatWidget::~ChatWidget() {
67   //qDebug() << "destroying chatwidget" << bufferName;
68   //foreach(ChatLine *l, lines) {
69   //  delete l;
70   //}
71 }
72
73 QSize ChatWidget::sizeHint() const {
74   //qDebug() << size();
75   return size();
76 }
77
78 void ChatWidget::adjustScrollBar() {
79   verticalScrollBar()->setPageStep(viewport()->height());
80   verticalScrollBar()->setSingleStep(20);
81   verticalScrollBar()->setMinimum(0);
82   verticalScrollBar()->setMaximum((int)height - verticalScrollBar()->pageStep());
83   //qDebug() << height << viewport()->height() << verticalScrollBar()->pageStep();
84   //if(bottomLine < 0) {
85   //  verticalScrollBar()->setValue(verticalScrollBar()->maximum());
86   //} else {
87     //int bot = verticalScrollBar()->value() + viewport()->height(); //qDebug() << bottomLine;
88     //verticalScrollBar()->setValue(qMax(0, (int)ycoords[bottomLine+1] - viewport()->height()));
89   //}
90 }
91
92 void ChatWidget::scrollBarValChanged(int /*val*/) {
93   /*
94   if(val >= verticalScrollBar()->maximum()) bottomLine = -1;
95   else {
96     int bot = val + viewport()->height();
97     int line = yToLineIdx(bot);
98     //bottomLine = line;
99   }
100   */
101 }
102
103 void ChatWidget::scrollBarAction(int action) {
104   switch(action) {
105     case QScrollBar::SliderSingleStepAdd:
106       // More elaborate. But what with loooong lines?
107       // verticalScrollBar()->setValue((int)ycoords[yToLineIdx(verticalScrollBar()->value() + viewport()->height()) + 1] - viewport()->height());
108       break;
109     case QScrollBar::SliderSingleStepSub:
110       //verticalScrollBar()->setValue((int)ycoords[yToLineIdx(verticalScrollBar()->value())]);
111       break;
112
113   }
114
115 }
116
117 void ChatWidget::handleScrollTimer() {
118   if(mouseMode == MarkText || mouseMode == MarkLines) {
119     if(pointerPosition.y() > viewport()->height()) {
120       verticalScrollBar()->setValue(verticalScrollBar()->value() + pointerPosition.y() - viewport()->height());
121       handleMouseMoveEvent(QPoint(pointerPosition.x(), viewport()->height()));
122     } else if(pointerPosition.y() < 0) {
123       verticalScrollBar()->setValue(verticalScrollBar()->value() + pointerPosition.y());
124       handleMouseMoveEvent(QPoint(pointerPosition.x(), 0));
125     }
126   }
127 }
128
129 void ChatWidget::ensureVisible(int line) {
130   int top = verticalScrollBar()->value();
131   int bot = top + viewport()->height();
132   if(ycoords[line+1] > bot) {
133     verticalScrollBar()->setValue(qMax(0, (int)ycoords[line+1] - viewport()->height()));
134   } else if(ycoords[line] < top) {
135     verticalScrollBar()->setValue((int)ycoords[line]);
136   }
137
138 }
139
140 void ChatWidget::clear() {
141   //contents->clear();
142 }
143
144 void ChatWidget::prependMsg(AbstractUiMsg *msg) {
145   ChatLine *line = dynamic_cast<ChatLine*>(msg);
146   Q_ASSERT(line);
147   prependChatLine(line);
148 }
149
150 void ChatWidget::prependChatLine(ChatLine *line) {
151   qreal h = line->layout(tsWidth, senderWidth, textWidth);
152   for(int i = 1; i < ycoords.count(); i++) ycoords[i] += h;
153   ycoords.insert(1, h);
154   lines.prepend(line);
155   height += h;
156   // Fix all variables containing line numbers
157   dragStartLine ++;
158   curLine ++;
159   selectionStart ++; selectionEnd ++;
160   adjustScrollBar();
161   verticalScrollBar()->setValue(verticalScrollBar()->value() + (int)h);
162   viewport()->update();
163 }
164
165 void ChatWidget::prependChatLines(QList<ChatLine *> clist) {
166   QList<qreal> tmpy; tmpy.append(0);
167   qreal h = 0;
168   foreach(ChatLine *l, clist) {
169     h += l->layout(tsWidth, senderWidth, textWidth);
170     tmpy.append(h);
171   }
172   ycoords.removeFirst();
173   for(int i = 0; i < ycoords.count(); i++) ycoords[i] += h;
174   ycoords = tmpy + ycoords;
175   lines = clist + lines;
176   height += h;
177   // Fix all variables containing line numbers
178   int i = clist.count();
179   dragStartLine += i;
180   curLine += i;
181   selectionStart += i; selectionEnd += i; //? selectionEnd += i;
182   //if(bottomLine >= 0) bottomLine += i;
183   adjustScrollBar();
184   //verticalScrollBar()->setPageStep(viewport()->height());
185   //verticalScrollBar()->setSingleStep(20);
186   //verticalScrollBar()->setMaximum((int)height - verticalScrollBar()->pageStep());
187   verticalScrollBar()->setValue(verticalScrollBar()->value() + (int)h);
188   viewport()->update();
189 }
190
191 void ChatWidget::appendMsg(AbstractUiMsg *msg) {
192   ChatLine *line = dynamic_cast<ChatLine*>(msg);
193   Q_ASSERT(line);
194   appendChatLine(line);
195 }
196
197 void ChatWidget::appendChatLine(ChatLine *line) {
198   qreal h = line->layout(tsWidth, senderWidth, textWidth);
199   ycoords.append(h + ycoords[ycoords.count() - 1]);
200   height += h;
201   bool flg = (verticalScrollBar()->value() == verticalScrollBar()->maximum());
202   adjustScrollBar();
203   if(flg) verticalScrollBar()->setValue(verticalScrollBar()->maximum());
204   lines.append(line);
205   viewport()->update();
206 }
207
208
209 void ChatWidget::appendChatLines(QList<ChatLine *> list) {
210   foreach(ChatLine *line, list) {
211     qreal h = line->layout(tsWidth, senderWidth, textWidth);
212     ycoords.append(h + ycoords[ycoords.count() - 1]);
213     height += h;
214     lines.append(line);
215   }
216   bool flg = (verticalScrollBar()->value() == verticalScrollBar()->maximum());
217   adjustScrollBar();
218   if(flg) verticalScrollBar()->setValue(verticalScrollBar()->maximum());
219   viewport()->update();
220 }
221
222 void ChatWidget::setContents(QList<ChatLine *> list) {
223   ycoords.clear();
224   ycoords.append(0);
225   height = 0;
226   lines.clear();
227   appendChatLines(list);
228 }
229
230 //!\brief Computes the different x position vars for given tsWidth and senderWidth.
231 void ChatWidget::computePositions() {
232   senderX = tsWidth + Style::sepTsSender();
233   textX = senderX + senderWidth + Style::sepSenderText();
234   tsGrabPos = tsWidth + (int)Style::sepTsSender()/2;
235   senderGrabPos = senderX + senderWidth + (int)Style::sepSenderText()/2;
236   textWidth = viewport()->size().width() - textX;
237 }
238
239 void ChatWidget::resizeEvent(QResizeEvent *event) {
240   //qDebug() << bufferName << isVisible() << event->size() << event->oldSize();
241   /*if(event->oldSize().isValid())*/
242   //contents->setWidth(event->size().width());
243   //setAlignment(Qt::AlignBottom);
244   if(event->size().width() != event->oldSize().width()) {
245     computePositions();
246     layout();
247   }
248   //adjustScrollBar();
249   //qDebug() << viewport()->size() << viewport()->height();
250   //QAbstractScrollArea::resizeEvent(event);
251   //qDebug() << viewport()->size() << viewport()->geometry();
252 }
253
254 void ChatWidget::paintEvent(QPaintEvent *event) {
255   QPainter painter(viewport());
256
257   //qDebug() <<  verticalScrollBar()->value();
258   painter.translate(0, -verticalScrollBar()->value());
259   int top = event->rect().top() + verticalScrollBar()->value();
260   int bot = top + event->rect().height();
261   int idx = yToLineIdx(top);
262   if(idx < 0) return;
263   for(int i = idx; i < lines.count() ; i++) {
264     lines[i]->draw(&painter, QPointF(0, ycoords[i]));
265     if(ycoords[i+1] > bot) return;
266   }
267 }
268
269 //!\brief Layout the widget.
270 void ChatWidget::layout() {
271   // TODO fix scrollbars
272   //int botLine = yToLineIdx(verticalScrollBar()->value() + 
273   for(int i = 0; i < lines.count(); i++) {
274     qreal h = lines[i]->layout(tsWidth, senderWidth, textWidth);
275     ycoords[i+1] = h + ycoords[i];
276   }
277   height = ycoords[ycoords.count()-1];
278   adjustScrollBar();
279   verticalScrollBar()->setValue(verticalScrollBar()->maximum());
280   viewport()->update();
281 }
282
283 int ChatWidget::yToLineIdx(qreal y) {
284   if(y >= ycoords[ycoords.count()-1]) return ycoords.count()-2;
285   if(ycoords.count() <= 1) return 0;
286   int uidx = 0;
287   int oidx = ycoords.count() - 1;
288   int idx;
289   while(1) {
290     if(uidx == oidx - 1) return uidx;
291     idx = (uidx + oidx) / 2;
292     if(ycoords[idx] > y) oidx = idx;
293     else uidx = idx;
294   }
295 }
296
297 void ChatWidget::mousePressEvent(QMouseEvent *event) {
298   if(lines.isEmpty()) return;
299   QPoint pos = event->pos() + QPoint(0, verticalScrollBar()->value());
300   if(event->button() == Qt::LeftButton) {
301     dragStartPos = pos;
302     dragStartMode = Normal;
303     switch(mouseMode) {
304       case Normal:
305         if(mousePos == OverTsSep) {
306           dragStartMode = DragTsSep;
307           setCursor(Qt::ClosedHandCursor);
308         } else if(mousePos == OverTextSep) {
309           dragStartMode = DragTextSep;
310           setCursor(Qt::ClosedHandCursor);
311         } else {
312           dragStartLine = yToLineIdx(pos.y());
313           dragStartCursor = lines[dragStartLine]->posToCursor(QPointF(pos.x(), pos.y()-ycoords[dragStartLine]));
314         }
315         mouseMode = Pressed;
316         break;
317       default:
318         break;
319     }
320   }
321 }
322
323 void ChatWidget::mouseDoubleClickEvent(QMouseEvent * /*event*/) {
324
325
326
327 }
328
329 void ChatWidget::mouseReleaseEvent(QMouseEvent *event) {
330   //QPoint pos = event->pos() + QPoint(0, verticalScrollBar()->value());
331
332   if(event->button() == Qt::LeftButton) {
333     dragStartPos = QPoint();
334     if(mousePos == OverTsSep || mousePos == OverTextSep) setCursor(Qt::OpenHandCursor);
335     else setCursor(Qt::ArrowCursor);
336
337     switch(mouseMode) {
338       case Pressed:
339         mouseMode = Normal;
340         clearSelection();
341         break;
342       case MarkText:
343         mouseMode = Normal;
344         selectionMode = TextSelected;
345         selectionLine = dragStartLine;
346         selectionStart = qMin(dragStartCursor, curCursor);
347         selectionEnd = qMax(dragStartCursor, curCursor);
348         // TODO Make X11SelectionMode configurable!
349         QApplication::clipboard()->setText(selectionToString());
350         break;
351       case MarkLines:
352         mouseMode = Normal;
353         selectionMode = LinesSelected;
354         selectionStart = qMin(dragStartLine, curLine);
355         selectionEnd = qMax(dragStartLine, curLine);
356         // TODO Make X11SelectionMode configurable!
357         QApplication::clipboard()->setText(selectionToString());
358         break;
359       default:
360         mouseMode = Normal;
361     }
362   }
363 }
364
365 //!\brief React to mouse movements over the ChatWidget.
366 /** This is called by Qt whenever the mouse moves. Here we have to do most of the mouse handling,
367  * such as changing column widths, marking text or initiating drag & drop.
368  */
369 void ChatWidget::mouseMoveEvent(QMouseEvent *event) {
370   QPoint pos = event->pos(); pointerPosition = pos;
371   // Scroll if mouse pointer leaves widget while dragging
372   if((mouseMode == MarkText || mouseMode == MarkLines) && (pos.y() > viewport()->height() || pos.y() < 0)) {
373     if(!scrollTimer->isActive()) {
374       scrollTimer->start();
375     }
376   } else {
377     if(scrollTimer->isActive()) {
378       scrollTimer->stop();
379     }
380   }
381   handleMouseMoveEvent(pos);
382 }
383
384 void ChatWidget::handleMouseMoveEvent(const QPoint &_pos) {
385   // FIXME
386   if(lines.count() <= 0) return;
387   // Set some basic properties of the current position
388   QPoint pos = _pos + QPoint(0, verticalScrollBar()->value());
389   int x = pos.x();
390   int y = pos.y();
391   //MousePos oldpos = mousePos;
392   if(x >= tsGrabPos - 3 && x <= tsGrabPos + 3) mousePos = OverTsSep;
393   else if(x >= senderGrabPos - 3 && x <= senderGrabPos + 3) mousePos = OverTextSep;
394   else mousePos = None;
395
396   // Pass 1: Do whatever we can before switching mouse mode (if at all).
397   switch(mouseMode) {
398     // No special mode. Set mouse cursor if appropriate.
399     case Normal:
400     {
401       //if(oldpos != mousePos) {
402       if(mousePos == OverTsSep || mousePos == OverTextSep) setCursor(Qt::OpenHandCursor);
403       else {
404         int l = yToLineIdx(y);
405         int c = lines[l]->posToCursor(QPointF(x, y - ycoords[l]));
406         if(c >= 0 && lines[l]->isUrl(c)) {
407           setCursor(Qt::PointingHandCursor);
408         } else {
409           setCursor(Qt::ArrowCursor);
410         }
411       }
412     }
413       break;
414     // Left button pressed. Might initiate marking or drag & drop if we moved past the drag distance.
415     case Pressed:
416       if(!dragStartPos.isNull() && (dragStartPos - pos).manhattanLength() >= QApplication::startDragDistance()) {
417         // Moving a column separator?
418         if(dragStartMode == DragTsSep) mouseMode = DragTsSep;
419         else if(dragStartMode == DragTextSep) mouseMode = DragTextSep;
420         // Nope. Check if we are over a selection to start drag & drop.
421         else if(dragStartMode == Normal) {
422           bool dragdrop = false;
423           if(selectionMode == TextSelected) {
424             int l = yToLineIdx(y);
425             if(selectionLine == l) {
426               int p = lines[l]->posToCursor(QPointF(x, y - ycoords[l]));
427               if(p >= selectionStart && p <= selectionEnd) dragdrop = true;
428             }
429           } else if(selectionMode == LinesSelected) {
430             int l = yToLineIdx(y);
431             if(l >= selectionStart && l <= selectionEnd) dragdrop = true;
432           }
433           // Ok, so just start drag & drop if appropriate.
434           if(dragdrop) {
435             QDrag *drag = new QDrag(this);
436             QMimeData *mimeData = new QMimeData;
437             mimeData->setText(selectionToString());
438             drag->setMimeData(mimeData);
439             drag->start();
440             mouseMode = Normal;
441           // Otherwise, clear the selection and start text marking!
442           } else {
443             setCursor(Qt::ArrowCursor);
444             clearSelection();
445             if(dragStartCursor < 0) { mouseMode = MarkLines; curLine = -1; }
446             else mouseMode = MarkText;
447           }
448         }
449       }
450       break;
451     case DragTsSep:
452       break;
453     case DragTextSep:
454       break;
455     default:
456       break;
457   }
458   // Pass 2: Some mouse modes need work after being set...
459   if(mouseMode == DragTsSep && x < size().width() - Style::sepSenderText() - senderWidth - 10) {
460     // Drag first column separator
461     int foo = Style::sepTsSender()/2;
462     tsWidth = qMax(x, foo) - foo;
463     computePositions();
464     layout();
465   } else if(mouseMode == DragTextSep && x < size().width() - 10) {
466     // Drag second column separator
467     int foo = tsWidth + Style::sepTsSender() + Style::sepSenderText()/2;
468     senderWidth = qMax(x, foo) - foo;
469     computePositions();
470     layout();
471   } else if(mouseMode == MarkText) {
472     // Change currently marked text
473     curLine = yToLineIdx(y);
474     int c = lines[curLine]->posToCursor(QPointF(x, y - ycoords[curLine]));
475     if(curLine == dragStartLine && c >= 0) {
476       if(c != curCursor) {
477         curCursor = c;
478         lines[curLine]->setSelection(ChatLine::Partial, dragStartCursor, c);
479         viewport()->update();
480       }
481     } else {
482       mouseMode = MarkLines;
483       selectionStart = qMin(curLine, dragStartLine); selectionEnd = qMax(curLine, dragStartLine);
484       for(int i = selectionStart; i <= selectionEnd; i++) lines[i]->setSelection(ChatLine::Full);
485       viewport()->update();
486     }
487   } else if(mouseMode == MarkLines) {
488     // Line marking
489     int l = yToLineIdx(y);
490     if(l != curLine) {
491       selectionStart = qMin(l, dragStartLine); selectionEnd = qMax(l, dragStartLine);
492       if(curLine < 0) {
493         Q_ASSERT(selectionStart == selectionEnd);
494         lines[l]->setSelection(ChatLine::Full);
495       } else {
496         if(curLine < selectionStart) {
497           for(int i = curLine; i < selectionStart; i++) lines[i]->setSelection(ChatLine::None);
498         } else if(curLine > selectionEnd) {
499           for(int i = selectionEnd+1; i <= curLine; i++) lines[i]->setSelection(ChatLine::None);
500         } else if(selectionStart < curLine && l < curLine) {
501           for(int i = selectionStart; i < curLine; i++) lines[i]->setSelection(ChatLine::Full);
502         } else if(curLine < selectionEnd && l > curLine) {
503           for(int i = curLine+1; i <= selectionEnd; i++) lines[i]->setSelection(ChatLine::Full);
504         }
505       }
506       curLine = l;
507       //ensureVisible(l);
508       viewport()->update();
509     }
510   }
511 }
512
513 //!\brief Clear current text selection.
514 void ChatWidget::clearSelection() {
515   if(selectionMode == TextSelected) {
516     lines[selectionLine]->setSelection(ChatLine::None);
517   } else if(selectionMode == LinesSelected) {
518     for(int i = selectionStart; i <= selectionEnd; i++) {
519       lines[i]->setSelection(ChatLine::None);
520     }
521   }
522   selectionMode = NoSelection;
523   viewport()->update();
524 }
525
526 //!\brief Convert current selection to human-readable string.
527 QString ChatWidget::selectionToString() {
528   //TODO Make selection format configurable!
529   if(selectionMode == NoSelection) return "";
530   if(selectionMode == LinesSelected) {
531     QString result;
532     for(int l = selectionStart; l <= selectionEnd; l++) {
533       result += QString("[%1] %2 %3\n").arg(lines[l]->timeStamp().toLocalTime().toString("hh:mm:ss"))
534           .arg(lines[l]->sender()).arg(lines[l]->text());
535     }
536     return result;
537   }
538   // selectionMode == TextSelected
539   return lines[selectionLine]->text().mid(selectionStart, selectionEnd - selectionStart);
540 }
541