diff --git a/libs/flake/KoShapeManager.h b/libs/flake/KoShapeManager.h index 809264f232..37c74afec2 100644 --- a/libs/flake/KoShapeManager.h +++ b/libs/flake/KoShapeManager.h @@ -1,272 +1,276 @@ /* This file is part of the KDE project Copyright (C) 2006-2008 Thorsten Zachmann Copyright (C) 2007, 2009 Thomas Zander 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. */ #ifndef KOSHAPEMANAGER_H #define KOSHAPEMANAGER_H #include #include #include #include #include "KoFlake.h" #include "kritaflake_export.h" #include #include class KoShape; class KoSelection; class KoViewConverter; class KoCanvasBase; class KoPointerEvent; class KoShapePaintingContext; class QPainter; class QPointF; class QRectF; /** * The shape manager hold a list of all shape which are in scope. * There is one shape manager per canvas. This makes the shape manager * different from QGraphicsScene, which contains the datamodel for all * graphics items: KoShapeManager only contains the subset of shapes * that are shown in its canvas. * * The selection in the different views can be different. */ class KRITAFLAKE_EXPORT KoShapeManager : public QObject { Q_OBJECT public: /// enum for add() enum Repaint { PaintShapeOnAdd, ///< Causes each shapes 'update()' to be called after being added to the shapeManager AddWithoutRepaint ///< Avoids each shapes 'update()' to be called for faster addition when its possible. }; /** * Constructor. */ explicit KoShapeManager(KoCanvasBase *canvas); /** * Constructor that takes a list of shapes, convenience version. * @param shapes the shapes to start out with, see also setShapes() * @param canvas the canvas this shape manager is working on. */ KoShapeManager(KoCanvasBase *canvas, const QList &shapes); ~KoShapeManager() override; /** * Remove all previously owned shapes and make the argument list the new shapes * to be managed by this manager. * @param shapes the new shapes to manage. * @param repaint if true it will trigger a repaint of the shapes */ void setShapes(const QList &shapes, Repaint repaint = PaintShapeOnAdd); /// returns the list of maintained shapes QList shapes() const; /** * Get a list of all shapes that don't have a parent. */ QList topLevelShapes() const; public Q_SLOTS: /** * Add a KoShape to be displayed and managed by this manager. * This will trigger a repaint of the shape. * @param shape the shape to add * @param repaint if true it will trigger a repaint of the shape */ void addShape(KoShape *shape, KoShapeManager::Repaint repaint = PaintShapeOnAdd); /** * Remove a KoShape from this manager * @param shape the shape to remove */ void remove(KoShape *shape); public: /// return the selection shapes for this shapeManager KoSelection *selection() const; struct PaintJob { using ShapesStorage = std::vector>; using SharedSafeStorage = std::shared_ptr; PaintJob() = default; PaintJob(QRectF _docUpdateRect, QRect _viewUpdateRect) : docUpdateRect(_docUpdateRect), viewUpdateRect(_viewUpdateRect) { } + bool isEmpty() const { + return shapes.isEmpty(); + } + QRectF docUpdateRect; QRect viewUpdateRect; QList shapes; SharedSafeStorage allClonedShapes; }; struct PaintJobsList : public QList { QRect uncroppedViewUpdateRect; }; /** * Prepare a shallow copy of all the shapes and the jobs to be rendered * asynchronoursly later. The copies are stored in jobs, so that the user * could later pass these jobs into paintJob() in a separate thread. * * @param jobs a list of rects that are going to be updated. docUpdateRect * and viewUpdateRect should be preinitialized by the caller. * @param excludeRoot the root shape which should not be copied. It is basically * a hack to avoid copying of KisShapeLayer, which is not * copiable. * \see paintJob() * \see a comment in KisShapeLayerCanvas::slotStartAsyncRepaint() */ void preparePaintJobs(PaintJobsList &jobs, KoShape *excludeRoot); /** * Render a \p job on \p painter. No mutable internals of the shape * manager are accessed, so calling this method is safe in multithreading * environment. * * @param painter a painter to paint on. Clip rect of the painter is expected to be setup correctly. * @param job a job to paint. * @param forPrint not used in Krita. * * \see preparePaintJobs */ void paintJob(QPainter &painter, const KoShapeManager::PaintJob &job, bool forPrint); /** * Paint all shapes and their selection handles etc. * @param painter the painter to paint to. * @param forPrint if true, make sure only actual content is drawn and no decorations. * @param converter to convert between document and view coordinates. */ void paint(QPainter &painter, bool forPrint); /** * Returns the shape located at a specific point in the document. * If more than one shape is located at the specific point, the given selection type * controls which of them is returned. * @param position the position in the document coordinate system. * @param selection controls which shape is returned when more than one shape is at the specific point * @param omitHiddenShapes if true, only visible shapes are considered */ KoShape *shapeAt(const QPointF &position, KoFlake::ShapeSelection selection = KoFlake::ShapeOnTop, bool omitHiddenShapes = true); /** * Returns the shapes which intersects the specific rect in the document. * @param rect the rectangle in the document coordinate system. * @param omitHiddenShapes if @c true, only visible shapes are considered * @param containedMode if @c true use contained mode */ QList shapesAt(const QRectF &rect, bool omitHiddenShapes = true, bool containedMode = false); /** * Request a repaint to be queued. * The repaint will be restricted to the parameters rectangle, which is expected to be * in points (the document coordinates system of KoShape) and it is expected to be * normalized and based in the global coordinates, not any local coordinates. *

