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