harbour-books/app/src/BooksCoverWidget.cpp
2018-05-16 18:27:12 +03:00

490 lines
14 KiB
C++

/*
* Copyright (C) 2015-2018 Jolla Ltd.
* Copyright (C) 2015-2018 Slava Monich <slava.monich@jolla.com>
*
* You may use this file under the terms of the BSD license as follows:
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in
* the documentation and/or other materials provided with the
* distribution.
* * Neither the name of Nemo Mobile nor the names of its contributors
* may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "BooksCoverWidget.h"
#include "HarbourDebug.h"
#include "HarbourTask.h"
#include "ZLibrary.h"
#include <QPainter>
// ==========================================================================
// BooksCoverWidget::ScaleTask
// ==========================================================================
class BooksCoverWidget::ScaleTask : public HarbourTask
{
public:
ScaleTask(QThreadPool* aPool, QImage aImage, int aWidth, int aHeight,
bool aStretch);
static QImage scale(QImage aImage, int aWidth, int aHeight, bool aStretch);
void performTask();
public:
QImage iImage;
QImage iScaledImage;
int iWidth;
int iHeight;
bool iStretch;
};
BooksCoverWidget::ScaleTask::ScaleTask(QThreadPool* aPool, QImage aImage,
int aWidth, int aHeight, bool aStretch) : HarbourTask(aPool),
iImage(aImage), iWidth(aWidth), iHeight(aHeight), iStretch(aStretch)
{
}
QImage BooksCoverWidget::ScaleTask::scale(QImage aImage,
int aWidth, int aHeight, bool aStretch)
{
if (aStretch){
return aImage.scaled(aWidth, aHeight, Qt::KeepAspectRatioByExpanding,
Qt::SmoothTransformation);
} else {
return (aWidth*aImage.height() > aImage.width()*aHeight) ?
aImage.scaledToHeight(aHeight, Qt::SmoothTransformation) :
aImage.scaledToWidth(aWidth, Qt::SmoothTransformation);
}
}
void BooksCoverWidget::ScaleTask::performTask()
{
if (!iImage.isNull() && !isCanceled()) {
iScaledImage = scale(iImage, iWidth, iHeight, iStretch);
}
}
// ==========================================================================
// BooksCoverWidget::DefaultImage
// ==========================================================================
// Image shared by all items in the bookshelf view
class BooksCoverWidget::DefaultImage
{
public:
static QImage scaled(int aWidth, int aHeight);
static QImage* retain();
static void release(QImage* aImage);
private:
static const char* iImageName;
static QImage* iImage;
static QImage* iScaledImage;
static int iRefCount;
static bool iMissing;
};
const char* BooksCoverWidget::DefaultImage::iImageName = "default-cover.jpg";
QImage* BooksCoverWidget::DefaultImage::iImage = NULL;
QImage* BooksCoverWidget::DefaultImage::iScaledImage = NULL;
int BooksCoverWidget::DefaultImage::iRefCount = 0;
bool BooksCoverWidget::DefaultImage::iMissing = false;
QImage* BooksCoverWidget::DefaultImage::retain()
{
if (!iImage && !iMissing) {
QString path(QString::fromStdString(
ZLibrary::DefaultFilesPathPrefix() + iImageName));
iImage = new QImage(path);
if (iImage->isNull() || !iImage->width() || !iImage->height()) {
HWARN("Failed to load" << qPrintable(path));
delete iImage;
iImage = NULL;
iMissing = true;
} else {
HDEBUG("loaded" << qPrintable(path));
}
}
if (iImage) {
iRefCount++;
}
return iImage;
}
QImage BooksCoverWidget::DefaultImage::scaled(int aWidth, int aHeight)
{
QImage scaled;
HASSERT(iImage);
if (iImage) {
const int iw = iImage->width();
const int ih = iImage->height();
if (aWidth*ih > iw*aHeight) {
// Scaling to height
if (iScaledImage && iScaledImage->height() != aHeight) {
delete iScaledImage;
iScaledImage = NULL;
}
if (iScaledImage) {
scaled = *iScaledImage;
} else {
HDEBUG("scaling to height" << aHeight);
scaled = iImage->scaledToHeight(aHeight,
Qt::SmoothTransformation);
iScaledImage = new QImage(scaled);
}
} else {
// Scaling to width
if (iScaledImage && iScaledImage->width() != aWidth) {
delete iScaledImage;
iScaledImage = NULL;
}
if (iScaledImage) {
scaled = *iScaledImage;
} else {
HDEBUG("scaling to width" << aHeight);
scaled = iImage->scaledToWidth(aWidth,
Qt::SmoothTransformation);
iScaledImage = new QImage(scaled);
}
}
}
return scaled;
}
void BooksCoverWidget::DefaultImage::release(QImage* aImage)
{
if (aImage) {
HASSERT(aImage == iImage);
if (!(--iRefCount)) {
HDEBUG("deleting cached image");
if (iImage) {
delete iImage;
iImage = NULL;
}
if (iScaledImage) {
delete iScaledImage;
iScaledImage = NULL;
}
}
}
}
// ==========================================================================
// BooksViewWidget
// ==========================================================================
BooksCoverWidget::BooksCoverWidget(QQuickItem* aParent) :
QQuickPaintedItem(aParent),
iTaskQueue(BooksTaskQueue::scaleQueue()),
iScaleTask(NULL),
iBook(NULL),
iDefaultImage(NULL),
iBorderWidth(0),
iBorderRadius(0),
iBorderColor(Qt::transparent),
iStretch(false),
iSynchronous(false)
{
setFlag(ItemHasContents, true);
setFillColor(Qt::transparent);
connect(this, SIGNAL(widthChanged()), SLOT(onSizeChanged()));
connect(this, SIGNAL(heightChanged()), SLOT(onSizeChanged()));
}
BooksCoverWidget::~BooksCoverWidget()
{
HDEBUG(iTitle);
DefaultImage::release(iDefaultImage);
if (iScaleTask) iScaleTask->release(this);
if (iBook) iBook->release();
}
void BooksCoverWidget::setBook(BooksBook* aBook)
{
if (iBook != aBook) {
const bool wasEmpty(empty());
const bool wasLoading = loading();
if (iBook) {
iBook->disconnect(this);
iBook->release();
}
if (aBook) {
(iBook = aBook)->retain();
iBook->requestCoverImage();
iCoverImage = iBook->coverImage();
iTitle = iBook->title();
connect(iBook,
SIGNAL(loadingCoverChanged()),
SIGNAL(loadingChanged()));
connect(iBook,
SIGNAL(coverImageChanged()),
SLOT(onCoverImageChanged()));
HDEBUG(iTitle);
} else {
iBook = NULL;
iCoverImage = QImage();
iTitle.clear();
HDEBUG("<none>");
}
scaleImage(wasEmpty);
Q_EMIT bookChanged();
if (wasLoading != loading()) {
Q_EMIT loadingChanged();
}
}
}
void BooksCoverWidget::onCoverImageChanged()
{
HDEBUG(iTitle);
const bool wasEmpty(empty());
iCoverImage = iBook->coverImage();
scaleImage(wasEmpty);
}
void BooksCoverWidget::setStretch(bool aValue)
{
if (iStretch != aValue) {
iStretch = aValue;
HDEBUG(aValue);
scaleImage();
Q_EMIT stretchChanged();
}
}
void BooksCoverWidget::setSynchronous(bool aValue)
{
if (iSynchronous != aValue) {
iSynchronous = aValue;
HDEBUG(aValue);
Q_EMIT synchronousChanged();
}
}
void BooksCoverWidget::setBorderWidth(qreal aWidth)
{
if (iBorderWidth != aWidth && aWidth >= 0) {
iBorderWidth = aWidth;
HDEBUG(iBorderWidth);
update();
Q_EMIT borderWidthChanged();
}
}
void BooksCoverWidget::setBorderRadius(qreal aRadius)
{
if (iBorderRadius != aRadius && aRadius >= 0) {
iBorderRadius = aRadius;
HDEBUG(iBorderRadius);
update();
Q_EMIT borderRadiusChanged();
}
}
void BooksCoverWidget::setBorderColor(QColor aColor)
{
if (iBorderColor != aColor) {
iBorderColor = aColor;
HDEBUG(iBorderColor);
update();
Q_EMIT borderColorChanged();
}
}
void BooksCoverWidget::setDefaultCover(QUrl aUrl)
{
if (iDefaultCover != aUrl) {
iDefaultCover = aUrl;
HDEBUG(iDefaultCover);
scaleImage();
Q_EMIT defaultCoverChanged();
}
}
void BooksCoverWidget::onSizeChanged()
{
scaleImage();
}
bool BooksCoverWidget::empty() const
{
return iScaledImage.isNull();
}
bool BooksCoverWidget::loading() const
{
return iBook && iBook->loadingCover();
}
void BooksCoverWidget::scaleImage(bool aWasEmpty)
{
const int w = width();
const int h = height();
if (iScaleTask) {
iScaleTask->release(this);
iScaleTask = NULL;
}
if (w > 0 && h > 0) {
if ((!iBook || !iBook->hasCoverImage()) && iDefaultCover.isValid()) {
QString path(iDefaultCover.toLocalFile());
if (!iCoverImage.load(path)) {
HWARN("Failed to load" << qPrintable(path));
}
}
if (iCoverImage.isNull()) {
if (!iDefaultImage) iDefaultImage = DefaultImage::retain();
if (iDefaultImage) iCoverImage = *iDefaultImage;
}
if (!iCoverImage.isNull()) {
if (iSynchronous) {
iScaledImage = ScaleTask::scale(iCoverImage, w, h, iStretch);
update();
} else {
(iScaleTask = new ScaleTask(iTaskQueue->pool(), iCoverImage, w, h,
iStretch))->submit(this, SLOT(onScaleTaskDone()));
}
} else {
iScaledImage = QImage();
update();
}
} else {
iScaledImage = QImage();
}
updateCenter();
if (aWasEmpty != empty()) {
Q_EMIT emptyChanged();
}
}
void BooksCoverWidget::onScaleTaskDone()
{
const bool wasEmpty(empty());
HASSERT(iScaleTask == sender());
iScaledImage = iScaleTask->iScaledImage;
iScaleTask->release(this);
iScaleTask = NULL;
update();
updateCenter();
if (wasEmpty != empty()) {
Q_EMIT emptyChanged();
}
}
void BooksCoverWidget::paint(QPainter* aPainter)
{
const qreal w = width();
const qreal h = height();
if (w > 0 && h > 0) {
qreal sw, sh;
// This has to be consistent with updateCenter()
if (!iScaledImage.isNull()) {
sw = iScaledImage.width();
sh = iScaledImage.height();
} else {
sw = w;
sh = h;
}
const int x = floor((w - sw)/2);
const int y = h - sh;
QPainterPath path;
qreal w1, h1, x1, y1;
if (iBorderRadius > 0) {
// The border rectangle is no less that 3*radius
// and no more than the size of the item.
const qreal d = 2*iBorderRadius;
w1 = qMin(w, qMax(sw, 2*d)) - iBorderWidth;
h1 = qMin(h, qMax(sh, 3*d)) - iBorderWidth;
x1 = floor((w - w1)/2);
y1 = h - h1 - iBorderWidth/2;
const qreal x2 = x1 + w1 - d;
const qreal y2 = y1 + h1 - d;
path.arcMoveTo(x1, y1, d, d, 180);
path.arcTo(x1, y1, d, d, 180, -90);
path.arcTo(x2, y1, d, d, 90, -90);
path.arcTo(x2, y2, d, d, 0, -90);
path.arcTo(x1, y2, d, d, 270, -90);
path.closeSubpath();
aPainter->setClipPath(path);
} else {
w1 = sw - iBorderWidth;
h1 = sh - iBorderWidth;
x1 = floor((w - w1)/2);
y1 = h - h1 - iBorderWidth/2;
}
if (!iScaledImage.isNull()) {
aPainter->drawImage(x, y, iScaledImage);
}
if (iBorderColor.alpha() && iBorderWidth > 0) {
aPainter->setRenderHint(QPainter::Antialiasing);
aPainter->setRenderHint(QPainter::HighQualityAntialiasing);
aPainter->setBrush(Qt::NoBrush);
aPainter->setPen(QPen(iBorderColor, iBorderWidth));
if (iBorderRadius > 0) {
aPainter->setClipping(false);
aPainter->drawPath(path);
} else {
aPainter->drawRect(x1, y1, w1, h1);
}
}
}
}
void BooksCoverWidget::updateCenter()
{
const QPoint oldCenter(iCenter);
const qreal w = width();
const qreal h = height();
// This has to be consistent with paint()
iCenter.setX(floor(w/2));
if (iScaledImage.isNull()) {
iCenter.setY(floor(h/2));
} else {
iCenter.setY(floor(h - iScaledImage.height()/2));
}
if (iCenter != oldCenter) {
Q_EMIT centerChanged();
if (iCenter.x() != oldCenter.x()) {
Q_EMIT centerXChanged();
}
if (iCenter.y() != oldCenter.y()) {
Q_EMIT centerYChanged();
}
}
}