You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
lximage-qt-packaging/src/imageview.cpp

405 lines
12 KiB

/*
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) 2013 <copyright holder> <email>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#include "imageview.h"
#include <QWheelEvent>
#include <QPaintEvent>
#include <QPainter>
#include <QTimer>
#include <QPolygon>
#include <QStyle>
#include <QLabel>
#include <QGraphicsProxyWidget>
#include <QGraphicsSvgItem>
#define CURSOR_HIDE_DELY 3000
namespace LxImage {
ImageView::ImageView(QWidget* parent):
QGraphicsView(parent),
scene_(new GraphicsScene(this)),
imageItem_(new QGraphicsRectItem()),
gifMovie_(nullptr),
cacheTimer_(nullptr),
cursorTimer_(nullptr),
scaleFactor_(1.0),
autoZoomFit_(false),
isSVG(false) {
setViewportMargins(0, 0, 0, 0);
setContentsMargins(0, 0, 0, 0);
setLineWidth(0);
setScene(scene_);
connect(scene_, &GraphicsScene::fileDropped, this, &ImageView::onFileDropped);
imageItem_->hide();
imageItem_->setPen(QPen(Qt::NoPen)); // remove the border
scene_->addItem(imageItem_);
}
ImageView::~ImageView() {
scene_->clear(); // deletes all items
if(gifMovie_)
delete gifMovie_;
if(cacheTimer_) {
cacheTimer_->stop();
delete cacheTimer_;
}
if(cursorTimer_) {
cursorTimer_->stop();
delete cursorTimer_;
}
}
void ImageView::onFileDropped(const QString file) {
Q_EMIT fileDropped(file);
}
void ImageView::wheelEvent(QWheelEvent* event) {
int delta = event->delta();
// Ctrl key is pressed
if(event->modifiers() & Qt::ControlModifier) {
if(delta > 0) { // forward
zoomIn();
}
else { // backward
zoomOut();
}
}
else {
// The default handler QGraphicsView::wheelEvent(event) tries to
// scroll the view, which is not what we need.
// Skip the default handler and use its parent QWidget's handler here.
QWidget::wheelEvent(event);
}
}
void ImageView::mouseDoubleClickEvent(QMouseEvent* event) {
// The default behaviour of QGraphicsView::mouseDoubleClickEvent() is
// not needed for us. We call its parent class instead so the event can be
// filtered by event filter installed on the view.
// QGraphicsView::mouseDoubleClickEvent(event);
QAbstractScrollArea::mouseDoubleClickEvent(event);
}
void ImageView::mousePressEvent(QMouseEvent * event) {
QGraphicsView::mousePressEvent(event);
if(cursorTimer_) cursorTimer_->stop();
}
void ImageView::mouseReleaseEvent(QMouseEvent* event) {
QGraphicsView::mouseReleaseEvent(event);
if(cursorTimer_) cursorTimer_->start(CURSOR_HIDE_DELY);
}
void ImageView::mouseMoveEvent(QMouseEvent* event) {
QGraphicsView::mouseMoveEvent(event);
if(cursorTimer_ && (viewport()->cursor().shape() == Qt::BlankCursor
|| viewport()->cursor().shape() == Qt::OpenHandCursor)) {
cursorTimer_->start(CURSOR_HIDE_DELY); // restart timer
viewport()->setCursor(Qt::OpenHandCursor);
}
}
void ImageView::focusInEvent(QFocusEvent* event) {
QGraphicsView::focusInEvent(event);
if(cursorTimer_ && (viewport()->cursor().shape() == Qt::BlankCursor
|| viewport()->cursor().shape() == Qt::OpenHandCursor)) {
cursorTimer_->start(CURSOR_HIDE_DELY); // restart timer
viewport()->setCursor(Qt::OpenHandCursor);
}
}
void ImageView::resizeEvent(QResizeEvent* event) {
QGraphicsView::resizeEvent(event);
if(autoZoomFit_)
zoomFit();
}
void ImageView::zoomFit() {
if(!image_.isNull()) {
// if the image is smaller than our view, use its original size
// instead of scaling it up.
if(image_.width() <= width() && image_.height() <= height()) {
zoomOriginal();
return;
}
}
fitInView(scene_->sceneRect(), Qt::KeepAspectRatio);
scaleFactor_ = transform().m11();
queueGenerateCache();
}
void ImageView::zoomIn() {
autoZoomFit_ = false;
if(!image_.isNull()) {
resetTransform();
scaleFactor_ *= 1.1;
scale(scaleFactor_, scaleFactor_);
queueGenerateCache();
}
}
void ImageView::zoomOut() {
autoZoomFit_ = false;
if(!image_.isNull()) {
resetTransform();
scaleFactor_ /= 1.1;
scale(scaleFactor_, scaleFactor_);
queueGenerateCache();
}
}
void ImageView::zoomOriginal() {
resetTransform();
scaleFactor_ = 1.0;
autoZoomFit_ = false;
queueGenerateCache();
}
void ImageView::setImage(QImage image, bool show) {
if(show && (gifMovie_ || isSVG)) { // a gif animation or SVG file was shown before
scene_->clear();
isSVG = false;
if(gifMovie_) { // should be deleted explicitly
delete gifMovie_;
gifMovie_ = nullptr;
}
// recreate the rect item
imageItem_ = new QGraphicsRectItem();
imageItem_->hide();
imageItem_->setPen(QPen(Qt::NoPen));
scene_->addItem(imageItem_);
}
image_ = image;
if(image.isNull()) {
imageItem_->hide();
imageItem_->setBrush(QBrush());
scene_->setSceneRect(0, 0, 0, 0);
}
else {
if(show) {
imageItem_->setRect(0, 0, image_.width(), image_.height());
imageItem_->setBrush(image_);
imageItem_->show();
}
scene_->setSceneRect(0, 0, image_.width(), image_.height());
}
if(autoZoomFit_)
zoomFit();
queueGenerateCache();
}
void ImageView::setGifAnimation(QString fileName) {
/* the built-in gif reader gives the first frame, which won't
be shown but is used for tracking position and dimensions */
image_ = QImage(fileName);
if(image_.isNull()) {
if(imageItem_) {
imageItem_->hide();
imageItem_->setBrush(QBrush());
}
scene_->setSceneRect(0, 0, 0, 0);
}
else {
scene_->clear();
imageItem_ = nullptr; // it's deleted by clear();
if(gifMovie_) {
delete gifMovie_;
gifMovie_ = nullptr;
}
QPixmap pix(image_.size());
pix.fill(Qt::transparent);
QGraphicsItem *gifItem = new QGraphicsPixmapItem(pix);
QLabel *gifLabel = new QLabel();
gifMovie_ = new QMovie(fileName);
QGraphicsProxyWidget* gifWidget = new QGraphicsProxyWidget(gifItem);
gifLabel->setAttribute(Qt::WA_NoSystemBackground);
gifLabel->setMovie(gifMovie_);
gifWidget->setWidget(gifLabel);
gifMovie_->start();
scene_->addItem(gifItem);
scene_->setSceneRect(gifItem->boundingRect());
}
if(autoZoomFit_)
zoomFit();
queueGenerateCache(); // deletes the cache timer in this case
}
void ImageView::setSVG(QString fileName) {
image_ = QImage(fileName); // for tracking position and dimensions
if(image_.isNull()) {
if(imageItem_) {
imageItem_->hide();
imageItem_->setBrush(QBrush());
}
scene_->setSceneRect(0, 0, 0, 0);
}
else {
scene_->clear();
imageItem_ = nullptr;
isSVG = true;
QGraphicsSvgItem *svgItem = new QGraphicsSvgItem(fileName);
scene_->addItem(svgItem);
scene_->setSceneRect(svgItem->boundingRect());
}
if(autoZoomFit_)
zoomFit();
queueGenerateCache(); // deletes the cache timer in this case
}
void ImageView::setScaleFactor(double factor) {
if(factor != scaleFactor_) {
scaleFactor_ = factor;
resetTransform();
scale(factor, factor);
queueGenerateCache();
}
}
void ImageView::paintEvent(QPaintEvent* event) {
// if the image is scaled and we have a high quality cached image
if(imageItem_ && scaleFactor_ != 1.0 && !cachedPixmap_.isNull()) {
// rectangle of the whole image in viewport coordinate
QRect viewportImageRect = sceneToViewport(imageItem_->rect());
// the visible part of the image.
QRect desiredCachedRect = viewportToScene(viewportImageRect.intersected(viewport()->rect()));
// check if the cached area is what we need and if the cache is out of date
if(cachedSceneRect_ == desiredCachedRect) {
// rect of the image area that needs repaint, in viewport coordinate
QRect repaintImageRect = viewportImageRect.intersected(event->rect());
// see if the part asking for repaint is contained by our cache.
if(cachedRect_.contains(repaintImageRect)) {
QPainter painter(viewport());
painter.fillRect(event->rect(), backgroundBrush());
painter.drawPixmap(repaintImageRect, cachedPixmap_);
return;
}
}
}
if(!image_.isNull()) { // we don't have a cache yet or it's out of date already, generate one
queueGenerateCache();
}
QGraphicsView::paintEvent(event);
}
void ImageView::queueGenerateCache() {
if(!cachedPixmap_.isNull()) // clear the old pixmap if there's any
cachedPixmap_ = QPixmap();
// we don't need to cache the scaled image if its the same as the original image (scale:1.0)
// no cache for gif animations or SVG images either
if(scaleFactor_ == 1.0 || gifMovie_ || isSVG) {
if(cacheTimer_) {
cacheTimer_->stop();
delete cacheTimer_;
cacheTimer_ = nullptr;
}
return;
}
if(!cacheTimer_) {
cacheTimer_ = new QTimer();
cacheTimer_->setSingleShot(true);
connect(cacheTimer_, &QTimer::timeout, this, &ImageView::generateCache);
}
if(cacheTimer_)
cacheTimer_->start(200); // restart the timer
}
// really generate the cache
void ImageView::generateCache() {
// disable the one-shot timer
cacheTimer_->deleteLater();
cacheTimer_ = nullptr;
if(!imageItem_ || image_.isNull()) return;
// generate a cache for "the visible part" of the scaled image
// rectangle of the whole image in viewport coordinate
QRect viewportImageRect = sceneToViewport(imageItem_->rect());
// rect of the image area that's visible in the viewport (in viewport coordinate)
cachedRect_ = viewportImageRect.intersected(viewport()->rect());
// convert to the coordinate of the original image
cachedSceneRect_ = viewportToScene(cachedRect_);
// create a sub image of the visible without real data copy
// Reference: https://stackoverflow.com/questions/12681554/dividing-qimage-to-smaller-pieces
QRect subRect = image_.rect().intersected(cachedSceneRect_);
const uchar* bits = image_.constBits();
unsigned int offset = subRect.x() * image_.depth() / 8 + subRect.y() * image_.bytesPerLine();
QImage subImage = QImage(bits + offset, subRect.width(), subRect.height(), image_.bytesPerLine(), image_.format());
// If the original image has a color table, also use it for the subImage
QVector<QRgb> colorTable = image_.colorTable();
if (!colorTable.empty())
subImage.setColorTable(colorTable);
// QImage scaled = subImage.scaled(subRect.width() * scaleFactor_, subRect.height() * scaleFactor_, Qt::KeepAspectRatio, Qt::SmoothTransformation);
QImage scaled = subImage.scaled(cachedRect_.width(), cachedRect_.height(), Qt::KeepAspectRatio, Qt::SmoothTransformation);
// convert the cached scaled image to pixmap
cachedPixmap_ = QPixmap::fromImage(scaled);
viewport()->update();
}
// convert viewport coordinate to the original image (not scaled).
QRect ImageView::viewportToScene(const QRect& rect) {
// QPolygon poly = mapToScene(imageItem_->rect());
QPoint topLeft = mapToScene(rect.topLeft()).toPoint();
QPoint bottomRight = mapToScene(rect.bottomRight()).toPoint();
return QRect(topLeft, bottomRight);
}
QRect ImageView::sceneToViewport(const QRectF& rect) {
QPoint topLeft = mapFromScene(rect.topLeft());
QPoint bottomRight = mapFromScene(rect.bottomRight());
return QRect(topLeft, bottomRight);
}
void ImageView::blankCursor() {
viewport()->setCursor(Qt::BlankCursor);
}
void ImageView::hideCursor(bool enable) {
if(enable) {
if(cursorTimer_) delete cursorTimer_;
cursorTimer_ = new QTimer(this);
cursorTimer_->setSingleShot(true);
connect(cursorTimer_, &QTimer::timeout, this, &ImageView::blankCursor);
if(viewport()->cursor().shape() == Qt::OpenHandCursor)
cursorTimer_->start(CURSOR_HIDE_DELY);
}
else if (cursorTimer_) {
cursorTimer_->stop();
delete cursorTimer_;
cursorTimer_ = nullptr;
if(viewport()->cursor().shape() == Qt::BlankCursor)
viewport()->setCursor(Qt::OpenHandCursor);
}
}
} // namespace LxImage