1 /***************************************************************************
2 * Copyright (C) 2005-07 by The Quassel Team *
3 * devel@quassel-irc.org *
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. *
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. *
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 ***************************************************************************/
23 #include "chatwidget.h"
27 ChatWidget::ChatWidget(QWidget *parent) : QScrollArea(parent) {
28 setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
29 setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn);
30 setAlignment(Qt::AlignLeft | Qt::AlignTop);
34 void ChatWidget::init(QString netname, QString bufname, ChatWidgetContents *contentsWidget) {
35 networkName = netname;
37 //setAlignment(Qt::AlignBottom);
38 contents = contentsWidget;
40 //setWidgetResizable(true);
41 //contents->setWidth(contents->sizeHint().width());
42 contents->setFocusProxy(this);
44 //setAlignment(Qt::AlignBottom);
47 ChatWidget::~ChatWidget() {
51 void ChatWidget::clear() {
55 void ChatWidget::appendMsg(Message msg) {
56 contents->appendMsg(msg);
57 //qDebug() << "appending" << msg.text;
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);
68 /*************************************************************************************/
70 ChatWidgetContents::ChatWidgetContents(QString net, QString buf, QWidget *parent) : QWidget(parent) {
73 layoutTimer = new QTimer(this);
74 layoutTimer->setSingleShot(true);
75 connect(layoutTimer, SIGNAL(timeout()), this, SLOT(triggerLayout()));
77 setBackgroundRole(QPalette::Base);
78 setFont(QFont("Fixed"));
85 //setFixedWidth((int)(tsWidth + senderWidth + textWidth + 20));
86 setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
87 setMouseTracking(true);
89 selectionMode = NoSelection;
94 ChatWidgetContents::~ChatWidgetContents() {
96 foreach(ChatLine *l, lines) {
101 QSize ChatWidgetContents::sizeHint() const {
102 //qDebug() << size();
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);
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;
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]);
129 void ChatWidgetContents::clear() {
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;
143 void ChatWidgetContents::setWidth(qreal w) {
144 textWidth = (int)w - (Style::sepTsSender() + Style::sepSenderText()) - tsWidth - senderWidth;
145 setFixedWidth((int)w);
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.
152 void ChatWidgetContents::triggerLayout() {
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.
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.
167 if(timer && !doLayout) return; // Only check doLayout if we have been triggered by the timer!
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];
173 setFixedHeight((int)ycoords[ycoords.count()-1]);
175 doLayout = false; // Clear previous layout requests
176 layoutTimer->start(50); // Minimum time until we start the next layout
179 int ChatWidgetContents::yToLineIdx(qreal y) {
180 if(y >= ycoords[ycoords.count()-1]) ycoords.count()-1;
181 if(ycoords.count() <= 1) return 0;
183 int oidx = ycoords.count() - 1;
186 if(uidx == oidx - 1) return uidx;
187 idx = (uidx + oidx) / 2;
188 if(ycoords[idx] > y) oidx = idx;
193 void ChatWidgetContents::mousePressEvent(QMouseEvent *event) {
194 if(event->button() == Qt::LeftButton) {
195 dragStartPos = event->pos();
196 dragStartMode = Normal;
199 if(mousePos == OverTsSep) {
200 dragStartMode = DragTsSep;
201 setCursor(Qt::ClosedHandCursor);
202 } else if(mousePos == OverTextSep) {
203 dragStartMode = DragTextSep;
204 setCursor(Qt::ClosedHandCursor);
206 dragStartLine = yToLineIdx(event->pos().y());
207 dragStartCursor = lines[dragStartLine]->posToCursor(QPointF(event->pos().x(), event->pos().y()-ycoords[dragStartLine]));
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);
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());
237 selectionMode = LinesSelected;
238 selectionStart = qMin(dragStartLine, curLine);
239 selectionEnd = qMax(dragStartLine, curLine);
240 // TODO Make X11SelectionMode configurable!
241 QApplication::clipboard()->setText(selectionToString());
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.
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;
262 // Pass 1: Do whatever we can before switching mouse mode (if at all).
264 // No special mode. Set mouse cursor if appropriate.
266 if(oldpos != mousePos) {
267 if(mousePos == OverTsSep || mousePos == OverTextSep) setCursor(Qt::OpenHandCursor);
268 else setCursor(Qt::ArrowCursor);
271 // Left button pressed. Might initiate marking or drag & drop if we moved past the drag distance.
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;
286 } else if(selectionMode == LinesSelected) {
287 int l = yToLineIdx(y);
288 if(l >= selectionStart && l <= selectionEnd) dragdrop = true;
290 // Ok, so just start drag & drop if appropriate.
292 QDrag *drag = new QDrag(this);
293 QMimeData *mimeData = new QMimeData;
294 mimeData->setText(selectionToString());
295 drag->setMimeData(mimeData);
298 // Otherwise, clear the selection and start text marking!
301 if(dragStartCursor < 0) { mouseMode = MarkLines; curLine = -1; }
302 else mouseMode = MarkText;
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;
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;
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) {
332 lines[curLine]->setSelection(ChatLine::Partial, dragStartCursor, c);
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);
341 } else if(mouseMode == MarkLines) {
343 int l = yToLineIdx(y);
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);
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);
370 selectionMode = NoSelection;
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) {
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());
386 // selectionMode == TextSelected
387 return lines[selectionLine]->getText().mid(selectionStart, selectionEnd - selectionStart);
390 /************************************************************************************/
392 //!\brief Construct a ChatLine object from a message.
394 * \param m The message to be layouted and rendered
395 * \param net The network name
396 * \param buf The buffer name
398 ChatLine::ChatLine(Message m, QString net, QString buf) : QObject() {
403 selectionMode = None;
408 ChatLine::~ChatLine() {
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);
418 QString c = tr("%DT[%1]").arg(msg.timeStamp.toLocalTime().toString("hh:mm:ss"));
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;
426 s = tr("%De*"); t = tr("%De%1").arg(text); break;
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;
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);
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);
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);
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);
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);
457 s = tr("%De%1").arg(msg.sender);
458 t = tr("%De[%1]").arg(msg.text);
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);
475 //!\brief Return the cursor position for the given coordinate pos.
477 * \param pos The position relative to the ChatLine
478 * \return The cursor position, or -2 for timestamp, or -1 for sender
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);
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;
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);
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);
520 QDateTime ChatLine::getTimeStamp() {
521 return msg.timeStamp;
524 QString ChatLine::getSender() {
525 return senderLayout.text();
528 QString ChatLine::getText() {
529 return textLayout.text();
532 qreal ChatLine::layout(qreal tsw, qreal senderw, qreal textw) {
533 tsWidth = tsw; senderWidth = senderw; textWidth = textw;
535 tsLayout.beginLayout();
536 tl = tsLayout.createLine();
537 tl.setLineWidth(tsWidth);
538 tl.setPosition(QPointF(0, 0));
539 tsLayout.endLayout();
541 senderLayout.beginLayout();
542 tl = senderLayout.createLine();
543 tl.setLineWidth(senderWidth);
544 tl.setPosition(QPointF(0, 0));
545 senderLayout.endLayout();
548 textLayout.beginLayout();
550 tl = textLayout.createLine();
551 if(!tl.isValid()) break;
552 tl.setLineWidth(textWidth);
553 tl.setPosition(QPointF(0, h));
556 textLayout.endLayout();
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) {
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);