Commit fac42c38 authored by Halla Rempt's avatar Halla Rempt

Nearly working shape caching

This implements caching of shapes using QPixmapCache following the
design of QGraphicsView's caching, with some flake-specific changes.
(Flake can show different sets of shapes on different canvases, while
QGraphicsView shows the same scene on different paint devices.)

Current problems:

* when editing, shapes get overpainted incorrectly
* in kword, the margin offset seems wrong, so the page is broken
off at the wrong place.
* editing text doesn't work atm

svn path=/branches/work/koffice-essen/; revision=1192455
parent 5044a84a
......@@ -36,6 +36,7 @@ set(flake_SRCS
KoShapeContainerDefaultModel.cpp
KoShapeGroup.cpp
KoShapeManagerPaintingStrategy.cpp
KoShapeManagerCachedPaintingStrategy.cpp
KoShapeManager.cpp
KoShapePainter.cpp
KoFrameShape.cpp
......@@ -257,6 +258,8 @@ install(
KoLoadingShapeUpdater.h
KoShapeConfigWidgetBase.h
KoShapeManager.h
KoShapeManagerPaintingStrategy.h
KoShapeManagerCachedPaintingStrategy.h
KoShapeRegistry.h
KoShapeSavingContext.h
KoShapeUserData.h
......
......@@ -3,6 +3,7 @@
Copyright (C) 2006-2010 Thomas Zander <zander@kde.org>
Copyright (C) 2006-2010 Thorsten Zachmann <zachmann@kde.org>
Copyright (C) 2007-2009 Jan Hambrecht <jaham@gmx.net>
CopyRight (C) 2010 Boudewijn Rempt <boud@kogmbh.com>
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Library General Public
......@@ -17,7 +18,7 @@
You should have received a copy of the GNU Library General Public License
along with this library; see the file COPYING.LIB. If not, write to
the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301, USA.
Boston, MA 02110-1301, USA.
*/
#include "KoShape.h"
......@@ -64,30 +65,43 @@
#include <QList>
#include <QMap>
#include <QByteArray>
#include <QPixmapCache>
#include <kdebug.h>
#include <limits>
// KoShapeCache
/// Empty all cached pixmaps from the pixmap cache
void KoShapeCache::purge()
{
qDeleteAll(deviceData);
deviceData.clear();
}
// KoShapePrivate
KoShapePrivate::KoShapePrivate(KoShape *shape)
: size(50, 50),
parent(0),
userData(0),
appData(0),
fill(0),
border(0),
q_ptr(shape),
shadow(0),
filterEffectStack(0),
transparency(0.0),
zIndex(0),
visible(true),
printable(true),
geometryProtected(false),
keepAspect(false),
selectable(true),
detectCollision(false),
protectContent(false)
parent(0),
userData(0),
appData(0),
fill(0),
border(0),
q_ptr(shape),
shadow(0),
filterEffectStack(0),
transparency(0.0),
zIndex(0),
visible(true),
printable(true),
geometryProtected(false),
keepAspect(false),
selectable(true),
detectCollision(false),
protectContent(false),
cacheMode(KoShape::ScaledCache),
cache(0)
{
connectors.append(QPointF(0.5, 0.0));
connectors.append(QPointF(1.0, 0.5));
......@@ -137,16 +151,16 @@ void KoShapePrivate::updateBorder()
QSizeF inner = q->size();
// update left
q->update(QRectF(-insets.left, -insets.top, insets.left,
inner.height() + insets.top + insets.bottom));
inner.height() + insets.top + insets.bottom));
// update top
q->update(QRectF(-insets.left, -insets.top,
inner.width() + insets.left + insets.right, insets.top));
inner.width() + insets.left + insets.right, insets.top));
// update right
q->update(QRectF(inner.width(), -insets.top, insets.right,
inner.height() + insets.top + insets.bottom));
inner.height() + insets.top + insets.bottom));
// update bottom
q->update(QRectF(-insets.left, inner.height(),
inner.width() + insets.left + insets.right, insets.bottom));
inner.width() + insets.left + insets.right, insets.bottom));
}
void KoShapePrivate::addShapeManager(KoShapeManager *manager)
......@@ -157,7 +171,37 @@ void KoShapePrivate::addShapeManager(KoShapeManager *manager)
void KoShapePrivate::removeShapeManager(KoShapeManager *manager)
{
shapeManagers.remove(manager);
if (cacheMode == KoShape::ScaledCache) {
if (KoShapeCache *cache = maybeShapeCache()) {
KoShapeCache::DeviceData *deviceData = cache->deviceData.take(manager);
delete deviceData;
}
}
}
KoShapeCache *KoShapePrivate::maybeShapeCache() const
{
return cache;
}
KoShapeCache *KoShapePrivate::shapeCache() const
{
if (!cache) {
const_cast<KoShapePrivate *>(this)->cache = new KoShapeCache;
}
return cache;
}
void KoShapePrivate::removeShapeCache()
{
if (cache) {
cache->purge();
delete cache;
cache = 0;
}
}
// static
QString KoShapePrivate::getStyleProperty(const char *property, KoShapeLoadingContext &context)
{
......@@ -175,7 +219,7 @@ QString KoShapePrivate::getStyleProperty(const char *property, KoShapeLoadingCon
// ======== KoShape
KoShape::KoShape()
: d_ptr(new KoShapePrivate(this))
: d_ptr(new KoShapePrivate(this))
{
notifyChanged();
}
......@@ -189,6 +233,7 @@ KoShape::~KoShape()
{
Q_D(KoShape);
d->shapeChanged(Deleted);
d->removeShapeCache();
delete d_ptr;
}
......@@ -206,7 +251,7 @@ void KoShape::paintDecorations(QPainter &painter, const KoViewConverter &convert
painter.setPen(pen);
painter.setBrush(Qt::NoBrush);
for (int i = 0; i < d->connectors.size(); ++i)
{
{
QPointF p = converter.documentToView(d->connectors[ i ]);
painter.drawLine(QPointF(p.x() - 2, p.y() + 2), QPointF(p.x() + 2, p.y() - 2));
painter.drawLine(QPointF(p.x() + 2, p.y() + 2), QPointF(p.x() - 2, p.y() - 2));
......@@ -455,20 +500,52 @@ int KoShape::zIndex() const
void KoShape::update() const
{
Q_D(const KoShape);
if (d->cacheMode != NoCache) {
KoShapeCache *cache = d->shapeCache();
foreach(KoShapeCache::DeviceData *data, cache->deviceData.values()) {
data->allExposed = true;
data->exposed.clear();
}
}
if (!d->shapeManagers.empty()) {
QRectF rect(boundingRect());
foreach(KoShapeManager * manager, d->shapeManagers)
foreach(KoShapeManager * manager, d->shapeManagers) {
manager->update(rect, this, true);
}
}
}
void KoShape::update(const QRectF &shape) const
void KoShape::update(const QRectF &rect) const
{
if (rect.isEmpty() && !rect.isNull()) {
return;
}
Q_D(const KoShape);
if (d->cacheMode != NoCache) {
KoShapeCache *cache = d->shapeCache();
foreach(KoShapeCache::DeviceData *data, cache->deviceData.values()) {
if (!data->allExposed) {
if (rect.isNull()) {
data->allExposed = true;
data->exposed.clear();
}
else {
data->exposed.append(rect);
}
}
}
}
if (!d->shapeManagers.empty() && isVisible()) {
QRectF rect(absoluteTransformation(0).mapRect(shape));
QRectF rc(absoluteTransformation(0).mapRect(rect));
foreach(KoShapeManager * manager, d->shapeManagers) {
manager->update(rect);
manager->update(rc);
}
}
}
......@@ -540,6 +617,10 @@ void KoShape::notifyChanged()
foreach(KoShapeManager * manager, d->shapeManagers) {
manager->notifyShapeChanged(this);
}
KoShapeCache *cache = d->maybeShapeCache();
if (cache) {
cache->purge();
}
}
void KoShape::setUserData(KoShapeUserData *userData)
......@@ -703,7 +784,14 @@ void KoShape::setZIndex(int zIndex)
void KoShape::setVisible(bool on)
{
Q_D(KoShape);
if (d->visible == on) return;
d->visible = on;
if (d->visible) {
KoShapeCache *cache = d->maybeShapeCache();
if (cache) {
cache->purge();
}
}
}
bool KoShape::isVisible(bool recursive) const
......@@ -966,8 +1054,8 @@ QString KoShape::saveStyle(KoGenStyle &style, KoShapeSavingContext &context) con
qreal bottom = parent()->size().height() - size().height() - top;
style.addProperty("fo:clip", QString("rect(%1pt, %2pt, %3pt, %4pt)")
.arg(top, 10, 'f').arg(right, 10, 'f')
.arg(bottom, 10, 'f').arg(left, 10, 'f'));
.arg(top, 10, 'f').arg(right, 10, 'f')
.arg(bottom, 10, 'f').arg(left, 10, 'f'));
}
......@@ -1110,8 +1198,8 @@ KoShapeBackground *KoShape::loadOdfFill(KoShapeLoadingContext &context) const
bg = new KoPatternBackground(context.imageCollection());
#ifndef NWORKAROUND_ODF_BUGS
} else if (fill.isEmpty()) {
bg = KoOdfWorkaround::fixBackgroundColor(this, context);
return bg;
bg = KoOdfWorkaround::fixBackgroundColor(this, context);
return bg;
#endif
} else {
return 0;
......@@ -1267,8 +1355,8 @@ QTransform KoShape::parseOdfTransform(const QString &transform)
QTransform m;
if (params.count() >= 6) {
m.setMatrix(params[0].toDouble(), params[1].toDouble(), 0,
params[2].toDouble(), params[3].toDouble(), 0,
KoUnit::parseValue(params[4]), KoUnit::parseValue(params[5]), 1);
params[2].toDouble(), params[3].toDouble(), 0,
KoUnit::parseValue(params[4]), KoUnit::parseValue(params[5]), 1);
}
matrix = matrix * m;
}
......@@ -1349,9 +1437,9 @@ void KoShape::saveOdfAttributes(KoShapeSavingContext &context, int attributes) c
context.xmlWriter().addAttribute("svg:y", QString("%1pt").arg(matrix.dy()));
} else {
QString m = QString("matrix(%1 %2 %3 %4 %5pt %6pt)")
.arg(matrix.m11()).arg(matrix.m12())
.arg(matrix.m21()).arg(matrix.m22())
.arg(matrix.dx()) .arg(matrix.dy());
.arg(matrix.m11()).arg(matrix.m12())
.arg(matrix.m21()).arg(matrix.m22())
.arg(matrix.dx()) .arg(matrix.dy());
context.xmlWriter().addAttribute("draw:transform", m);
}
}
......@@ -1528,3 +1616,22 @@ KoShapePrivate *KoShape::priv()
Q_D(KoShape);
return d;
}
KoShape::CacheMode KoShape::cacheMode() const
{
Q_D(const KoShape);
return d->cacheMode;
}
void KoShape::setCacheMode(CacheMode mode)
{
Q_D(KoShape);
d->cacheMode = mode;
if (mode == NoCache) {
d->removeShapeCache();
} else {
KoShapeCache *cache = d->shapeCache();
// Reset old cache
cache->purge();
}
}
......@@ -126,6 +126,12 @@ public:
ChildChanged ///< a child of a container was changed/removed. This is propagated to all parents
};
/// See QGraphicsItem::CacheMode
enum CacheMode {
NoCache, ///< no cache -- the default
ScaledCache, ///< cache at every zoomlevel
};
/**
* @brief Constructor
*/
......@@ -492,9 +498,9 @@ public:
* normalized.
* <p>This method will return immediately and only request a repaint. Successive calls
* will be merged into an appropriate repaint action.
* @param shape the rectangle (in pt) to queue for repaint.
* @param rect the rectangle (in pt) to queue for repaint.
*/
virtual void update(const QRectF &shape) const;
virtual void update(const QRectF &rect) const;
/**
* This is a method used to sort a list using the STL sorting methods.
......@@ -731,11 +737,11 @@ public:
* In this case it can be shown on screen probably partially but it should really not be printed
* until it is fully done processing.
* Warning! This method can be blocking for a long time
* @param asynchronous If set to true the processing will can take place in a different thread and the
* function will not block until the shape is finised.
* In case of printing Flake will call this method from a non-main thread and only
* @param asynchronous If set to true the processing will can take place in a different thread and the
* function will not block until the shape is finised.
* In case of printing Flake will call this method from a non-main thread and only
* start printing it when the in case of printing method returned.
* If set to false the processing needs to be done synchronously and will
* If set to false the processing needs to be done synchronously and will
* block until the result is finished.
*/
virtual void waitUntilReady(const KoViewConverter &converter, bool asynchronous = true) const;
......@@ -873,6 +879,17 @@ public:
*/
KoShapePrivate *priv();
/**
* Returns the cache mode for this shape. The default mode is NoCache (i.e.,
* cache is disabled and all painting is immediate).
*/
CacheMode cacheMode() const;
/**
* Set the shape's cache mode to @param mode.
*/
void setCacheMode(CacheMode cacheMode);
protected:
/// constructor
KoShape(KoShapePrivate &);
......@@ -955,6 +972,8 @@ protected:
/// return the current matrix that contains the rotation/scale/position of this shape
QTransform transform() const;
friend class KoShapeManagerCachedPaintingStrategy;
KoShapePrivate *d_ptr;
private:
......
......@@ -476,7 +476,7 @@ void KoShapeManager::paintShape(KoShape *shape, QPainter &painter, const KoViewC
painter.drawImage(clippingOffset, imageBuffers.value(lastEffect->output()));
painter.restore();
}
if (! forPrint) {
if (!forPrint) {
painter.setRenderHint(QPainter::Antialiasing, false);
shape->paintDecorations(painter, converter, d->canvas);
}
......@@ -634,4 +634,9 @@ void KoShapeManager::setPaintingStrategy(KoShapeManagerPaintingStrategy *strateg
d->strategy = strategy;
}
KoCanvasBase *KoShapeManager::canvas()
{
return d->canvas;
}
#include <KoShapeManager.moc>
......@@ -73,6 +73,7 @@ public:
KoShapeManager(KoCanvasBase *canvas, const QList<KoShape *> &shapes);
virtual ~KoShapeManager();
/**
* Remove all previously owned shapes and make the argument list the new shapes
* to be managed by this manager.
......@@ -202,6 +203,11 @@ signals:
void selectionContentChanged();
private:
friend class KoShapeManagerPaintingStrategy;
KoCanvasBase *canvas();
class Private;
Private * const d;
Q_PRIVATE_SLOT(d, void updateTree())
......
/* This file is part of the KDE project
* Copyright (C) 2010 KO GmbH <boud@kogmbh.org>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or ( at your option ) any later version.
*
* This library 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
* Library General Public License for more details.
*
* You should have received a copy of the GNU Library General Public License
* along with this library; see the file COPYING.LIB. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301, USA.
*/
#include "KoShapeManagerCachedPaintingStrategy.h"
#include <kdebug.h>
#include "KoShape.h"
#include "KoShape_p.h"
#include "KoShapeManager.h"
#include "KoCanvasBase.h"
#include "KoViewConverter.h"
#include "KoFilterEffectStack.h"
#include <QApplication>
#include <QDesktopWidget>
#include <QPainter>
#include <QPixmap>
#include <QPixmapCache>
#include <QTransform>
#include <QRegion>
/*
Notes:
We have two coordinate systems:
* shape coordinate system: in pts. There are two origins: the internal 0,0 origin which is the
top-left of the cache pixmap, and the position of the shape on the canvas.
* view coordinate system: pts per inch multiplied by zoom
The shape registers with areas of the shape's surface are exposed in shape update events. This uses
the 0,0 = top-left origin and is registered in shape coordinates. We try not to paint outside those
update events.
The shapemanager has a painter and the painter has a clipregion in view coordinates. We cannot paint
outside them and should be careful not to do that once the shape cache has been initialized.
Qt's QGraphicsView only paints the cache of the exposed parts of the shape and does not care about
the QPainter's clipRegion. But it does not do a full initialization at the current zoomlevel of
the shape's cache, which is what we should do.
*/
/// Returns the bounding rect of the shape including all the filters
/// This has an origin of 0,0
static QRectF shapeBoundingRect(KoShape *shape)
{
// First step, compute the rectangle used for the pixmap if there are effects
// -- they make the pixmap bigger. And then convert clip region to view coordinates.
QRectF rc;
if (shape->filterEffectStack() && !shape->filterEffectStack()->isEmpty()) {
rc = shape->filterEffectStack()->clipRectForBoundingRect(QRectF(QPointF(), shape->size()));
}
else {
rc = QRectF(QPointF(), shape->size());
}
return rc;
}
/// Create an empty, transparent pixmap
static QPixmap createCachePixmap(KoShape *shape, const KoViewConverter &viewConverter)
{
QRectF zoomedClipRect = viewConverter.documentToView(shapeBoundingRect(shape));
// Initialize the buffer image
QPixmap pix(zoomedClipRect.size().toSize());
pix.fill(Qt::transparent);
kDebug(30006) << "Created empty pixmap for shape "
<< shape << shape->shapeId()
<< "size" << pix.size();
return pix;
}
/**
* Ask the shape manager to paint (part of) the current shape onto the pixmap.
* We need to setup the painter clipping very carefully to make this work correctly.
*
* @param shape the shape that is going to be painted
* @shapeManager the shapeManager that provides the logic for painting shapes
* and their associated effects
* @param viewConverter convert between view and shape (document coordinates)
* @param pixmapExposed the exposed region of the shape in document coordinates
* @param pix the cached pixmap
*/
static void paintIntoCache(KoShape *shape,
KoShapeManager *shapeManager,
const KoViewConverter &viewConverter,
QRegion pixmapExposed,
QPixmap *pix)
{
kDebug(30006) << "paintIntoCache. Shape:" << shape << "Zoom:" << viewConverter.zoom() << "clip" << pixmapExposed.boundingRect();
QPainter pixmapPainter(pix);
pixmapPainter.setClipRegion(pixmapExposed);
pixmapPainter.fillRect(pixmapExposed.boundingRect(), Qt::transparent);
shapeManager->paintShape(shape, pixmapPainter, viewConverter, false);
}
// QRectF::intersects() returns false always if either the source or target
// rectangle's width or height are 0. This works around that problem.
static inline void adjustRect(QRectF *rect)
{
Q_ASSERT(rect);
if (!rect->width()) {
rect->adjust(qreal(-0.00001), 0, qreal(0.00001), 0);
}
if (!rect->height()) {
rect->adjust(0, qreal(-0.00001), 0, qreal(0.00001));
}
}
KoShapeManagerCachedPaintingStrategy::KoShapeManagerCachedPaintingStrategy(KoShapeManager *shapeManager)
: KoShapeManagerPaintingStrategy(shapeManager)
{
}
KoShapeManagerCachedPaintingStrategy::~KoShapeManagerCachedPaintingStrategy()
{
}
void KoShapeManagerCachedPaintingStrategy::paint(KoShape *shape, QPainter &painter, const KoViewConverter &viewConverter, bool forPrint)
{
kDebug(30006) << ">>>>>>>>>>>>>>>>>>>>>>>>>>>\n\tshape" << shape->shapeId() << "viewConverter zoom" << viewConverter.zoom();
kDebug(30006) << "\tpainter clipregion" << painter.clipRegion().boundingRect();
if (!shapeManager()) {
return;
}
KoShape::CacheMode cacheMode = shape->cacheMode();
// For printing, we never use the cache. It would be in the wrong resolution.
if (cacheMode == KoShape::NoCache || forPrint) {
painter.save();
painter.setTransform(shape->absoluteTransformation(&viewConverter) * painter.transform());
shapeManager()->paintShape(shape, painter, viewConverter, forPrint);
painter.restore(); // for the matrix
return;
}
if (cacheMode == KoShape::ScaledCache) {
// Shape's (local) bounding rect in document coordinates
QRectF boundingRect = shapeBoundingRect(shape);
QRectF adjustedBoundingRect(boundingRect);
adjustRect(&adjustedBoundingRect);
if (adjustedBoundingRect.isEmpty()) {
return;
}
// transpose to boundingrect to where in the document the shape is, to
// make it comparable to the exposed area of the painter.
adjustedBoundingRect.translate(shape->position());
kDebug(30006) << "\tshape->boundingRect" << boundingRect <<
"\n\tadjustedrect" << adjustedBoundingRect;
// view coordinates
QRectF painterClippingBounds = painter.clipRegion().boundingRect();
// document coordinates
QRectF painterShapeClippingBounds = viewConverter.viewToDocument(painterClippingBounds);
kDebug(30006) << "\tpainterClippingBounds in view coordinates:" << painterClippingBounds
<< "in document coordinates:" << painterShapeClippingBounds;
QRectF exposedRect = adjustedBoundingRect.intersected(painterShapeClippingBounds);
if (exposedRect.isEmpty()) {
kDebug(30006) << "\t\tNo exposed area for shape" << shape << shape->shapeId();