1 // vim: set tabstop=4 shiftwidth=4 expandtab:
2 /*
3 Gwenview: an image viewer
4 Copyright 2011 Aurélien Gâteau <agateau@kde.org>
5 
6 This program is free software; you can redistribute it and/or
7 modify it under the terms of the GNU General Public License
8 as published by the Free Software Foundation; either version 2
9 of the License, or (at your option) any later version.
10 
11 This program is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 GNU General Public License for more details.
15 
16 You should have received a copy of the GNU General Public License
17 along with this program; if not, write to the Free Software
18 Foundation, Inc., 51 Franklin Street, Fifth Floor, Cambridge, MA 02110-1301, USA.
19 
20 */
21 // Self
22 #include "birdeyeview.h"
23 
24 // Local
25 #include "gwenview_lib_debug.h"
26 #include <lib/document/document.h>
27 #include <lib/documentview/documentview.h>
28 
29 // KF
30 
31 // Qt
32 #include <QApplication>
33 #include <QGraphicsSceneEvent>
34 #include <QPainter>
35 #include <QPropertyAnimation>
36 #include <QTimer>
37 
38 namespace Gwenview
39 {
40 static qreal MIN_SIZE = 72;
41 static qreal VIEW_OFFSET = MIN_SIZE / 4;
42 
43 static int AUTOHIDE_DELAY = 2000;
44 
45 /**
46  * Returns a QRectF whose coordinates are rounded to completely contains rect
47  */
alignedRectF(const QRectF & rect)48 inline QRectF alignedRectF(const QRectF &rect)
49 {
50     return QRectF(rect.toAlignedRect());
51 }
52 
53 struct BirdEyeViewPrivate {
54     BirdEyeView *q;
55     DocumentView *mDocView;
56     QPropertyAnimation *mOpacityAnim;
57     QTimer *mAutoHideTimer;
58     QRectF mVisibleRect;
59     QPointF mStartDragMousePos;
60     QPointF mStartDragViewPos;
61 
updateCursorGwenview::BirdEyeViewPrivate62     void updateCursor(const QPointF &pos)
63     {
64         q->setCursor(mVisibleRect.contains(pos) ? Qt::OpenHandCursor : Qt::ArrowCursor);
65     }
66 
updateVisibilityGwenview::BirdEyeViewPrivate67     void updateVisibility()
68     {
69         bool visible;
70         if (!mDocView->canZoom() || mDocView->zoomToFit()) {
71             // No need to show
72             visible = false;
73         } else if (mDocView->isAnimated()) {
74             // Do not show while animated
75             visible = false;
76         } else if (mVisibleRect == q->boundingRect()) {
77             // All of the image is visible
78             visible = false;
79         } else if (q->isUnderMouse() || !mStartDragMousePos.isNull()) {
80             // User is interacting or about to interact with birdeyeview
81             visible = true;
82         } else if (mAutoHideTimer->isActive()) {
83             // User triggered some activity recently (move mouse, scroll, zoom)
84             visible = true;
85         } else {
86             // No recent activity
87             visible = false;
88         }
89         qreal wantedOpacity = visible ? 1 : 0;
90         if (!qFuzzyCompare(wantedOpacity, q->opacity())) {
91             mOpacityAnim->setEndValue(wantedOpacity);
92             mOpacityAnim->start();
93         }
94 
95         if (visible) {
96             mAutoHideTimer->start();
97         }
98     }
99 };
100 
BirdEyeView(DocumentView * docView)101 BirdEyeView::BirdEyeView(DocumentView *docView)
102     : QGraphicsWidget(docView)
103     , d(new BirdEyeViewPrivate)
104 {
105     d->q = this;
106     d->mDocView = docView;
107     setFlag(ItemIsSelectable);
108     setCursor(Qt::ArrowCursor);
109     setAcceptHoverEvents(true);
110 
111     d->mOpacityAnim = new QPropertyAnimation(this, "opacity", this);
112 
113     d->mAutoHideTimer = new QTimer(this);
114     d->mAutoHideTimer->setSingleShot(true);
115     d->mAutoHideTimer->setInterval(AUTOHIDE_DELAY);
116     connect(d->mAutoHideTimer, &QTimer::timeout, this, &BirdEyeView::slotAutoHideTimeout);
117 
118     // Hide ourself by default, to avoid startup flashes (if we let updateOpacity
119     // update opacity, it will do so through an animation)
120     setOpacity(0);
121     slotZoomOrSizeChanged();
122 
123     connect(docView->document().data(), &Document::metaInfoUpdated, this, &BirdEyeView::slotZoomOrSizeChanged);
124     connect(docView, &DocumentView::zoomChanged, this, &BirdEyeView::slotZoomOrSizeChanged);
125     connect(docView, &DocumentView::zoomToFitChanged, this, &BirdEyeView::slotZoomOrSizeChanged);
126     connect(docView, &DocumentView::positionChanged, this, &BirdEyeView::slotPositionChanged);
127 }
128 
~BirdEyeView()129 BirdEyeView::~BirdEyeView()
130 {
131     delete d;
132 }
133 
adjustGeometry()134 void BirdEyeView::adjustGeometry()
135 {
136     if (!d->mDocView->canZoom() || d->mDocView->zoomToFit()) {
137         return;
138     }
139     QSizeF size = d->mDocView->document()->size() / qApp->devicePixelRatio();
140     size.scale(MIN_SIZE, MIN_SIZE, Qt::KeepAspectRatioByExpanding);
141     QRectF docViewRect = d->mDocView->boundingRect();
142     int maxBevHeight = docViewRect.height() - 2 * VIEW_OFFSET;
143     int maxBevWidth = docViewRect.width() - 2 * VIEW_OFFSET;
144     if (size.height() > maxBevHeight) {
145         size.scale(MIN_SIZE, maxBevHeight, Qt::KeepAspectRatio);
146     }
147     if (size.width() > maxBevWidth) {
148         size.scale(maxBevWidth, MIN_SIZE, Qt::KeepAspectRatio);
149     }
150     QRectF geom = QRectF(QApplication::isRightToLeft() ? docViewRect.left() + VIEW_OFFSET : docViewRect.right() - VIEW_OFFSET - size.width(),
151                          docViewRect.bottom() - VIEW_OFFSET - size.height(),
152                          size.width(),
153                          size.height());
154     setGeometry(alignedRectF(geom));
155     adjustVisibleRect();
156 }
157 
adjustVisibleRect()158 void BirdEyeView::adjustVisibleRect()
159 {
160     QSizeF docSize = d->mDocView->document()->size() / qApp->devicePixelRatio();
161     qreal viewZoom = d->mDocView->zoom();
162     qreal bevZoom;
163     if (docSize.height() > docSize.width()) {
164         bevZoom = size().height() / docSize.height();
165     } else {
166         bevZoom = size().width() / docSize.width();
167     }
168 
169     if (qFuzzyIsNull(viewZoom) || qFuzzyIsNull(bevZoom)) {
170         // Prevent divide-by-zero crashes
171         return;
172     }
173 
174     QRectF rect = QRectF(QPointF(d->mDocView->position()) / viewZoom * bevZoom, (d->mDocView->size() / viewZoom).boundedTo(docSize) * bevZoom);
175     d->mVisibleRect = rect;
176 }
177 
slotAutoHideTimeout()178 void BirdEyeView::slotAutoHideTimeout()
179 {
180     d->updateVisibility();
181 }
182 
slotZoomOrSizeChanged()183 void BirdEyeView::slotZoomOrSizeChanged()
184 {
185     if (!d->mDocView->canZoom() || d->mDocView->zoomToFit()) {
186         d->updateVisibility();
187         return;
188     }
189     adjustGeometry();
190     update();
191     d->mAutoHideTimer->start();
192     d->updateVisibility();
193 }
194 
slotPositionChanged()195 void BirdEyeView::slotPositionChanged()
196 {
197     adjustVisibleRect();
198     update();
199     d->mAutoHideTimer->start();
200     d->updateVisibility();
201 }
202 
drawTransparentRect(QPainter * painter,const QRectF & rect,const QColor & color)203 inline void drawTransparentRect(QPainter *painter, const QRectF &rect, const QColor &color)
204 {
205     QColor bg = color;
206     bg.setAlphaF(.33);
207     QColor fg = color;
208     fg.setAlphaF(.66);
209     painter->setPen(fg);
210     painter->setBrush(bg);
211     painter->drawRect(rect.adjusted(0, 0, -1, -1));
212 }
213 
paint(QPainter * painter,const QStyleOptionGraphicsItem *,QWidget *)214 void BirdEyeView::paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *)
215 {
216     static const QColor bgColor = QColor::fromHsvF(0, 0, .33);
217     drawTransparentRect(painter, boundingRect(), bgColor);
218     drawTransparentRect(painter, d->mVisibleRect, Qt::white);
219 }
220 
onMouseMoved()221 void BirdEyeView::onMouseMoved()
222 {
223     d->mAutoHideTimer->start();
224     d->updateVisibility();
225 }
226 
slotIsAnimatedChanged()227 void BirdEyeView::slotIsAnimatedChanged()
228 {
229     d->updateVisibility();
230 }
231 
mousePressEvent(QGraphicsSceneMouseEvent * event)232 void BirdEyeView::mousePressEvent(QGraphicsSceneMouseEvent *event)
233 {
234     if (d->mVisibleRect.contains(event->pos()) && event->button() == Qt::LeftButton) {
235         setCursor(Qt::ClosedHandCursor);
236         d->mStartDragMousePos = event->pos();
237         d->mStartDragViewPos = d->mDocView->position();
238     }
239 }
240 
mouseMoveEvent(QGraphicsSceneMouseEvent * event)241 void BirdEyeView::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
242 {
243     QGraphicsItem::mouseMoveEvent(event);
244     if (d->mStartDragMousePos.isNull()) {
245         // Do not drag if mouse was pressed outside visible rect
246         return;
247     }
248     qreal ratio = qMin(d->mDocView->boundingRect().height() / d->mVisibleRect.height(), d->mDocView->boundingRect().width() / d->mVisibleRect.width());
249     QPointF mousePos = event->pos();
250     QPointF viewPos = d->mStartDragViewPos + (mousePos - d->mStartDragMousePos) * ratio;
251 
252     d->mDocView->setPosition(viewPos.toPoint());
253 }
254 
mouseReleaseEvent(QGraphicsSceneMouseEvent * event)255 void BirdEyeView::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
256 {
257     QGraphicsItem::mouseReleaseEvent(event);
258     if (d->mStartDragMousePos.isNull()) {
259         return;
260     }
261     d->updateCursor(event->pos());
262     d->mStartDragMousePos = QPointF();
263     d->mAutoHideTimer->start();
264 }
265 
hoverEnterEvent(QGraphicsSceneHoverEvent *)266 void BirdEyeView::hoverEnterEvent(QGraphicsSceneHoverEvent * /*event*/)
267 {
268     d->mAutoHideTimer->stop();
269     d->updateVisibility();
270 }
271 
hoverMoveEvent(QGraphicsSceneHoverEvent * event)272 void BirdEyeView::hoverMoveEvent(QGraphicsSceneHoverEvent *event)
273 {
274     if (d->mStartDragMousePos.isNull()) {
275         d->updateCursor(event->pos());
276     }
277 }
278 
hoverLeaveEvent(QGraphicsSceneHoverEvent *)279 void BirdEyeView::hoverLeaveEvent(QGraphicsSceneHoverEvent * /*event*/)
280 {
281     d->mAutoHideTimer->start();
282     d->updateVisibility();
283 }
284 
285 } // namespace
286