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.
405 lines
12 KiB
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
|