Vidalia  0.2.21
ZImageView.cpp
Go to the documentation of this file.
1 /*
2 ** This file is part of Vidalia, and is subject to the license terms in the
3 ** LICENSE file, found in the top level directory of this distribution. If you
4 ** did not receive the LICENSE file with this file, you may obtain it from the
5 ** Vidalia source package distributed by the Vidalia Project at
6 ** http://www.torproject.org/projects/vidalia.html. No part of Vidalia,
7 ** including this file, may be copied, modified, propagated, or distributed
8 ** except according to the terms described in the LICENSE file.
9 */
10 
11 /*
12 ** \file ZImageView.cpp
13 ** \brief Displays an image and allows zooming and panning
14 */
15 
16 #include "ZImageView.h"
17 
18 #include <QPainter>
19 #include <QMouseEvent>
20 
21 #include <cmath>
22 
23 #if QT_VERSION >= 0x040200
24 #define CURSOR_NORMAL QCursor(Qt::OpenHandCursor)
25 #define CURSOR_MOUSE_PRESS QCursor(Qt::ClosedHandCursor)
26 #else
27 #define CURSOR_NORMAL QCursor(Qt::CrossCursor)
28 #define CURSOR_MOUSE_PRESS QCursor(Qt::SizeAllCursor)
29 #endif
30 
31 
32 /** Constructor. */
33 ZImageView::ZImageView(QWidget *parent)
34  : QWidget(parent)
35 {
36  /* Initialize members */
37  _zoom = 0.0;
38  _desiredX = 0.0;
39  _desiredY = 0.0;
40  _maxZoomFactor = 2.0;
41  _padding = 60;
42 
43  setCursor(CURSOR_NORMAL);
46  repaint();
47 }
48 
49 /** Sets the displayed image. */
50 void
52 {
53  _image = img.copy();
56 
57  if (isVisible()) {
58  repaint();
59  }
60 }
61 
62 /** Draws the scaled image on the widget. */
63 void
65 {
66  if (!isVisible()) {
67  return;
68  }
69 
70  QBrush background(QColor("#fdfdfd"));
71  if (_image.isNull()) {
72  QPainter p(this);
73  p.fillRect(rect(), background);
74  return;
75  }
76 
77  QRect sRect = rect();
78  QRect iRect = _image.rect();
79  QRect r = _view;
80 
81  // Think of the _view as being overlaid on the image. The _view has the same
82  // aspect ratio as the screen, so we cut the _view region out of the _image
83  // and scale it to the screen dimensions and paint it.
84 
85  // There is a slight catch in that the _view may be larger than the image in
86  // one or both directions. In that case, we need to reduce the _view region
87  // to lie within the image, then paint the background around it. Copying
88  // a region from an image where the region is bigger than the image results
89  // in the parts outside the image being black, which is not what we want.
90 
91  // The view has the same aspect ratio as the screen, so the vertical and
92  // horizontal scale factors will be equal.
93 
94  double scaleFactor = double(sRect.width()) / double(_view.width());
95 
96  // Constrain r to lie entirely within the image.
97  if (r.top() < 0) {
98  r.setTop(0);
99  }
100  if (iRect.bottom() < r.bottom()) {
101  r.setBottom(iRect.bottom());
102  }
103  if (r.left() < 0) {
104  r.setLeft(0);
105  }
106  if (iRect.right() < r.right()) {
107  r.setRight(iRect.right());
108  }
109 
110  // Figure out the size that the 'r' region will be when drawn to the screen.
111  QSize scaleTo(int(double(r.width()) * scaleFactor),
112  int(double(r.height()) * scaleFactor));
113 
114  /** Make a copy of the image so we don't ruin the original */
115  QImage i = _image.copy();
116 
117  /** Create a QPainter that draws directly on the copied image and call the
118  * virtual function to draw whatever the subclasses need to on the image. */
119  QPainter painter;
120  painter.begin(&i);
121  paintImage(&painter);
122  painter.end();
123 
124  /** Rescale the image copy */
125  i = i.copy(r).scaled(scaleTo,
126  Qt::KeepAspectRatioByExpanding,
127  Qt::SmoothTransformation);
128 
129  int extraWidth = int(double(sRect.width() - i.width()) / 2.0);
130  int extraHeight = int(double(sRect.height() - i.height()) / 2.0);
131 
132  // We don't want to paint the background
133  // because this isn't double buffered and that would flicker.
134  // We could double buffer it, but that would cost ~3 MB of memory.
135 
136  QPainter p(this);
137  if (extraWidth > 0) {
138  p.fillRect(0, 0, extraWidth, sRect.height(), background);
139  p.fillRect(sRect.width() - extraWidth, 0,
140  sRect.width(), sRect.height(), background);
141  }
142 
143  if (extraHeight > 0) {
144  p.fillRect(0, 0, sRect.width(), extraHeight, background);
145  p.fillRect(0, sRect.height() - extraHeight,
146  sRect.width(), sRect.height(), background);
147  }
148 
149  // Finally, paint the image copy.
150  p.drawImage(extraWidth, extraHeight, i);
151 }
152 
153 /** Updates the displayed viewport. */
154 void
155 ZImageView::updateViewport(int screendx, int screendy)
156 {
157  /* The gist of this is to find the biggest and smallest possible viewports,
158  * then use the _zoom factor to interpolate between them. Also pan the
159  * viewport, but constrain each dimension to lie within the image or to be
160  * centered if the image is too small in that direction. */
161 
162  QRect sRect = rect();
163  QRect iRect = _image.rect();
164 
165  float sw = float(sRect.width());
166  float sh = float(sRect.height());
167  float iw = float(iRect.width());
168  float ih = float(iRect.height());
169 
170  // Get the initial max and min sizes for the viewport. These won't have the
171  // correct aspect ratio. They will actually be the least upper bound and
172  // greatest lower bound of the set containing the screen and image rects.
173  float maxw = float(std::max<int>(sRect.width(), iRect.width())) + _padding;
174  float maxh = float(std::max<int>(sRect.height(), iRect.height())) + _padding;
175  float minw = std::ceil(float(sRect.width()) / _maxZoomFactor);
176  float minh = std::ceil(float(sRect.height()) / _maxZoomFactor);
177 
178  // Now that we have the glb and the lub, we expand/shrink them until
179  // the aspect ratio is that of the screen.
180  float aspect = sw / sh;
181 
182  // Fix the max rect.
183  float newmaxh = maxh;
184  float newmaxw = aspect * newmaxh;
185  if (newmaxw < maxw) {
186  newmaxw = maxw;
187  newmaxh = maxw / aspect;
188  }
189 
190  // Fix the min rect.
191  float newminh = minh;
192  float newminw = aspect * newminh;
193  if (minw < newminw) {
194  newminw = minw;
195  newminh = newminw / aspect;
196  }
197 
198  // Now interpolate between max and min.
199  float vw = (1.0f - _zoom) * (newmaxw - newminw) + newminw;
200  float vh = (1.0f - _zoom) * (newmaxh - newminh) + newminh;
201 
202  _view.setWidth(int(vw));
203  _view.setHeight(int(vh));
204 
205  // Now pan the view
206 
207  // Convert the pan delta from screen coordinates to view coordinates.
208  float vdx = vw * (float(screendx) / sw);
209  float vdy = vh * (float(screendy) / sh);
210 
211  // Constrain the center of the viewport to the image rect.
212  _desiredX = qBound(0.0f, _desiredX + vdx, iw);
213  _desiredY = qBound(0.0f, _desiredY + vdy, ih);
214  _view.moveCenter(QPoint(int(_desiredX), int(_desiredY)));
215 
216  QPoint viewCenter = _view.center();
217  float vx = viewCenter.x();
218  float vy = viewCenter.y();
219 
220  // The viewport may be wider than the height and/or width. In that case,
221  // center the view over the image in the appropriate directions.
222  //
223  // If the viewport is smaller than the image in either direction, then make
224  // sure the edge of the viewport isn't past the edge of the image.
225 
226  vdx = 0;
227  vdy = 0;
228 
229  if (iw <= vw) {
230  vdx = (iw / 2.0f) - vx; // Center horizontally.
231  } else {
232  // Check that the edge of the view isn't past the edge of the image.
233  float vl = float(_view.left());
234  float vr = float(_view.right());
235  if (vl < 0) {
236  vdx = -vl;
237  } else if (vr > iw) {
238  vdx = iw - vr;
239  }
240  }
241 
242  if (ih <= vh) {
243  vdy = (ih / 2.0f) - vy; // Center vertically.
244  } else {
245  // Check that the edge of the view isn't past the edge of the image.
246  float vt = float(_view.top());
247  float vb = float(_view.bottom());
248  if (vt < 0) {
249  vdy = -vt;
250  } else if (vb > ih) {
251  vdy = ih - vb;
252  }
253  }
254 
255  _view.translate(int(vdx), int(vdy));
256 }
257 
258 /** Resets the zoom point back to the center of the viewport. */
259 void
261 {
262  QPoint viewCenter = _view.center();
263  _desiredX = viewCenter.x();
264  _desiredY = viewCenter.y();
265 }
266 
267 /** Handles repainting this widget by updating the viewport and drawing the
268  * scaled image. */
269 void
271 {
272  updateViewport();
273  drawScaledImage();
274 }
275 
276 /** Sets the current zoom percentage to the given value and scrolls the
277  * viewport to center the given point. */
278 void
279 ZImageView::zoom(QPoint zoomAt, float pct)
280 {
281  _desiredX = zoomAt.x();
282  _desiredY = zoomAt.y();
283  zoom(pct);
284 }
285 
286 /** Sets the current zoom percentage to the given value. */
287 void
289 {
290  _zoom = qBound(0.0f, pct, 1.0f);
291  repaint();
292 }
293 
294 /** Zooms into the image by 10% */
295 void
297 {
298  zoom(_zoom + .1);
299 }
300 
301 /** Zooms away from the image by 10% */
302 void
304 {
305  zoom(_zoom - .1);
306 }
307 
308 /** Responds to the user pressing a mouse button. */
309 void
311 {
312  e->accept();
313  setCursor(CURSOR_MOUSE_PRESS);
314  _mouseX = e->x();
315  _mouseY = e->y();
316 }
317 
318 /** Responds to the user releasing a mouse button. */
319 void
321 {
322  e->accept();
323  setCursor(CURSOR_NORMAL);
324  updateViewport();
325  resetZoomPoint();
326 }
327 
328 /** Responds to the user double-clicking a mouse button on the image. A left
329  * double-click zooms in on the image and a right double-click zooms out.
330  * Zooming is centered on the location of the double-click. */
331 void
333 {
334  e->accept();
335 
336  QPoint center = rect().center();
337  int dx = e->x() - center.x();
338  int dy = e->y() - center.y();
339  updateViewport(dx, dy);
340  resetZoomPoint();
341 
342  Qt::MouseButton btn = e->button();
343  if (btn == Qt::LeftButton)
344  zoomIn();
345  else if (btn == Qt::RightButton)
346  zoomOut();
347 }
348 
349 /** Responds to the user moving the mouse. */
350 void
352 {
353  e->accept();
354  int dx = _mouseX - e->x();
355  int dy = _mouseY - e->y();
356  _mouseX = e->x();
357  _mouseY = e->y();
358 
359  updateViewport(dx, dy);
360  if (0.001 <= _zoom) {
361  repaint();
362  }
363 }
364 
365 void
366 ZImageView::wheelEvent(QWheelEvent *e)
367 {
368  if (e->delta() > 0) {
369  zoomIn();
370  } else {
371  zoomOut();
372  }
373 }