Refactor the markerline into a proper QGraphicsWidget
[quassel.git] / src / qtui / chatview.cpp
1 /***************************************************************************
2  *   Copyright (C) 2005-09 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 <QGraphicsTextItem>
22 #include <QKeyEvent>
23 #include <QMenu>
24 #include <QScrollBar>
25
26 #include "bufferwidget.h"
27 #include "chatscene.h"
28 #include "chatview.h"
29 #include "client.h"
30 #include "messagefilter.h"
31 #include "qtui.h"
32 #include "qtuistyle.h"
33 #include "clientignorelistmanager.h"
34
35 #include "chatline.h"
36
37 ChatView::ChatView(BufferId bufferId, QWidget *parent)
38   : QGraphicsView(parent),
39     AbstractChatView()
40 {
41   QList<BufferId> filterList;
42   filterList.append(bufferId);
43   MessageFilter *filter = new MessageFilter(Client::messageModel(), filterList, this);
44   init(filter);
45 }
46
47 ChatView::ChatView(MessageFilter *filter, QWidget *parent)
48   : QGraphicsView(parent),
49     AbstractChatView()
50 {
51   init(filter);
52 }
53
54 void ChatView::init(MessageFilter *filter) {
55   _bufferContainer = 0;
56   _currentScaleFactor = 1;
57   _invalidateFilter = false;
58
59   setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
60   setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
61   setAlignment(Qt::AlignLeft|Qt::AlignBottom);
62   setInteractive(true);
63   //setOptimizationFlags(QGraphicsView::DontClipPainter | QGraphicsView::DontAdjustForAntialiasing);
64   // setOptimizationFlags(QGraphicsView::DontAdjustForAntialiasing);
65   setViewportUpdateMode(QGraphicsView::BoundingRectViewportUpdate);
66   // setTransformationAnchor(QGraphicsView::NoAnchor);
67   setTransformationAnchor(QGraphicsView::AnchorViewCenter);
68
69   _scrollTimer.setInterval(100);
70   _scrollTimer.setSingleShot(true);
71   connect(&_scrollTimer, SIGNAL(timeout()), SLOT(scrollTimerTimeout()));
72
73   _scene = new ChatScene(filter, filter->idString(), viewport()->width(), this);
74   connect(_scene, SIGNAL(sceneRectChanged(const QRectF &)), this, SLOT(adjustSceneRect()));
75   connect(_scene, SIGNAL(lastLineChanged(QGraphicsItem *, qreal)), this, SLOT(lastLineChanged(QGraphicsItem *, qreal)));
76   connect(_scene, SIGNAL(mouseMoveWhileSelecting(const QPointF &)), this, SLOT(mouseMoveWhileSelecting(const QPointF &)));
77   setScene(_scene);
78
79   connect(verticalScrollBar(), SIGNAL(valueChanged(int)), this, SLOT(verticalScrollbarChanged(int)));
80   _lastScrollbarPos = verticalScrollBar()->value();
81
82   connect(Client::networkModel(), SIGNAL(markerLineSet(BufferId,MsgId)), SLOT(markerLineSet(BufferId,MsgId)));
83
84   // only connect if client is synched with a core
85   if(Client::isConnected())
86     connect(Client::ignoreListManager(), SIGNAL(ignoreListChanged()), this, SLOT(invalidateFilter()));
87 }
88
89 bool ChatView::event(QEvent *event) {
90   if(event->type() == QEvent::KeyPress) {
91     QKeyEvent *keyEvent = static_cast<QKeyEvent*>(event);
92     switch(keyEvent->key()) {
93     case Qt::Key_Up:
94     case Qt::Key_Down:
95     case Qt::Key_PageUp:
96     case Qt::Key_PageDown:
97       if(!verticalScrollBar()->isVisible()) {
98         scene()->requestBacklog();
99         return true;
100       }
101     default:
102       break;
103     }
104   }
105
106   if(event->type() == QEvent::Wheel) {
107     if(!verticalScrollBar()->isVisible()) {
108       scene()->requestBacklog();
109       return true;
110     }
111   }
112
113   if(event->type() == QEvent::Show) {
114     if(_invalidateFilter)
115       invalidateFilter();
116   }
117
118   return QGraphicsView::event(event);
119 }
120
121 void ChatView::resizeEvent(QResizeEvent *event) {
122   QGraphicsView::resizeEvent(event);
123
124   // FIXME: do we really need to scroll down on resize?
125
126   // we can reduce viewport updates if we scroll to the bottom allready at the beginning
127   verticalScrollBar()->setValue(verticalScrollBar()->maximum());
128   scene()->updateForViewport(viewport()->width(), viewport()->height());
129   adjustSceneRect();
130
131   _lastScrollbarPos = verticalScrollBar()->maximum();
132   verticalScrollBar()->setValue(verticalScrollBar()->maximum());
133
134   checkChatLineCaches();
135 }
136
137 void ChatView::adjustSceneRect() {
138   // Workaround for QTBUG-6322
139   // If the viewport's sceneRect() is (almost) as wide as as the viewport itself,
140   // Qt wants to reserve space for scrollbars even if they're turned off, resulting in
141   // an ugly white space at the bottom of the ChatView.
142   // Since the view's scene's width actually doesn't matter at all, we just adjust it
143   // by some hopefully large enough value to avoid this problem.
144
145   setSceneRect(scene()->sceneRect().adjusted(0, 0, -25 ,0));
146 }
147
148 void ChatView::mouseMoveWhileSelecting(const QPointF &scenePos) {
149   int y = (int)mapFromScene(scenePos).y();
150   _scrollOffset = 0;
151   if(y < 0)
152     _scrollOffset = y;
153   else if(y > height())
154     _scrollOffset = y - height();
155
156   if(_scrollOffset && !_scrollTimer.isActive())
157     _scrollTimer.start();
158 }
159
160 void ChatView::scrollTimerTimeout() {
161   // scroll view
162   QAbstractSlider *vbar = verticalScrollBar();
163   if(_scrollOffset < 0 && vbar->value() > 0)
164     vbar->setValue(qMax(vbar->value() + _scrollOffset, 0));
165   else if(_scrollOffset > 0 && vbar->value() < vbar->maximum())
166     vbar->setValue(qMin(vbar->value() + _scrollOffset, vbar->maximum()));
167 }
168
169 void ChatView::lastLineChanged(QGraphicsItem *chatLine, qreal offset) {
170   Q_UNUSED(chatLine)
171   // disabled until further testing/discussion
172   //if(!scene()->isScrollingAllowed())
173   //  return;
174
175   QAbstractSlider *vbar = verticalScrollBar();
176   Q_ASSERT(vbar);
177   if(vbar->maximum() - vbar->value() <= (offset + 5) * _currentScaleFactor ) { // 5px grace area
178     vbar->setValue(vbar->maximum());
179   }
180 }
181
182 void ChatView::verticalScrollbarChanged(int newPos) {
183   QAbstractSlider *vbar = verticalScrollBar();
184   Q_ASSERT(vbar);
185
186   // check for backlog request
187   if(newPos < _lastScrollbarPos) {
188     int relativePos = 100;
189     if(vbar->maximum() - vbar->minimum() != 0)
190       relativePos = (newPos - vbar->minimum()) * 100 / (vbar->maximum() - vbar->minimum());
191
192     if(relativePos < 20) {
193       scene()->requestBacklog();
194     }
195   }
196   _lastScrollbarPos = newPos;
197
198   // FIXME: Fugly workaround for the ChatView scrolling up 1px on buffer switch
199   if(vbar->maximum() - newPos <= 2)
200     vbar->setValue(vbar->maximum());
201 }
202
203 MsgId ChatView::lastMsgId() const {
204   if(!scene())
205     return MsgId();
206
207   QAbstractItemModel *model = scene()->model();
208   if(!model || model->rowCount() == 0)
209     return MsgId();
210
211   return model->index(model->rowCount() - 1, 0).data(MessageModel::MsgIdRole).value<MsgId>();
212 }
213
214 MsgId ChatView::lastVisibleMsgId() const {
215   ChatLine *line = lastVisibleChatLine();
216
217   if(line)
218     return line->msgId();
219
220   return MsgId();
221 }
222
223 bool chatLinePtrLessThan(ChatLine *one, ChatLine *other) {
224   return one->row() < other->row();
225 }
226
227 // TODO: figure out if it's cheaper to use a cached list (that we'd need to keep updated)
228 QSet<ChatLine *> ChatView::visibleChatLines(Qt::ItemSelectionMode mode) const {
229   QSet<ChatLine *> result;
230   foreach(QGraphicsItem *item, items(viewport()->rect().adjusted(-1, -1, 1, 1), mode)) {
231     ChatLine *line = qgraphicsitem_cast<ChatLine *>(item);
232     if(line)
233       result.insert(line);
234   }
235   return result;
236 }
237
238 QList<ChatLine *> ChatView::visibleChatLinesSorted(Qt::ItemSelectionMode mode) const {
239   QList<ChatLine *> result = visibleChatLines(mode).toList();
240   qSort(result.begin(), result.end(), chatLinePtrLessThan);
241   return result;
242 }
243
244 ChatLine *ChatView::lastVisibleChatLine() const {
245   if(!scene())
246     return 0;
247
248   QAbstractItemModel *model = scene()->model();
249   if(!model || model->rowCount() == 0)
250     return 0;
251
252   int row = -1;
253
254   QSet<ChatLine *> visibleLines = visibleChatLines(Qt::ContainsItemBoundingRect);
255   foreach(ChatLine *line, visibleLines) {
256     if(line->row() > row)
257       row = line->row();
258   }
259
260   if(row >= 0)
261     return scene()->chatLine(row);
262
263   return 0;
264 }
265
266 void ChatView::setMarkerLineVisible(bool visible) {
267   scene()->setMarkerLineVisible(visible);
268 }
269
270 void ChatView::setMarkerLine(MsgId msgId) {
271   if(!scene()->isSingleBufferScene())
272     return;
273
274   BufferId bufId = scene()->singleBufferId();
275   Client::setMarkerLine(bufId, msgId);
276 }
277
278 void ChatView::markerLineSet(BufferId buffer, MsgId msgId) {
279   if(!scene()->isSingleBufferScene() || scene()->singleBufferId() != buffer)
280     return;
281
282   scene()->setMarkerLine(msgId);
283   scene()->setMarkerLineVisible(true);
284 }
285
286 void ChatView::addActionsToMenu(QMenu *menu, const QPointF &pos) {
287   // zoom actions
288   BufferWidget *bw = qobject_cast<BufferWidget *>(bufferContainer());
289   if(bw) {
290     bw->addActionsToMenu(menu, pos);
291     menu->addSeparator();
292   }
293 }
294
295 void ChatView::zoomIn() {
296     _currentScaleFactor *= 1.2;
297     scale(1.2, 1.2);
298     scene()->setWidth(viewport()->width() / _currentScaleFactor - 2);
299 }
300
301 void ChatView::zoomOut() {
302     _currentScaleFactor /= 1.2;
303     scale(1 / 1.2, 1 / 1.2);
304     scene()->setWidth(viewport()->width() / _currentScaleFactor - 2);
305 }
306
307 void ChatView::zoomOriginal() {
308     scale(1/_currentScaleFactor, 1/_currentScaleFactor);
309     _currentScaleFactor = 1;
310     scene()->setWidth(viewport()->width() - 2);
311 }
312
313 void ChatView::invalidateFilter() {
314   // if this is the currently selected chatview
315   // invalidate immediately
316   if(isVisible()) {
317     _scene->filter()->invalidateFilter();
318     _invalidateFilter = false;
319   }
320   // otherwise invalidate whenever the view is shown
321   else {
322     _invalidateFilter = true;
323   }
324 }
325
326 void ChatView::scrollContentsBy(int dx, int dy) {
327   QGraphicsView::scrollContentsBy(dx, dy);
328   checkChatLineCaches();
329 }
330
331 void ChatView::setHasCache(ChatLine *line, bool hasCache) {
332   if(hasCache)
333     _linesWithCache.insert(line);
334   else
335     _linesWithCache.remove(line);
336 }
337
338 void ChatView::checkChatLineCaches() {
339   qreal top = mapToScene(viewport()->rect().topLeft()).y() - 10; // some grace area to avoid premature cleaning
340   qreal bottom = mapToScene(viewport()->rect().bottomRight()).y() + 10;
341   QSet<ChatLine *>::iterator iter = _linesWithCache.begin();
342   while(iter != _linesWithCache.end()) {
343     ChatLine *line = *iter;
344     if(line->pos().y() + line->height() < top || line->pos().y() > bottom) {
345       line->clearCache();
346       iter = _linesWithCache.erase(iter);
347     } else
348       ++iter;
349   }
350 }