This method will return immediately and only request a repaint. Successive calls * will be merged into an appropriate repaint action. * @param rect the rectangle (in pt) to queue for repaint. * @param shape the shape that is going to be redrawn; only needed when selectionHandles=true * @param selectionHandles if true; find out if the shape is selected and repaint its * selection handles at the same time. */ void update(const QRectF &rect, const KoShape *shape = 0, bool selectionHandles = false); /** * Block all updates initiated with update() call. The incoming updates will * be dropped completely. */ void setUpdatesBlocked(bool value); /** * \see setUpdatesBlocked() */ bool updatesBlocked() const; /** * Update the tree for finding the shapes. * This will remove the shape from the tree and will reinsert it again. * The update to the tree will be posponed until it is needed so that successive calls * will be merged into one. * @param shape the shape to updated its position in the tree. */ void notifyShapeChanged(KoShape *shape); /** * @brief renderSingleShape renders a shape on \p painter. This method includes all the * needed steps for painting a single shape: setting transformations, clipping and masking. */ static void renderSingleShape(KoShape *shape, QPainter &painter, KoShapePaintingContext &paintContext); /** * A special interface for KoShape to use during shape destruction. Don't use this * interface directly unless you are KoShape. */ struct ShapeInterface { ShapeInterface(KoShapeManager *_q); /** * Called by a shape when it is destructed. Please note that you cannot access * any shape's method type or information during this call because the shape might be * semi-destroyed. */ void notifyShapeDestructed(KoShape *shape); protected: KoShapeManager *q; }; ShapeInterface* shapeInterface(); Q_SIGNALS: /// emitted when the selection is changed void selectionChanged(); /// emitted when an object in the selection is changed (moved/rotated etc) void selectionContentChanged(); /// emitted when any object changed (moved/rotated etc) void contentChanged(); private: KoCanvasBase *canvas(); class Private; Private * const d; Q_PRIVATE_SLOT(d, void updateTree()) Q_PRIVATE_SLOT(d, void forwardCompressedUdpate()) }; #endif diff --git a/libs/ui/flake/kis_shape_layer_canvas.cpp b/libs/ui/flake/kis_shape_layer_canvas.cpp index 2ff2ef08b3..8856c159b7 100644 --- a/libs/ui/flake/kis_shape_layer_canvas.cpp +++ b/libs/ui/flake/kis_shape_layer_canvas.cpp @@ -1,446 +1,451 @@ /* * Copyright (c) 2007 Boudewijn Rempt * * 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 "kis_shape_layer_canvas.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kis_image_view_converter.h" #include #include #include #include #include "kis_global.h" #include "krita_utils.h" KisShapeLayerCanvasBase::KisShapeLayerCanvasBase(KisShapeLayer *parent, KisImageWSP image) : KoCanvasBase(0) , m_viewConverter(new KisImageViewConverter(image)) , m_shapeManager(new KoShapeManager(this)) , m_selectedShapesProxy(new KoSelectedShapesProxySimple(m_shapeManager.data())) { m_shapeManager->selection()->setActiveLayer(parent); } KoShapeManager *KisShapeLayerCanvasBase::shapeManager() const { return m_shapeManager.data(); } KoSelectedShapesProxy *KisShapeLayerCanvasBase::selectedShapesProxy() const { return m_selectedShapesProxy.data(); } KoViewConverter* KisShapeLayerCanvasBase::viewConverter() const { return m_viewConverter.data(); } void KisShapeLayerCanvasBase::gridSize(QPointF *offset, QSizeF *spacing) const { KIS_SAFE_ASSERT_RECOVER_NOOP(false); // This should never be called as this canvas should have no tools. Q_UNUSED(offset); Q_UNUSED(spacing); } bool KisShapeLayerCanvasBase::snapToGrid() const { KIS_SAFE_ASSERT_RECOVER_NOOP(false); // This should never be called as this canvas should have no tools. return false; } void KisShapeLayerCanvasBase::addCommand(KUndo2Command *) { KIS_SAFE_ASSERT_RECOVER_NOOP(false); // This should never be called as this canvas should have no tools. } KoToolProxy * KisShapeLayerCanvasBase::toolProxy() const { // KIS_SAFE_ASSERT_RECOVER_NOOP(false); // This should never be called as this canvas should have no tools. return 0; } QWidget* KisShapeLayerCanvasBase::canvasWidget() { return 0; } const QWidget* KisShapeLayerCanvasBase::canvasWidget() const { return 0; } KoUnit KisShapeLayerCanvasBase::unit() const { KIS_SAFE_ASSERT_RECOVER_NOOP(false); // This should never be called as this canvas should have no tools. return KoUnit(KoUnit::Point); } void KisShapeLayerCanvasBase::prepareForDestroying() { m_isDestroying = true; } bool KisShapeLayerCanvasBase::hasChangedWhileBeingInvisible() { return m_hasChangedWhileBeingInvisible; } KisShapeLayerCanvas::KisShapeLayerCanvas(KisShapeLayer *parent, KisImageWSP image) : KisShapeLayerCanvasBase(parent, image) , m_projection(0) , m_parentLayer(parent) , m_asyncUpdateSignalCompressor(100, KisSignalCompressor::FIRST_INACTIVE) , m_safeForcedConnection(std::bind(&KisShapeLayerCanvas::slotStartAsyncRepaint, this)) { /** * The layour should also add itself to its own shape manager, so that the canvas * would track its changes/transformations */ m_shapeManager->addShape(parent, KoShapeManager::AddWithoutRepaint); m_shapeManager->selection()->setActiveLayer(parent); connect(&m_asyncUpdateSignalCompressor, SIGNAL(timeout()), SLOT(slotStartAsyncRepaint())); setImage(image); } KisShapeLayerCanvas::~KisShapeLayerCanvas() { m_shapeManager->remove(m_parentLayer); } void KisShapeLayerCanvas::setImage(KisImageWSP image) { if (m_image) { disconnect(m_image, 0, this, 0); } m_viewConverter->setImage(image); m_image = image; if (image) { connect(m_image, SIGNAL(sigSizeChanged(QPointF,QPointF)), SLOT(slotImageSizeChanged())); m_cachedImageRect = m_image->bounds(); } } class KisRepaintShapeLayerLayerJob : public KisSpontaneousJob { public: KisRepaintShapeLayerLayerJob(KisShapeLayerSP layer, KisShapeLayerCanvas *canvas) : m_layer(layer), m_canvas(canvas) { } bool overrides(const KisSpontaneousJob *_otherJob) override { const KisRepaintShapeLayerLayerJob *otherJob = dynamic_cast(_otherJob); return otherJob && otherJob->m_canvas == m_canvas; } void run() override { m_canvas->repaint(); } int levelOfDetail() const override { return 0; } QString debugName() const override { QString result; QDebug dbg(&result); dbg << "KisRepaintShapeLayerLayerJob" << m_layer; return result; } private: // we store a pointer to the layer just // to keep the lifetime of the canvas! KisShapeLayerSP m_layer; KisShapeLayerCanvas *m_canvas; }; void KisShapeLayerCanvas::updateCanvas(const QVector ®ion) { if (!m_parentLayer->image() || m_isDestroying) { return; } { QMutexLocker locker(&m_dirtyRegionMutex); Q_FOREACH (const QRectF &rc, region) { // grow for antialiasing const QRect imageRect = kisGrowRect(m_viewConverter->documentToView(rc).toAlignedRect(), 2); m_dirtyRegion += imageRect; } } m_asyncUpdateSignalCompressor.start(); m_hasUpdateInCompressor = true; } void KisShapeLayerCanvas::updateCanvas(const QRectF& rc) { updateCanvas(QVector({rc})); } void KisShapeLayerCanvas::slotStartAsyncRepaint() { QRect repaintRect; QRect uncroppedRepaintRect; bool forceUpdateHiddenAreasOnly = false; const qint32 MASK_IMAGE_WIDTH = 256; const qint32 MASK_IMAGE_HEIGHT = 256; { QMutexLocker locker(&m_dirtyRegionMutex); repaintRect = m_dirtyRegion.boundingRect(); forceUpdateHiddenAreasOnly = m_forceUpdateHiddenAreasOnly; /// Since we are going to override the previous jobs, we should fetch /// all the area covered by it. Otherwise we'll get dirty leftovers of /// the layer on the projection Q_FOREACH (const KoShapeManager::PaintJob &job, m_paintJobs) { repaintRect |= m_viewConverter->documentToView().mapRect(job.docUpdateRect).toAlignedRect(); } m_paintJobs.clear(); m_dirtyRegion = QRegion(); m_forceUpdateHiddenAreasOnly = false; } if (!forceUpdateHiddenAreasOnly) { if (repaintRect.isEmpty()) { return; } // Crop the update rect by the image bounds. We keep the cache consistent // by tracking the size of the image in slotImageSizeChanged() uncroppedRepaintRect = repaintRect; repaintRect = repaintRect.intersected(m_parentLayer->image()->bounds()); } else { const QRectF shapesBounds = KoShape::boundingRect(m_shapeManager->shapes()); repaintRect |= kisGrowRect(m_viewConverter->documentToView(shapesBounds).toAlignedRect(), 2); uncroppedRepaintRect = repaintRect; } /** * Vector shapes are not thread-safe against concurrent read-writes, so we * need to utilize rather complicated policy on accessing them: * * 1) All shape writes happen in GUI thread (right in the tools) * 2) No concurrent reads from the shapes may happen in other threads * while the user is modifying them. * * That is why our shape rendering code is split into two parts: * * 1) First we just fetch a shallow copy of the shapes of the layer (it * takes about 1ms for complicated vector layers) and pack them into * KoShapeManager::PaintJobsList jobs. It happens here, in * slotStartAsyncRepaint(), which runs in the GUI thread. It guarantees * that noone is accessing the shapes during the copy operation. * * 2) The rendering itself happens in the worker thread in repaint(). But * repaint() doesn't access original shapes anymore. It accesses only they * shallow copies, which means that there is no concurrent * access to anything (*). * * (*) "no concurrent access to anything" is a rather fragile term :) There * will still be concurrent access to it, on detaching... But(!), when detaching, * the original data is kept unchanged, so "it should be safe enough"(c). Especially * if we guarantee that rendering thread may not cause a detach (?), and the detach * can happen only from a single GUI thread. */ const QVector updateRects = KritaUtils::splitRectIntoPatchesTight(repaintRect, QSize(MASK_IMAGE_WIDTH, MASK_IMAGE_HEIGHT)); KoShapeManager::PaintJobsList jobs; Q_FOREACH (const QRect &viewUpdateRect, updateRects) { jobs << KoShapeManager::PaintJob(m_viewConverter->viewToDocument().mapRect(QRectF(viewUpdateRect)), viewUpdateRect); } jobs.uncroppedViewUpdateRect = uncroppedRepaintRect; m_shapeManager->preparePaintJobs(jobs, m_parentLayer); { QMutexLocker locker(&m_dirtyRegionMutex); m_paintJobs = jobs; } m_hasUpdateInCompressor = false; m_image->addSpontaneousJob(new KisRepaintShapeLayerLayerJob(m_parentLayer, this)); } void KisShapeLayerCanvas::slotImageSizeChanged() { QRegion dirtyCacheRegion; dirtyCacheRegion += m_image->bounds(); dirtyCacheRegion += m_cachedImageRect; dirtyCacheRegion -= m_image->bounds() & m_cachedImageRect; QVector dirtyRects; auto rc = dirtyCacheRegion.begin(); while (rc != dirtyCacheRegion.end()) { dirtyRects.append(m_viewConverter->viewToDocument(*rc)); rc++; } updateCanvas(dirtyRects); m_cachedImageRect = m_image->bounds(); } void KisShapeLayerCanvas::repaint() { KoShapeManager::PaintJobsList paintJobs; { QMutexLocker locker(&m_dirtyRegionMutex); std::swap(paintJobs, m_paintJobs); } const qint32 MASK_IMAGE_WIDTH = 256; const qint32 MASK_IMAGE_HEIGHT = 256; QImage image(MASK_IMAGE_WIDTH, MASK_IMAGE_HEIGHT, QImage::Format_ARGB32); QPainter tempPainter(&image); tempPainter.setRenderHint(QPainter::Antialiasing); tempPainter.setRenderHint(QPainter::TextAntialiasing); quint8 * dstData = new quint8[MASK_IMAGE_WIDTH * MASK_IMAGE_HEIGHT * m_projection->pixelSize()]; QRect repaintRect = paintJobs.uncroppedViewUpdateRect; m_projection->clear(repaintRect); Q_FOREACH (const KoShapeManager::PaintJob &job, paintJobs) { + if (job.isEmpty()) { + m_projection->clear(job.viewUpdateRect); + continue; + } + image.fill(0); tempPainter.setTransform(QTransform()); tempPainter.setClipRect(QRect(0,0,MASK_IMAGE_WIDTH,MASK_IMAGE_HEIGHT)); tempPainter.setTransform(m_viewConverter->documentToView() * QTransform::fromTranslate(-job.viewUpdateRect.x(), -job.viewUpdateRect.y())); m_shapeManager->paintJob(tempPainter, job, false); KoColorSpaceRegistry::instance()->rgb8() ->convertPixelsTo(image.constBits(), dstData, m_projection->colorSpace(), MASK_IMAGE_WIDTH * MASK_IMAGE_HEIGHT, KoColorConversionTransformation::internalRenderingIntent(), KoColorConversionTransformation::internalConversionFlags()); // TODO: use job.viewUpdateRect instead of MASK_IMAGE_WIDTH/HEIGHT m_projection->writeBytes(dstData, job.viewUpdateRect.x(), job.viewUpdateRect.y(), MASK_IMAGE_WIDTH, MASK_IMAGE_HEIGHT); repaintRect |= job.viewUpdateRect; } delete[] dstData; m_projection->purgeDefaultPixels(); m_parentLayer->setDirty(repaintRect); m_hasChangedWhileBeingInvisible |= !m_parentLayer->visible(true); } void KisShapeLayerCanvas::forceRepaint() { /** * WARNING! Although forceRepaint() may be called from different threads, it is * not entirely safe. If the user plays with shapes at the same time (vector tools are * not ported to strokes yet), the shapes my be accessed from two different places at * the same time, which will cause a crash. * * The only real solution to this is to port vector tools to strokes framework. */ if (hasPendingUpdates()) { m_asyncUpdateSignalCompressor.stop(); m_safeForcedConnection.start(); } } bool KisShapeLayerCanvas::hasPendingUpdates() const { return m_hasUpdateInCompressor; } void KisShapeLayerCanvas::forceRepaintWithHiddenAreas() { KIS_SAFE_ASSERT_RECOVER_RETURN(m_parentLayer->image()); KIS_SAFE_ASSERT_RECOVER_RETURN(!m_isDestroying); { QMutexLocker locker(&m_dirtyRegionMutex); m_forceUpdateHiddenAreasOnly = true; } m_asyncUpdateSignalCompressor.stop(); m_safeForcedConnection.start(); } void KisShapeLayerCanvas::resetCache() { m_projection->clear(); QList shapes = m_shapeManager->shapes(); Q_FOREACH (const KoShape* shape, shapes) { shape->update(); } } void KisShapeLayerCanvas::rerenderAfterBeingInvisible() { KIS_SAFE_ASSERT_RECOVER_RETURN(m_parentLayer->visible(true)); m_hasChangedWhileBeingInvisible = false; resetCache(); }