diff --git a/libs/global/kis_assert.h b/libs/global/kis_assert.h index c37a383a80..c0047ef3f6 100644 --- a/libs/global/kis_assert.h +++ b/libs/global/kis_assert.h @@ -1,143 +1,143 @@ /* * Copyright (c) 2013 Dmitry Kazakov * * 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. */ #ifndef __KIS_ASSERT_H #define __KIS_ASSERT_H #include #include KRITAGLOBAL_EXPORT void kis_assert_exception(const char *assertion, const char *file, int line); KRITAGLOBAL_EXPORT void kis_assert_recoverable(const char *assertion, const char *file, int line); KRITAGLOBAL_EXPORT void kis_assert_x_exception(const char *assertion, const char *where, const char *what, const char *file, int line); KRITAGLOBAL_EXPORT void kis_safe_assert_recoverable(const char *assertion, const char *file, int line); /** * KIS_ASSERT family of macros allows the user to choose whether to * try to continue working in Krita or to abort an application and see * a backtrace. * * Note, the macro are present in Release mode by default! */ /** * Checks the condition and depending on the user action either aborts * the program or throws an exception, which restarts event loop. */ #define KIS_ASSERT(cond) ((!(cond)) ? kis_assert_exception(#cond,__FILE__,__LINE__) : qt_noop()) /** * Same as KIS_ASSERT, but allows to show more text to the user. * * \see KIS_ASSERT */ #define KIS_ASSERT_X(cond, where, what) ((!(cond)) ? kis_assert_x_exception(#cond,where, what,__FILE__,__LINE__) : qt_noop()) /** * This is a recoverable variant of KIS_ASSERT. It doesn't throw any * exceptions. It checks the condition, and either aborts the * application, or executes user-supplied code. The typical usecase is * the following: * * int fooBar = ...; * KIS_ASSERT_RECOVER (fooBar > 0) { * // the code which is executed in a case of emergency * } * */ #define KIS_ASSERT_RECOVER(cond) if (!(cond) && (kis_assert_recoverable(#cond,__FILE__,__LINE__), true)) /** * Equivalent of the following: * * KIS_ASSERT_RECOVER(cond) { * break; * } * */ -#define KIS_ASSERT_RECOVER_BREAK(cond) KIS_ASSERT_RECOVER(cond) { break; } +#define KIS_ASSERT_RECOVER_BREAK(cond) do { KIS_ASSERT_RECOVER(cond) { break; } } while (0) /** * Equivalent of the following: * * KIS_ASSERT_RECOVER(cond) { * return; * } * */ -#define KIS_ASSERT_RECOVER_RETURN(cond) KIS_ASSERT_RECOVER(cond) { return; } +#define KIS_ASSERT_RECOVER_RETURN(cond) do { KIS_ASSERT_RECOVER(cond) { return; } } while (0) /** * Equivalent of the following: * * KIS_ASSERT_RECOVER(cond) { * return val; * } * */ -#define KIS_ASSERT_RECOVER_RETURN_VALUE(cond, val) KIS_ASSERT_RECOVER(cond) { return (val); } +#define KIS_ASSERT_RECOVER_RETURN_VALUE(cond, val) do { KIS_ASSERT_RECOVER(cond) { return (val); } } while (0) /** * Does nothing in case of a failure. Just continues execution. * * Equivalent of the following: * * KIS_ASSERT_RECOVER(cond) { * qt_noop(); * } * */ -#define KIS_ASSERT_RECOVER_NOOP(cond) KIS_ASSERT_RECOVER(cond) { qt_noop(); } +#define KIS_ASSERT_RECOVER_NOOP(cond) do { KIS_ASSERT_RECOVER(cond) { qt_noop(); } } while (0) /** * This set of macros work in exactly the same way as their non-safe * counterparts, but they are automatically converted into console * warnings in release builds. That is the user will not see any * message box, just a warning message will be printed in a terminal * and a recovery branch will be taken automatically. * * Rules when to use "safe" asserts. Use them if the following * conditions are met: * * 1) The condition in the assert shows that a real *bug* has * happened. It is not just a warning. It is a bug that should be * fixed. * * 2) The recovery branch will *workaround* the bug and the user will * be able to continue his work *as if nothing has * happened*. Again, please mark the assert "safe" if and only if * you are 100% sure Krita will not crash in a minute after you * faced that bug. The user is not notified about this bug, so he * is not going to take any emergency steps like saving his work * and restarting Krita! * * 3) If you think that Krita should be better restarted after this * bug, please use a usual KIS_ASSERT_RECOVER. */ #define KIS_SAFE_ASSERT_RECOVER(cond) if (!(cond) && (kis_safe_assert_recoverable(#cond,__FILE__,__LINE__), true)) -#define KIS_SAFE_ASSERT_RECOVER_BREAK(cond) KIS_SAFE_ASSERT_RECOVER(cond) { break; } -#define KIS_SAFE_ASSERT_RECOVER_RETURN(cond) KIS_SAFE_ASSERT_RECOVER(cond) { return; } -#define KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(cond, val) KIS_SAFE_ASSERT_RECOVER(cond) { return (val); } -#define KIS_SAFE_ASSERT_RECOVER_NOOP(cond) KIS_SAFE_ASSERT_RECOVER(cond) { qt_noop(); } +#define KIS_SAFE_ASSERT_RECOVER_BREAK(cond) do { KIS_SAFE_ASSERT_RECOVER(cond) { break; } } while (0) +#define KIS_SAFE_ASSERT_RECOVER_RETURN(cond) do { KIS_SAFE_ASSERT_RECOVER(cond) { return; } } while (0) +#define KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(cond, val) do { KIS_SAFE_ASSERT_RECOVER(cond) { return (val); } } while (0) +#define KIS_SAFE_ASSERT_RECOVER_NOOP(cond) do { KIS_SAFE_ASSERT_RECOVER(cond) { qt_noop(); } } while (0) #endif /* __KIS_ASSERT_H */ diff --git a/libs/image/KisRecycleProjectionsJob.cpp b/libs/image/KisRecycleProjectionsJob.cpp index c2041b0da8..0d46b0bdf9 100644 --- a/libs/image/KisRecycleProjectionsJob.cpp +++ b/libs/image/KisRecycleProjectionsJob.cpp @@ -1,48 +1,53 @@ /* * Copyright (c) 2019 Dmitry Kazakov * * 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 "KisRecycleProjectionsJob.h" #include "KisSafeNodeProjectionStore.h" #include "kis_paint_device.h" KisRecycleProjectionsJob::KisRecycleProjectionsJob(KisSafeNodeProjectionStoreBaseWSP projectionStore) : m_projectionStore(projectionStore) { setExclusive(true); } bool KisRecycleProjectionsJob::overrides(const KisSpontaneousJob *_otherJob) { const KisRecycleProjectionsJob *otherJob = dynamic_cast(_otherJob); return otherJob && otherJob->m_projectionStore == m_projectionStore; } void KisRecycleProjectionsJob::run() { KisSafeNodeProjectionStoreBaseSP store = m_projectionStore; if (store) { store->recycleProjectionsInSafety(); } } int KisRecycleProjectionsJob::levelOfDetail() const { return 0; } + +QString KisRecycleProjectionsJob::debugName() const +{ + return "KisRecycleProjectionsJob"; +} diff --git a/libs/image/KisRecycleProjectionsJob.h b/libs/image/KisRecycleProjectionsJob.h index 85c2a03332..c0a88183cc 100644 --- a/libs/image/KisRecycleProjectionsJob.h +++ b/libs/image/KisRecycleProjectionsJob.h @@ -1,48 +1,50 @@ /* * Copyright (c) 2019 Dmitry Kazakov * * 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. */ #ifndef KISRECYCLEPROJECTIONSJOB_H #define KISRECYCLEPROJECTIONSJOB_H #include "kis_types.h" #include "kis_spontaneous_job.h" class QMutex; class KisSafeNodeProjectionStoreBase; typedef KisWeakSharedPtr KisSafeNodeProjectionStoreBaseWSP; /** * This is a simple job for initiating recycling of KisLayer's * projections in an exclusive context. The problem is that the * projection might still be used by update workers after it * has been disabled. So this cleaning should run in an exclusive * context. */ class KRITAIMAGE_EXPORT KisRecycleProjectionsJob : public KisSpontaneousJob { public: KisRecycleProjectionsJob(KisSafeNodeProjectionStoreBaseWSP projectionStore); bool overrides(const KisSpontaneousJob *otherJob) override; void run() override; int levelOfDetail() const override; + QString debugName() const override; + private: KisSafeNodeProjectionStoreBaseWSP m_projectionStore; }; #endif // KISRECYCLEPROJECTIONSJOB_H diff --git a/libs/image/KisRunnableBasedStrokeStrategy.cpp b/libs/image/KisRunnableBasedStrokeStrategy.cpp index 42e9f9b26d..53105f2da1 100644 --- a/libs/image/KisRunnableBasedStrokeStrategy.cpp +++ b/libs/image/KisRunnableBasedStrokeStrategy.cpp @@ -1,79 +1,79 @@ /* * Copyright (c) 2017 Dmitry Kazakov * * 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 "KisRunnableBasedStrokeStrategy.h" #include #include #include "KisRunnableStrokeJobData.h" #include "KisRunnableStrokeJobsInterface.h" struct KisRunnableBasedStrokeStrategy::JobsInterface : public KisRunnableStrokeJobsInterface { JobsInterface(KisRunnableBasedStrokeStrategy *q) : m_q(q) { } void addRunnableJobs(const QVector &list) { QVector newList; Q_FOREACH (KisRunnableStrokeJobDataBase *item, list) { newList.append(item); } m_q->addMutatedJobs(newList); } private: KisRunnableBasedStrokeStrategy *m_q; }; -KisRunnableBasedStrokeStrategy::KisRunnableBasedStrokeStrategy(QString id, const KUndo2MagicString &name) +KisRunnableBasedStrokeStrategy::KisRunnableBasedStrokeStrategy(const QLatin1String &id, const KUndo2MagicString &name) : KisSimpleStrokeStrategy(id, name), m_jobsInterface(new JobsInterface(this)) { } KisRunnableBasedStrokeStrategy::KisRunnableBasedStrokeStrategy(const KisRunnableBasedStrokeStrategy &rhs) : KisSimpleStrokeStrategy(rhs), m_jobsInterface(new JobsInterface(this)) { } KisRunnableBasedStrokeStrategy::~KisRunnableBasedStrokeStrategy() { } void KisRunnableBasedStrokeStrategy::doStrokeCallback(KisStrokeJobData *data) { if (!data) return; KisRunnableStrokeJobDataBase *runnable = dynamic_cast(data); if (!runnable) return; runnable->run(); } KisRunnableStrokeJobsInterface *KisRunnableBasedStrokeStrategy::runnableJobsInterface() const { return m_jobsInterface.data(); } diff --git a/libs/image/KisRunnableBasedStrokeStrategy.h b/libs/image/KisRunnableBasedStrokeStrategy.h index 46cd031ae0..476ac466b1 100644 --- a/libs/image/KisRunnableBasedStrokeStrategy.h +++ b/libs/image/KisRunnableBasedStrokeStrategy.h @@ -1,44 +1,44 @@ /* * Copyright (c) 2017 Dmitry Kazakov * * 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. */ #ifndef KISRUNNABLEBASEDSTROKESTRATEGY_H #define KISRUNNABLEBASEDSTROKESTRATEGY_H #include "kis_simple_stroke_strategy.h" class KisRunnableStrokeJobsInterface; class KRITAIMAGE_EXPORT KisRunnableBasedStrokeStrategy : public KisSimpleStrokeStrategy { private: struct JobsInterface; public: - KisRunnableBasedStrokeStrategy(QString id, const KUndo2MagicString &name = KUndo2MagicString()); + KisRunnableBasedStrokeStrategy(const QLatin1String &id, const KUndo2MagicString &name = KUndo2MagicString()); KisRunnableBasedStrokeStrategy(const KisRunnableBasedStrokeStrategy &rhs); ~KisRunnableBasedStrokeStrategy(); void doStrokeCallback(KisStrokeJobData *data) override; KisRunnableStrokeJobsInterface *runnableJobsInterface() const; private: const QScopedPointer m_jobsInterface; }; #endif // KISRUNNABLEBASEDSTROKESTRATEGY_H diff --git a/libs/image/KisSafeNodeProjectionStore.cpp b/libs/image/KisSafeNodeProjectionStore.cpp index 1c1677fb56..ee754419cf 100644 --- a/libs/image/KisSafeNodeProjectionStore.cpp +++ b/libs/image/KisSafeNodeProjectionStore.cpp @@ -1,284 +1,284 @@ /* * Copyright (c) 2019 Dmitry Kazakov * * 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 "KisSafeNodeProjectionStore.h" #include #include #include #include "kis_image.h" #include "kis_paint_device.h" #include "kis_selection.h" #include "KisRecycleProjectionsJob.h" /**********************************************************************/ /* StoreImplementaionInterface */ /**********************************************************************/ struct StoreImplementaionInterface { virtual ~StoreImplementaionInterface() {} virtual StoreImplementaionInterface* clone() const = 0; virtual bool releaseDevice() = 0; virtual void discardCaches() = 0; virtual void recycleProjectionsInSafety() = 0; }; /**********************************************************************/ /* StoreImplementaion */ /**********************************************************************/ template struct StoreImplementation : public StoreImplementaionInterface { - bool releaseDevice() { + bool releaseDevice() override { bool hasDeletedProjection = false; if (m_projection) { m_dirtyProjections.append(m_projection); m_projection = 0; hasDeletedProjection = true; // qDebug() << "release a device"; } return hasDeletedProjection; } - virtual void discardCaches() { + virtual void discardCaches() override { // qDebug() << "discard caches"; m_dirtyProjections.clear(); } - virtual void recycleProjectionsInSafety() { + virtual void recycleProjectionsInSafety() override { // qDebug() << "recycle caches"; Q_FOREACH (DeviceSP projection, m_dirtyProjections) { projection->clear(); m_cleanProjections.append(projection); } m_dirtyProjections.clear(); } protected: DeviceSP m_projection; QVector m_dirtyProjections; QVector m_cleanProjections; }; /**********************************************************************/ /* StoreImplementaionForDevice */ /**********************************************************************/ struct StoreImplementationForDevice : StoreImplementation { StoreImplementationForDevice() {} StoreImplementationForDevice(const KisPaintDevice &prototype) { m_projection = new KisPaintDevice(prototype); } - StoreImplementaionInterface* clone() const { + StoreImplementaionInterface* clone() const override { return m_projection ? new StoreImplementationForDevice(*m_projection) : new StoreImplementationForDevice(); } KisPaintDeviceSP getDeviceLazy(KisPaintDeviceSP prototype) { if(!m_projection || *m_projection->colorSpace() != *prototype->colorSpace()) { if (!m_cleanProjections.isEmpty()) { m_projection = m_cleanProjections.takeLast(); m_projection->makeCloneFromRough(prototype, prototype->extent()); } else { m_projection = new KisPaintDevice(*prototype); } m_projection->setProjectionDevice(true); } return m_projection; } }; /**********************************************************************/ /* StoreImplementaionForSelection */ /**********************************************************************/ struct StoreImplementationForSelection : StoreImplementation { StoreImplementationForSelection() {} StoreImplementationForSelection(const KisSelection &prototype) { m_projection = new KisSelection(prototype); } - StoreImplementaionInterface* clone() const { + StoreImplementaionInterface* clone() const override { return m_projection ? new StoreImplementationForSelection(*m_projection) : new StoreImplementationForSelection(); } KisSelectionSP getDeviceLazy(KisSelectionSP prototype) { if(!m_projection) { if (!m_cleanProjections.isEmpty()) { m_projection = m_cleanProjections.takeLast(); m_projection->pixelSelection()->makeCloneFromRough(prototype->pixelSelection(), prototype->selectedRect()); } else { m_projection = new KisSelection(*prototype); } m_projection->pixelSelection()->setProjectionDevice(true); } return m_projection; } }; /**********************************************************************/ /* KisSafeNodeProjectionStoreBase */ /**********************************************************************/ struct KisSafeNodeProjectionStoreBase::Private { mutable QMutex lock; KisImageWSP image; QScopedPointer store; }; KisSafeNodeProjectionStoreBase::KisSafeNodeProjectionStoreBase(StoreImplementaionInterface *storeImpl) : m_d(new Private) { m_d->store.reset(storeImpl); moveToThread(qApp->thread()); connect(this, SIGNAL(internalInitiateProjectionsCleanup()), this, SLOT(slotInitiateProjectionsCleanup())); } KisSafeNodeProjectionStoreBase::KisSafeNodeProjectionStoreBase(const KisSafeNodeProjectionStoreBase &rhs) : QObject(), KisShared(), m_d(new Private) { { QMutexLocker rhsLocker(&rhs.m_d->lock); m_d->image = rhs.m_d->image; m_d->store.reset(rhs.m_d->store->clone()); } moveToThread(qApp->thread()); connect(this, SIGNAL(internalInitiateProjectionsCleanup()), this, SLOT(slotInitiateProjectionsCleanup())); } KisSafeNodeProjectionStoreBase::~KisSafeNodeProjectionStoreBase() { } void KisSafeNodeProjectionStoreBase::releaseDevice() { QMutexLocker locker(&m_d->lock); if (m_d->store->releaseDevice()) { locker.unlock(); emit internalInitiateProjectionsCleanup(); } } void KisSafeNodeProjectionStoreBase::setImage(KisImageWSP image) { m_d->image = image; } void KisSafeNodeProjectionStoreBase::slotInitiateProjectionsCleanup() { /** * After the projection has been used, we should clean it. But we cannot * clean it until all the workers accessing it have completed their job. * * Therefore we just schedule an exclusive job that will execute the * recycling action in an exclusive context, when no jobs are running. */ KisImageSP image = m_d->image; if (image) { image->addSpontaneousJob(new KisRecycleProjectionsJob(this)); } else { discardCaches(); } } void KisSafeNodeProjectionStoreBase::discardCaches() { QMutexLocker locker(&m_d->lock); m_d->store->discardCaches(); } void KisSafeNodeProjectionStoreBase::recycleProjectionsInSafety() { QMutexLocker locker(&m_d->lock); m_d->store->recycleProjectionsInSafety(); } /**********************************************************************/ /* KisSafeNodeProjectionStore */ /**********************************************************************/ KisSafeNodeProjectionStore::KisSafeNodeProjectionStore() : KisSafeNodeProjectionStoreBase(new StoreImplementationForDevice) { } KisSafeNodeProjectionStore::KisSafeNodeProjectionStore(const KisSafeNodeProjectionStore &rhs) : KisSafeNodeProjectionStoreBase(rhs) { } KisPaintDeviceSP KisSafeNodeProjectionStore::getDeviceLazy(KisPaintDeviceSP prototype) { QMutexLocker locker(&m_d->lock); StoreImplementationForDevice *store = dynamic_cast(m_d->store.data()); KIS_ASSERT(store); return store->getDeviceLazy(prototype); } /**********************************************************************/ /* KisSafeSelectionNodeProjectionStore */ /**********************************************************************/ KisSafeSelectionNodeProjectionStore::KisSafeSelectionNodeProjectionStore() : KisSafeNodeProjectionStoreBase(new StoreImplementationForSelection) { } KisSafeSelectionNodeProjectionStore::KisSafeSelectionNodeProjectionStore(const KisSafeSelectionNodeProjectionStore &rhs) : KisSafeNodeProjectionStoreBase(rhs) { } KisSelectionSP KisSafeSelectionNodeProjectionStore::getDeviceLazy(KisSelectionSP prototype) { QMutexLocker locker(&m_d->lock); StoreImplementationForSelection *store = dynamic_cast(m_d->store.data()); KIS_ASSERT(store); return store->getDeviceLazy(prototype); } diff --git a/libs/image/commands/kis_node_property_list_command.cpp b/libs/image/commands/kis_node_property_list_command.cpp index 94d48a5c6f..66a4ad11d2 100644 --- a/libs/image/commands/kis_node_property_list_command.cpp +++ b/libs/image/commands/kis_node_property_list_command.cpp @@ -1,227 +1,228 @@ /* * Copyright (c) 2009 Cyrille Berger * * 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 #include "kis_node.h" #include "kis_layer.h" #include "kis_image.h" #include "kis_selection_mask.h" #include "kis_paint_layer.h" #include "commands/kis_node_property_list_command.h" #include "kis_undo_adapter.h" #include "kis_layer_properties_icons.h" // HACK! please refactor out! #include "kis_simple_stroke_strategy.h" KisNodePropertyListCommand::KisNodePropertyListCommand(KisNodeSP node, KisBaseNode::PropertyList newPropertyList) : KisNodeCommand(kundo2_i18n("Property Changes"), node), m_newPropertyList(newPropertyList), m_oldPropertyList(node->sectionModelProperties()) /** * TODO instead of "Property Changes" check which property * has been changed and display either lock/unlock, visible/hidden * or "Property Changes" (this require new strings) */ { } void KisNodePropertyListCommand::redo() { const KisBaseNode::PropertyList propsBefore = m_node->sectionModelProperties(); const QRect oldExtent = m_node->extent(); m_node->setSectionModelProperties(m_newPropertyList); doUpdate(propsBefore, m_node->sectionModelProperties(), oldExtent | m_node->extent()); } void KisNodePropertyListCommand::undo() { const KisBaseNode::PropertyList propsBefore = m_node->sectionModelProperties(); const QRect oldExtent = m_node->extent(); m_node->setSectionModelProperties(m_oldPropertyList); doUpdate(propsBefore, m_node->sectionModelProperties(), oldExtent | m_node->extent()); } bool checkOnionSkinChanged(const KisBaseNode::PropertyList &oldPropertyList, const KisBaseNode::PropertyList &newPropertyList) { if (oldPropertyList.size() != newPropertyList.size()) return false; bool oldOnionSkinsValue = false; bool newOnionSkinsValue = false; Q_FOREACH (const KisBaseNode::Property &prop, oldPropertyList) { if (prop.id == KisLayerPropertiesIcons::onionSkins.id()) { oldOnionSkinsValue = prop.state.toBool(); } } Q_FOREACH (const KisBaseNode::Property &prop, newPropertyList) { if (prop.id == KisLayerPropertiesIcons::onionSkins.id()) { newOnionSkinsValue = prop.state.toBool(); } } return oldOnionSkinsValue != newOnionSkinsValue; } void KisNodePropertyListCommand::doUpdate(const KisBaseNode::PropertyList &oldPropertyList, const KisBaseNode::PropertyList &newPropertyList, const QRect &totalUpdateExtent) { /** * Sometimes the node might refuse to change the property, e.g. needs-update for colorize * mask. In this case we should avoid issuing the update and set-modified call. */ if (oldPropertyList == newPropertyList) { return; } bool oldPassThroughValue = false; bool newPassThroughValue = false; bool oldVisibilityValue = false; bool newVisibilityValue = false; Q_FOREACH (const KisBaseNode::Property &prop, oldPropertyList) { if (prop.id == KisLayerPropertiesIcons::passThrough.id()) { oldPassThroughValue = prop.state.toBool(); } if (prop.id == KisLayerPropertiesIcons::visible.id()) { oldVisibilityValue = prop.state.toBool(); } } Q_FOREACH (const KisBaseNode::Property &prop, newPropertyList) { if (prop.id == KisLayerPropertiesIcons::passThrough.id()) { newPassThroughValue = prop.state.toBool(); } if (prop.id == KisLayerPropertiesIcons::visible.id()) { newVisibilityValue = prop.state.toBool(); } } if (oldPassThroughValue && !newPassThroughValue) { KisLayerSP layer(qobject_cast(m_node.data())); KisImageSP image = layer->image().toStrongRef(); if (image) { image->refreshGraphAsync(layer); } } else if ((m_node->parent() && !oldPassThroughValue && newPassThroughValue) || (oldPassThroughValue && newPassThroughValue && !oldVisibilityValue && newVisibilityValue)) { KisLayerSP layer(qobject_cast(m_node->parent().data())); KisImageSP image = layer->image().toStrongRef(); if (image) { image->refreshGraphAsync(layer); } } else if (checkOnionSkinChanged(oldPropertyList, newPropertyList)) { m_node->setDirtyDontResetAnimationCache(totalUpdateExtent); } else { m_node->setDirty(totalUpdateExtent); // TODO check if visibility was actually changed or not } } void KisNodePropertyListCommand::setNodePropertiesNoUndo(KisNodeSP node, KisImageSP image, PropertyList proplist) { QVector undo; Q_FOREACH (const KisBaseNode::Property &prop, proplist) { if (prop.isInStasis) undo << false; if (prop.name == i18n("Visible") && node->visible() != prop.state.toBool()) { undo << false; continue; } else if (prop.name == i18n("Locked") && node->userLocked() != prop.state.toBool()) { undo << false; continue; } else if (prop.name == i18n("Active")) { if (KisSelectionMask *m = dynamic_cast(node.data())) { if (m->active() != prop.state.toBool()) { undo << false; continue; } } } else if (prop.name == i18n("Alpha Locked")) { if (KisPaintLayer* l = dynamic_cast(node.data())) { if (l->alphaLocked() != prop.state.toBool()) { undo << false; continue; } } } // This property is known, but it hasn't got the same value, and it isn't one of // the previous properties, so we need to add the command to the undo list. Q_FOREACH(const KisBaseNode::Property &p2, node->sectionModelProperties()) { if (p2.name == prop.name && p2.state != prop.state) { undo << true; break; } } } QScopedPointer cmd(new KisNodePropertyListCommand(node, proplist)); image->setModified(); if (undo.contains(true)) { image->undoAdapter()->addCommand(cmd.take()); } else { /** * HACK ALERT! * * Here we start a fake legacy stroke, so that all the LoD planes would * be invalidated. Ideally, we should refactor this method and avoid * resetting LoD planes when node visibility changes, Instead there should * be two commands executes: LoD agnostic one (which sets the properties * themselves), and two LoD-specific update commands: one for lodN and * another one for lod0. */ struct SimpleLodResettingStroke : public KisSimpleStrokeStrategy { SimpleLodResettingStroke(KUndo2Command *cmd) - : m_cmd(cmd) + : KisSimpleStrokeStrategy(QLatin1String("SimpleLodResettingStroke")), + m_cmd(cmd) { setClearsRedoOnStart(false); this->enableJob(JOB_INIT, true); } void initStrokeCallback() { m_cmd->redo(); } private: QScopedPointer m_cmd; }; KisStrokeId strokeId = image->startStroke(new SimpleLodResettingStroke(cmd.take())); image->endStroke(strokeId); } } diff --git a/libs/image/kis_image.cc b/libs/image/kis_image.cc index 43a191a9c7..1baa06810b 100644 --- a/libs/image/kis_image.cc +++ b/libs/image/kis_image.cc @@ -1,2235 +1,2236 @@ /* * Copyright (c) 2002 Patrick Julien * 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_image.h" #include // WORDS_BIGENDIAN #include #include #include #include #include #include #include #include #include #include #include "KoColorSpaceRegistry.h" #include "KoColor.h" #include "KoColorProfile.h" #include #include "KisProofingConfiguration.h" #include "kis_adjustment_layer.h" #include "kis_annotation.h" #include "kis_count_visitor.h" #include "kis_filter_strategy.h" #include "kis_group_layer.h" #include "commands/kis_image_commands.h" #include "kis_layer.h" #include "kis_meta_data_merge_strategy_registry.h" #include "kis_name_server.h" #include "kis_paint_layer.h" #include "kis_projection_leaf.h" #include "kis_painter.h" #include "kis_selection.h" #include "kis_transaction.h" #include "kis_meta_data_merge_strategy.h" #include "kis_memory_statistics_server.h" #include "kis_image_config.h" #include "kis_update_scheduler.h" #include "kis_image_signal_router.h" #include "kis_image_animation_interface.h" #include "kis_stroke_strategy.h" #include "kis_simple_stroke_strategy.h" #include "kis_image_barrier_locker.h" #include "kis_undo_stores.h" #include "kis_legacy_undo_adapter.h" #include "kis_post_execution_undo_adapter.h" #include "kis_transform_worker.h" #include "kis_processing_applicator.h" #include "processing/kis_crop_processing_visitor.h" #include "processing/kis_crop_selections_processing_visitor.h" #include "processing/kis_transform_processing_visitor.h" #include "processing/kis_convert_color_space_processing_visitor.h" #include "processing/kis_assign_profile_processing_visitor.h" #include "commands_new/kis_image_resize_command.h" #include "commands_new/kis_image_set_resolution_command.h" #include "commands_new/kis_activate_selection_mask_command.h" #include "kis_do_something_command.h" #include "kis_composite_progress_proxy.h" #include "kis_layer_composition.h" #include "kis_wrapped_rect.h" #include "kis_crop_saved_extra_data.h" #include "kis_layer_utils.h" #include "kis_lod_transform.h" #include "kis_suspend_projection_updates_stroke_strategy.h" #include "kis_sync_lod_cache_stroke_strategy.h" #include "kis_projection_updates_filter.h" #include "kis_layer_projection_plane.h" #include "kis_update_time_monitor.h" #include "tiles3/kis_lockless_stack.h" #include #include #include "kis_time_range.h" #include "KisRunnableBasedStrokeStrategy.h" #include "KisRunnableStrokeJobData.h" #include "KisRunnableStrokeJobUtils.h" #include "KisRunnableStrokeJobsInterface.h" // #define SANITY_CHECKS #ifdef SANITY_CHECKS #define SANITY_CHECK_LOCKED(name) \ if (!locked()) warnKrita() << "Locking policy failed:" << name \ << "has been called without the image" \ "being locked"; #else #define SANITY_CHECK_LOCKED(name) #endif struct KisImageSPStaticRegistrar { KisImageSPStaticRegistrar() { qRegisterMetaType("KisImageSP"); } }; static KisImageSPStaticRegistrar __registrar; class KisImage::KisImagePrivate { public: KisImagePrivate(KisImage *_q, qint32 w, qint32 h, const KoColorSpace *c, KisUndoStore *undo, KisImageAnimationInterface *_animationInterface) : q(_q) , lockedForReadOnly(false) , width(w) , height(h) , colorSpace(c ? c : KoColorSpaceRegistry::instance()->rgb8()) , nserver(1) , undoStore(undo ? undo : new KisDumbUndoStore()) , legacyUndoAdapter(undoStore.data(), _q) , postExecutionUndoAdapter(undoStore.data(), _q) , signalRouter(_q) , animationInterface(_animationInterface) , scheduler(_q, _q) , axesCenter(QPointF(0.5, 0.5)) { { KisImageConfig cfg(true); if (cfg.enableProgressReporting()) { scheduler.setProgressProxy(&compositeProgressProxy); } // Each of these lambdas defines a new factory function. scheduler.setLod0ToNStrokeStrategyFactory( [=](bool forgettable) { return KisLodSyncPair( new KisSyncLodCacheStrokeStrategy(KisImageWSP(q), forgettable), KisSyncLodCacheStrokeStrategy::createJobsData(KisImageWSP(q))); }); scheduler.setSuspendUpdatesStrokeStrategyFactory( [=]() { return KisSuspendResumePair( new KisSuspendProjectionUpdatesStrokeStrategy(KisImageWSP(q), true), KisSuspendProjectionUpdatesStrokeStrategy::createSuspendJobsData(KisImageWSP(q))); }); scheduler.setResumeUpdatesStrokeStrategyFactory( [=]() { return KisSuspendResumePair( new KisSuspendProjectionUpdatesStrokeStrategy(KisImageWSP(q), false), KisSuspendProjectionUpdatesStrokeStrategy::createResumeJobsData(KisImageWSP(q))); }); } connect(q, SIGNAL(sigImageModified()), KisMemoryStatisticsServer::instance(), SLOT(notifyImageChanged())); } ~KisImagePrivate() { /** * Stop animation interface. It may use the rootLayer. */ delete animationInterface; /** * First delete the nodes, while strokes * and undo are still alive */ rootLayer.clear(); } KisImage *q; quint32 lockCount = 0; bool lockedForReadOnly; qint32 width; qint32 height; double xres = 1.0; double yres = 1.0; const KoColorSpace * colorSpace; KisProofingConfigurationSP proofingConfig; KisSelectionSP deselectedGlobalSelection; KisGroupLayerSP rootLayer; // The layers are contained in here KisSelectionMaskSP targetOverlaySelectionMask; // the overlay switching stroke will try to switch into this mask KisSelectionMaskSP overlaySelectionMask; QList compositions; KisNodeSP isolatedRootNode; bool wrapAroundModePermitted = false; KisNameServer nserver; QScopedPointer undoStore; KisLegacyUndoAdapter legacyUndoAdapter; KisPostExecutionUndoAdapter postExecutionUndoAdapter; vKisAnnotationSP annotations; QAtomicInt disableUIUpdateSignals; KisLocklessStack savedDisabledUIUpdates; KisProjectionUpdatesFilterSP projectionUpdatesFilter; KisImageSignalRouter signalRouter; KisImageAnimationInterface *animationInterface; KisUpdateScheduler scheduler; QAtomicInt disableDirtyRequests; KisCompositeProgressProxy compositeProgressProxy; bool blockLevelOfDetail = false; QPointF axesCenter; bool allowMasksOnRootNode = false; bool tryCancelCurrentStrokeAsync(); void notifyProjectionUpdatedInPatches(const QRect &rc, QVector &jobs); void convertImageColorSpaceImpl(const KoColorSpace *dstColorSpace, bool convertLayers, KoColorConversionTransformation::Intent renderingIntent, KoColorConversionTransformation::ConversionFlags conversionFlags); struct SetImageProjectionColorSpace; }; KisImage::KisImage(KisUndoStore *undoStore, qint32 width, qint32 height, const KoColorSpace * colorSpace, const QString& name) : QObject(0) , KisShared() , m_d(new KisImagePrivate(this, width, height, colorSpace, undoStore, new KisImageAnimationInterface(this))) { // make sure KisImage belongs to the GUI thread moveToThread(qApp->thread()); connect(this, SIGNAL(sigInternalStopIsolatedModeRequested()), SLOT(stopIsolatedMode())); setObjectName(name); setRootLayer(new KisGroupLayer(this, "root", OPACITY_OPAQUE_U8)); } KisImage::~KisImage() { dbgImage << "deleting kisimage" << objectName(); /** * Request the tools to end currently running strokes */ waitForDone(); delete m_d; disconnect(); // in case Qt gets confused } KisImageSP KisImage::fromQImage(const QImage &image, KisUndoStore *undoStore) { const KoColorSpace *colorSpace = 0; switch (image.format()) { case QImage::Format_Invalid: case QImage::Format_Mono: case QImage::Format_MonoLSB: colorSpace = KoColorSpaceRegistry::instance()->graya8(); break; case QImage::Format_Indexed8: case QImage::Format_RGB32: case QImage::Format_ARGB32: case QImage::Format_ARGB32_Premultiplied: colorSpace = KoColorSpaceRegistry::instance()->rgb8(); break; case QImage::Format_RGB16: colorSpace = KoColorSpaceRegistry::instance()->rgb16(); break; case QImage::Format_ARGB8565_Premultiplied: case QImage::Format_RGB666: case QImage::Format_ARGB6666_Premultiplied: case QImage::Format_RGB555: case QImage::Format_ARGB8555_Premultiplied: case QImage::Format_RGB888: case QImage::Format_RGB444: case QImage::Format_ARGB4444_Premultiplied: case QImage::Format_RGBX8888: case QImage::Format_RGBA8888: case QImage::Format_RGBA8888_Premultiplied: colorSpace = KoColorSpaceRegistry::instance()->rgb8(); break; case QImage::Format_BGR30: case QImage::Format_A2BGR30_Premultiplied: case QImage::Format_RGB30: case QImage::Format_A2RGB30_Premultiplied: colorSpace = KoColorSpaceRegistry::instance()->rgb8(); break; case QImage::Format_Alpha8: colorSpace = KoColorSpaceRegistry::instance()->alpha8(); break; case QImage::Format_Grayscale8: colorSpace = KoColorSpaceRegistry::instance()->graya8(); break; #if QT_VERSION >= QT_VERSION_CHECK(5, 13, 0) case QImage::Format_Grayscale16: colorSpace = KoColorSpaceRegistry::instance()->graya16(); break; #endif #if QT_VERSION >= QT_VERSION_CHECK(5, 12, 0) case QImage::Format_RGBX64: case QImage::Format_RGBA64: case QImage::Format_RGBA64_Premultiplied: colorSpace = KoColorSpaceRegistry::instance()->colorSpace(RGBAColorModelID.id(), Float32BitsColorDepthID.id(), 0); break; #endif default: colorSpace = 0; } KisImageSP img = new KisImage(undoStore, image.width(), image.height(), colorSpace, i18n("Imported Image")); KisPaintLayerSP layer = new KisPaintLayer(img, img->nextLayerName(), 255); layer->paintDevice()->convertFromQImage(image, 0, 0, 0); img->addNode(layer.data(), img->rootLayer().data()); return img; } KisImage *KisImage::clone(bool exactCopy) { return new KisImage(*this, 0, exactCopy); } void KisImage::copyFromImage(const KisImage &rhs) { copyFromImageImpl(rhs, REPLACE); } void KisImage::copyFromImageImpl(const KisImage &rhs, int policy) { // make sure we choose exactly one from REPLACE and CONSTRUCT KIS_ASSERT_RECOVER_RETURN((policy & REPLACE) != (policy & CONSTRUCT)); // only when replacing do we need to emit signals #define EMIT_IF_NEEDED if (!(policy & REPLACE)) {} else emit if (policy & REPLACE) { // if we are constructing the image, these are already set if (m_d->width != rhs.width() || m_d->height != rhs.height()) { m_d->width = rhs.width(); m_d->height = rhs.height(); emit sigSizeChanged(QPointF(), QPointF()); } if (m_d->colorSpace != rhs.colorSpace()) { m_d->colorSpace = rhs.colorSpace(); emit sigColorSpaceChanged(m_d->colorSpace); } } // from KisImage::KisImage(const KisImage &, KisUndoStore *, bool) setObjectName(rhs.objectName()); if (m_d->xres != rhs.m_d->xres || m_d->yres != rhs.m_d->yres) { m_d->xres = rhs.m_d->xres; m_d->yres = rhs.m_d->yres; EMIT_IF_NEEDED sigResolutionChanged(m_d->xres, m_d->yres); } m_d->allowMasksOnRootNode = rhs.m_d->allowMasksOnRootNode; if (rhs.m_d->proofingConfig) { KisProofingConfigurationSP proofingConfig(new KisProofingConfiguration(*rhs.m_d->proofingConfig)); if (policy & REPLACE) { setProofingConfiguration(proofingConfig); } else { m_d->proofingConfig = proofingConfig; } } KisNodeSP newRoot = rhs.root()->clone(); newRoot->setGraphListener(this); newRoot->setImage(this); m_d->rootLayer = dynamic_cast(newRoot.data()); setRoot(newRoot); bool exactCopy = policy & EXACT_COPY; - if (exactCopy || rhs.m_d->isolatedRootNode) { + if (exactCopy || rhs.m_d->isolatedRootNode || rhs.m_d->overlaySelectionMask) { QQueue linearizedNodes; KisLayerUtils::recursiveApplyNodes(rhs.root(), [&linearizedNodes](KisNodeSP node) { linearizedNodes.enqueue(node); }); KisLayerUtils::recursiveApplyNodes(newRoot, [&linearizedNodes, exactCopy, &rhs, this](KisNodeSP node) { KisNodeSP refNode = linearizedNodes.dequeue(); if (exactCopy) { node->setUuid(refNode->uuid()); } if (rhs.m_d->isolatedRootNode && rhs.m_d->isolatedRootNode == refNode) { m_d->isolatedRootNode = node; } + + if (rhs.m_d->overlaySelectionMask && + KisNodeSP(rhs.m_d->overlaySelectionMask) == refNode) { + m_d->targetOverlaySelectionMask = dynamic_cast(node.data()); + m_d->overlaySelectionMask = m_d->targetOverlaySelectionMask; + m_d->rootLayer->notifyChildMaskChanged(); + } }); } KisLayerUtils::recursiveApplyNodes(newRoot, [](KisNodeSP node) { dbgImage << "Node: " << (void *)node.data(); }); m_d->compositions.clear(); Q_FOREACH (KisLayerCompositionSP comp, rhs.m_d->compositions) { m_d->compositions << toQShared(new KisLayerComposition(*comp, this)); } EMIT_IF_NEEDED sigLayersChangedAsync(); m_d->nserver = rhs.m_d->nserver; vKisAnnotationSP newAnnotations; Q_FOREACH (KisAnnotationSP annotation, rhs.m_d->annotations) { newAnnotations << annotation->clone(); } m_d->annotations = newAnnotations; KIS_ASSERT_RECOVER_NOOP(!rhs.m_d->projectionUpdatesFilter); KIS_ASSERT_RECOVER_NOOP(!rhs.m_d->disableUIUpdateSignals); KIS_ASSERT_RECOVER_NOOP(!rhs.m_d->disableDirtyRequests); m_d->blockLevelOfDetail = rhs.m_d->blockLevelOfDetail; - /** - * The overlay device is not inherited when cloning the image! - */ - if (rhs.m_d->overlaySelectionMask) { - const QRect dirtyRect = rhs.m_d->overlaySelectionMask->extent(); - m_d->rootLayer->setDirty(dirtyRect); - } #undef EMIT_IF_NEEDED } KisImage::KisImage(const KisImage& rhs, KisUndoStore *undoStore, bool exactCopy) : KisNodeFacade(), KisNodeGraphListener(), KisShared(), m_d(new KisImagePrivate(this, rhs.width(), rhs.height(), rhs.colorSpace(), undoStore ? undoStore : new KisDumbUndoStore(), new KisImageAnimationInterface(*rhs.animationInterface(), this))) { // make sure KisImage belongs to the GUI thread moveToThread(qApp->thread()); connect(this, SIGNAL(sigInternalStopIsolatedModeRequested()), SLOT(stopIsolatedMode())); copyFromImageImpl(rhs, CONSTRUCT | (exactCopy ? EXACT_COPY : 0)); } void KisImage::aboutToAddANode(KisNode *parent, int index) { KisNodeGraphListener::aboutToAddANode(parent, index); SANITY_CHECK_LOCKED("aboutToAddANode"); } void KisImage::nodeHasBeenAdded(KisNode *parent, int index) { KisNodeGraphListener::nodeHasBeenAdded(parent, index); SANITY_CHECK_LOCKED("nodeHasBeenAdded"); m_d->signalRouter.emitNodeHasBeenAdded(parent, index); } void KisImage::aboutToRemoveANode(KisNode *parent, int index) { KisNodeSP deletedNode = parent->at(index); if (!dynamic_cast(deletedNode.data()) && deletedNode == m_d->isolatedRootNode) { emit sigInternalStopIsolatedModeRequested(); } KisNodeGraphListener::aboutToRemoveANode(parent, index); SANITY_CHECK_LOCKED("aboutToRemoveANode"); m_d->signalRouter.emitAboutToRemoveANode(parent, index); } void KisImage::nodeChanged(KisNode* node) { KisNodeGraphListener::nodeChanged(node); requestStrokeEnd(); m_d->signalRouter.emitNodeChanged(node); } void KisImage::invalidateAllFrames() { invalidateFrames(KisTimeRange::infinite(0), QRect()); } void KisImage::setOverlaySelectionMask(KisSelectionMaskSP mask) { if (m_d->targetOverlaySelectionMask == mask) return; m_d->targetOverlaySelectionMask = mask; struct UpdateOverlaySelectionStroke : public KisSimpleStrokeStrategy { UpdateOverlaySelectionStroke(KisImageSP image) - : KisSimpleStrokeStrategy("update-overlay-selection-mask", kundo2_noi18n("update-overlay-selection-mask")), + : KisSimpleStrokeStrategy(QLatin1String("update-overlay-selection-mask"), kundo2_noi18n("update-overlay-selection-mask")), m_image(image) { this->enableJob(JOB_INIT, true, KisStrokeJobData::BARRIER, KisStrokeJobData::EXCLUSIVE); setClearsRedoOnStart(false); } void initStrokeCallback() { KisSelectionMaskSP oldMask = m_image->m_d->overlaySelectionMask; KisSelectionMaskSP newMask = m_image->m_d->targetOverlaySelectionMask; if (oldMask == newMask) return; KIS_SAFE_ASSERT_RECOVER_RETURN(!newMask || newMask->graphListener() == m_image); m_image->m_d->overlaySelectionMask = newMask; if (oldMask || newMask) { m_image->m_d->rootLayer->notifyChildMaskChanged(); } if (oldMask) { m_image->m_d->rootLayer->setDirtyDontResetAnimationCache(oldMask->extent()); } if (newMask) { newMask->setDirty(); } m_image->undoAdapter()->emitSelectionChanged(); } private: KisImageSP m_image; }; KisStrokeId id = startStroke(new UpdateOverlaySelectionStroke(this)); endStroke(id); } KisSelectionMaskSP KisImage::overlaySelectionMask() const { return m_d->overlaySelectionMask; } bool KisImage::hasOverlaySelectionMask() const { return m_d->overlaySelectionMask; } KisSelectionSP KisImage::globalSelection() const { KisSelectionMaskSP selectionMask = m_d->rootLayer->selectionMask(); if (selectionMask) { return selectionMask->selection(); } else { return 0; } } void KisImage::setGlobalSelection(KisSelectionSP globalSelection) { KisSelectionMaskSP selectionMask = m_d->rootLayer->selectionMask(); if (!globalSelection) { if (selectionMask) { removeNode(selectionMask); } } else { if (!selectionMask) { selectionMask = new KisSelectionMask(this); selectionMask->initSelection(m_d->rootLayer); addNode(selectionMask); // If we do not set the selection now, the setActive call coming next // can be very, very expensive, depending on the size of the image. selectionMask->setSelection(globalSelection); selectionMask->setActive(true); } else { selectionMask->setSelection(globalSelection); } KIS_SAFE_ASSERT_RECOVER_NOOP(m_d->rootLayer->childCount() > 0); KIS_SAFE_ASSERT_RECOVER_NOOP(m_d->rootLayer->selectionMask()); } m_d->deselectedGlobalSelection = 0; m_d->legacyUndoAdapter.emitSelectionChanged(); } void KisImage::deselectGlobalSelection() { KisSelectionSP savedSelection = globalSelection(); setGlobalSelection(0); m_d->deselectedGlobalSelection = savedSelection; } bool KisImage::canReselectGlobalSelection() { return m_d->deselectedGlobalSelection; } void KisImage::reselectGlobalSelection() { if(m_d->deselectedGlobalSelection) { setGlobalSelection(m_d->deselectedGlobalSelection); } } QString KisImage::nextLayerName(const QString &_baseName) const { QString baseName = _baseName; if (m_d->nserver.currentSeed() == 0) { m_d->nserver.number(); return i18n("background"); } if (baseName.isEmpty()) { baseName = i18n("Layer"); } return QString("%1 %2").arg(baseName).arg(m_d->nserver.number()); } void KisImage::rollBackLayerName() { m_d->nserver.rollback(); } KisCompositeProgressProxy* KisImage::compositeProgressProxy() { return &m_d->compositeProgressProxy; } bool KisImage::locked() const { return m_d->lockCount != 0; } void KisImage::barrierLock(bool readOnly) { if (!locked()) { requestStrokeEnd(); m_d->scheduler.barrierLock(); m_d->lockedForReadOnly = readOnly; } else { m_d->lockedForReadOnly &= readOnly; } m_d->lockCount++; } bool KisImage::tryBarrierLock(bool readOnly) { bool result = true; if (!locked()) { result = m_d->scheduler.tryBarrierLock(); m_d->lockedForReadOnly = readOnly; } if (result) { m_d->lockCount++; m_d->lockedForReadOnly &= readOnly; } return result; } bool KisImage::isIdle(bool allowLocked) { return (allowLocked || !locked()) && m_d->scheduler.isIdle(); } void KisImage::lock() { if (!locked()) { requestStrokeEnd(); m_d->scheduler.lock(); } m_d->lockCount++; m_d->lockedForReadOnly = false; } void KisImage::unlock() { Q_ASSERT(locked()); if (locked()) { m_d->lockCount--; if (m_d->lockCount == 0) { m_d->scheduler.unlock(!m_d->lockedForReadOnly); } } } void KisImage::blockUpdates() { m_d->scheduler.blockUpdates(); } void KisImage::unblockUpdates() { m_d->scheduler.unblockUpdates(); } void KisImage::setSize(const QSize& size) { m_d->width = size.width(); m_d->height = size.height(); } void KisImage::resizeImageImpl(const QRect& newRect, bool cropLayers) { if (newRect == bounds() && !cropLayers) return; KUndo2MagicString actionName = cropLayers ? kundo2_i18n("Crop Image") : kundo2_i18n("Resize Image"); KisImageSignalVector emitSignals; emitSignals << ComplexSizeChangedSignal(newRect, newRect.size()); emitSignals << ModifiedSignal; KisCropSavedExtraData *extraData = new KisCropSavedExtraData(cropLayers ? KisCropSavedExtraData::CROP_IMAGE : KisCropSavedExtraData::RESIZE_IMAGE, newRect); KisProcessingApplicator applicator(this, m_d->rootLayer, KisProcessingApplicator::RECURSIVE | KisProcessingApplicator::NO_UI_UPDATES, emitSignals, actionName, extraData); if (cropLayers || !newRect.topLeft().isNull()) { KisProcessingVisitorSP visitor = new KisCropProcessingVisitor(newRect, cropLayers, true); applicator.applyVisitorAllFrames(visitor, KisStrokeJobData::CONCURRENT); } applicator.applyCommand(new KisImageResizeCommand(this, newRect.size())); applicator.end(); } void KisImage::resizeImage(const QRect& newRect) { resizeImageImpl(newRect, false); } void KisImage::cropImage(const QRect& newRect) { resizeImageImpl(newRect, true); } void KisImage::cropNode(KisNodeSP node, const QRect& newRect) { bool isLayer = qobject_cast(node.data()); KUndo2MagicString actionName = isLayer ? kundo2_i18n("Crop Layer") : kundo2_i18n("Crop Mask"); KisImageSignalVector emitSignals; emitSignals << ModifiedSignal; KisCropSavedExtraData *extraData = new KisCropSavedExtraData(KisCropSavedExtraData::CROP_LAYER, newRect, node); KisProcessingApplicator applicator(this, node, KisProcessingApplicator::RECURSIVE, emitSignals, actionName, extraData); KisProcessingVisitorSP visitor = new KisCropProcessingVisitor(newRect, true, false); applicator.applyVisitorAllFrames(visitor, KisStrokeJobData::CONCURRENT); applicator.end(); } void KisImage::scaleImage(const QSize &size, qreal xres, qreal yres, KisFilterStrategy *filterStrategy) { bool resolutionChanged = xres != xRes() && yres != yRes(); bool sizeChanged = size != this->size(); if (!resolutionChanged && !sizeChanged) return; KisImageSignalVector emitSignals; if (resolutionChanged) emitSignals << ResolutionChangedSignal; if (sizeChanged) emitSignals << ComplexSizeChangedSignal(bounds(), size); emitSignals << ModifiedSignal; KUndo2MagicString actionName = sizeChanged ? kundo2_i18n("Scale Image") : kundo2_i18n("Change Image Resolution"); KisProcessingApplicator::ProcessingFlags signalFlags = (resolutionChanged || sizeChanged) ? KisProcessingApplicator::NO_UI_UPDATES : KisProcessingApplicator::NONE; KisProcessingApplicator applicator(this, m_d->rootLayer, KisProcessingApplicator::RECURSIVE | signalFlags, emitSignals, actionName); qreal sx = qreal(size.width()) / this->size().width(); qreal sy = qreal(size.height()) / this->size().height(); QTransform shapesCorrection; if (resolutionChanged) { shapesCorrection = QTransform::fromScale(xRes() / xres, yRes() / yres); } KisProcessingVisitorSP visitor = new KisTransformProcessingVisitor(sx, sy, 0, 0, QPointF(), 0, 0, 0, filterStrategy, shapesCorrection); applicator.applyVisitorAllFrames(visitor, KisStrokeJobData::CONCURRENT); if (resolutionChanged) { KUndo2Command *parent = new KisResetShapesCommand(m_d->rootLayer); new KisImageSetResolutionCommand(this, xres, yres, parent); applicator.applyCommand(parent); } if (sizeChanged) { applicator.applyCommand(new KisImageResizeCommand(this, size)); } applicator.end(); } void KisImage::scaleNode(KisNodeSP node, const QPointF ¢er, qreal scaleX, qreal scaleY, KisFilterStrategy *filterStrategy, KisSelectionSP selection) { KUndo2MagicString actionName(kundo2_i18n("Scale Layer")); KisImageSignalVector emitSignals; emitSignals << ModifiedSignal; QPointF offset; { KisTransformWorker worker(0, scaleX, scaleY, 0, 0, 0, 0, 0.0, 0, 0, 0, 0); QTransform transform = worker.transform(); offset = center - transform.map(center); } KisProcessingApplicator applicator(this, node, KisProcessingApplicator::RECURSIVE, emitSignals, actionName); KisTransformProcessingVisitor *visitor = new KisTransformProcessingVisitor(scaleX, scaleY, 0, 0, QPointF(), 0, offset.x(), offset.y(), filterStrategy); visitor->setSelection(selection); if (selection) { applicator.applyVisitor(visitor, KisStrokeJobData::CONCURRENT); } else { applicator.applyVisitorAllFrames(visitor, KisStrokeJobData::CONCURRENT); } applicator.end(); } void KisImage::rotateImpl(const KUndo2MagicString &actionName, KisNodeSP rootNode, double radians, bool resizeImage, KisSelectionSP selection) { // we can either transform (and resize) the whole image or // transform a selection, we cannot do both at the same time KIS_SAFE_ASSERT_RECOVER(!(bool(selection) && resizeImage)) { selection = 0; } const QRect baseBounds = resizeImage ? bounds() : selection ? selection->selectedExactRect() : rootNode->exactBounds(); QPointF offset; QSize newSize; { KisTransformWorker worker(0, 1.0, 1.0, 0, 0, 0, 0, radians, 0, 0, 0, 0); QTransform transform = worker.transform(); if (resizeImage) { QRect newRect = transform.mapRect(baseBounds); newSize = newRect.size(); offset = -newRect.topLeft(); } else { QPointF origin = QRectF(baseBounds).center(); newSize = size(); offset = -(transform.map(origin) - origin); } } bool sizeChanged = resizeImage && (newSize.width() != baseBounds.width() || newSize.height() != baseBounds.height()); // These signals will be emitted after processing is done KisImageSignalVector emitSignals; if (sizeChanged) emitSignals << ComplexSizeChangedSignal(baseBounds, newSize); emitSignals << ModifiedSignal; // These flags determine whether updates are transferred to the UI during processing KisProcessingApplicator::ProcessingFlags signalFlags = sizeChanged ? KisProcessingApplicator::NO_UI_UPDATES : KisProcessingApplicator::NONE; KisProcessingApplicator applicator(this, rootNode, KisProcessingApplicator::RECURSIVE | signalFlags, emitSignals, actionName); KisFilterStrategy *filter = KisFilterStrategyRegistry::instance()->value("Bicubic"); KisTransformProcessingVisitor *visitor = new KisTransformProcessingVisitor(1.0, 1.0, 0.0, 0.0, QPointF(), radians, offset.x(), offset.y(), filter); if (selection) { visitor->setSelection(selection); } if (selection) { applicator.applyVisitor(visitor, KisStrokeJobData::CONCURRENT); } else { applicator.applyVisitorAllFrames(visitor, KisStrokeJobData::CONCURRENT); } if (sizeChanged) { applicator.applyCommand(new KisImageResizeCommand(this, newSize)); } applicator.end(); } void KisImage::rotateImage(double radians) { rotateImpl(kundo2_i18n("Rotate Image"), root(), radians, true, 0); } void KisImage::rotateNode(KisNodeSP node, double radians, KisSelectionSP selection) { if (node->inherits("KisMask")) { rotateImpl(kundo2_i18n("Rotate Mask"), node, radians, false, selection); } else { rotateImpl(kundo2_i18n("Rotate Layer"), node, radians, false, selection); } } void KisImage::shearImpl(const KUndo2MagicString &actionName, KisNodeSP rootNode, bool resizeImage, double angleX, double angleY, KisSelectionSP selection) { const QRect baseBounds = resizeImage ? bounds() : selection ? selection->selectedExactRect() : rootNode->exactBounds(); const QPointF origin = QRectF(baseBounds).center(); //angleX, angleY are in degrees const qreal pi = 3.1415926535897932385; const qreal deg2rad = pi / 180.0; qreal tanX = tan(angleX * deg2rad); qreal tanY = tan(angleY * deg2rad); QPointF offset; QSize newSize; { KisTransformWorker worker(0, 1.0, 1.0, tanX, tanY, origin.x(), origin.y(), 0, 0, 0, 0, 0); QRect newRect = worker.transform().mapRect(baseBounds); newSize = newRect.size(); if (resizeImage) offset = -newRect.topLeft(); } if (newSize == baseBounds.size()) return; KisImageSignalVector emitSignals; if (resizeImage) emitSignals << ComplexSizeChangedSignal(baseBounds, newSize); emitSignals << ModifiedSignal; KisProcessingApplicator::ProcessingFlags signalFlags = KisProcessingApplicator::RECURSIVE; if (resizeImage) signalFlags |= KisProcessingApplicator::NO_UI_UPDATES; KisProcessingApplicator applicator(this, rootNode, signalFlags, emitSignals, actionName); KisFilterStrategy *filter = KisFilterStrategyRegistry::instance()->value("Bilinear"); KisTransformProcessingVisitor *visitor = new KisTransformProcessingVisitor(1.0, 1.0, tanX, tanY, origin, 0, offset.x(), offset.y(), filter); if (selection) { visitor->setSelection(selection); } if (selection) { applicator.applyVisitor(visitor, KisStrokeJobData::CONCURRENT); } else { applicator.applyVisitorAllFrames(visitor, KisStrokeJobData::CONCURRENT); } if (resizeImage) { applicator.applyCommand(new KisImageResizeCommand(this, newSize)); } applicator.end(); } void KisImage::shearNode(KisNodeSP node, double angleX, double angleY, KisSelectionSP selection) { if (node->inherits("KisMask")) { shearImpl(kundo2_i18n("Shear Mask"), node, false, angleX, angleY, selection); } else { shearImpl(kundo2_i18n("Shear Layer"), node, false, angleX, angleY, selection); } } void KisImage::shear(double angleX, double angleY) { shearImpl(kundo2_i18n("Shear Image"), m_d->rootLayer, true, angleX, angleY, 0); } void KisImage::convertLayerColorSpace(KisNodeSP node, const KoColorSpace *dstColorSpace, KoColorConversionTransformation::Intent renderingIntent, KoColorConversionTransformation::ConversionFlags conversionFlags) { if (!node->projectionLeaf()->isLayer()) return; const KoColorSpace *srcColorSpace = node->colorSpace(); if (!dstColorSpace || *srcColorSpace == *dstColorSpace) return; KUndo2MagicString actionName = kundo2_i18n("Convert Layer Color Space"); KisImageSignalVector emitSignals; emitSignals << ModifiedSignal; KisProcessingApplicator applicator(this, node, KisProcessingApplicator::RECURSIVE | KisProcessingApplicator::NO_UI_UPDATES, emitSignals, actionName); applicator.applyVisitor( new KisConvertColorSpaceProcessingVisitor( srcColorSpace, dstColorSpace, renderingIntent, conversionFlags), KisStrokeJobData::CONCURRENT); applicator.end(); } struct KisImage::KisImagePrivate::SetImageProjectionColorSpace : public KisCommandUtils::FlipFlopCommand { SetImageProjectionColorSpace(const KoColorSpace *cs, KisImageWSP image, State initialState, KUndo2Command *parent = 0) : KisCommandUtils::FlipFlopCommand(initialState, parent), m_cs(cs), m_image(image) { } void partA() override { KisImageSP image = m_image; if (image) { image->setProjectionColorSpace(m_cs); } } private: const KoColorSpace *m_cs; KisImageWSP m_image; }; void KisImage::KisImagePrivate::convertImageColorSpaceImpl(const KoColorSpace *dstColorSpace, bool convertLayers, KoColorConversionTransformation::Intent renderingIntent, KoColorConversionTransformation::ConversionFlags conversionFlags) { const KoColorSpace *srcColorSpace = this->colorSpace; if (!dstColorSpace || *srcColorSpace == *dstColorSpace) return; const KUndo2MagicString actionName = convertLayers ? kundo2_i18n("Convert Image Color Space") : kundo2_i18n("Convert Projection Color Space"); KisImageSignalVector emitSignals; emitSignals << ColorSpaceChangedSignal; emitSignals << ModifiedSignal; KisProcessingApplicator applicator(q, this->rootLayer, KisProcessingApplicator::RECURSIVE | KisProcessingApplicator::NO_UI_UPDATES, emitSignals, actionName); applicator.applyCommand( new KisImagePrivate::SetImageProjectionColorSpace(dstColorSpace, KisImageWSP(q), KisCommandUtils::FlipFlopCommand::INITIALIZING), KisStrokeJobData::BARRIER); if (convertLayers) { applicator.applyVisitor( new KisConvertColorSpaceProcessingVisitor( srcColorSpace, dstColorSpace, renderingIntent, conversionFlags), KisStrokeJobData::CONCURRENT); } else { applicator.applyCommand( new KisDoSomethingCommand< KisDoSomethingCommandOps::ResetOp, KisGroupLayerSP> (this->rootLayer, false)); applicator.applyCommand( new KisDoSomethingCommand< KisDoSomethingCommandOps::ResetOp, KisGroupLayerSP> (this->rootLayer, true)); } applicator.applyCommand( new KisImagePrivate::SetImageProjectionColorSpace(srcColorSpace, KisImageWSP(q), KisCommandUtils::FlipFlopCommand::FINALIZING), KisStrokeJobData::BARRIER); applicator.end(); } void KisImage::convertImageColorSpace(const KoColorSpace *dstColorSpace, KoColorConversionTransformation::Intent renderingIntent, KoColorConversionTransformation::ConversionFlags conversionFlags) { m_d->convertImageColorSpaceImpl(dstColorSpace, true, renderingIntent, conversionFlags); } void KisImage::convertImageProjectionColorSpace(const KoColorSpace *dstColorSpace) { m_d->convertImageColorSpaceImpl(dstColorSpace, false, KoColorConversionTransformation::internalRenderingIntent(), KoColorConversionTransformation::internalConversionFlags()); } bool KisImage::assignLayerProfile(KisNodeSP node, const KoColorProfile *profile) { const KoColorSpace *srcColorSpace = node->colorSpace(); if (!node->projectionLeaf()->isLayer()) return false; if (!profile || *srcColorSpace->profile() == *profile) return false; KUndo2MagicString actionName = kundo2_i18n("Assign Profile to Layer"); KisImageSignalVector emitSignals; emitSignals << ModifiedSignal; const KoColorSpace *dstColorSpace = KoColorSpaceRegistry::instance()->colorSpace(colorSpace()->colorModelId().id(), colorSpace()->colorDepthId().id(), profile); if (!dstColorSpace) return false; KisProcessingApplicator applicator(this, node, KisProcessingApplicator::RECURSIVE | KisProcessingApplicator::NO_UI_UPDATES, emitSignals, actionName); applicator.applyVisitor( new KisAssignProfileProcessingVisitor( srcColorSpace, dstColorSpace), KisStrokeJobData::CONCURRENT); applicator.end(); return true; } bool KisImage::assignImageProfile(const KoColorProfile *profile, bool blockAllUpdates) { if (!profile) return false; const KoColorSpace *srcColorSpace = m_d->colorSpace; bool imageProfileIsSame = *srcColorSpace->profile() == *profile; imageProfileIsSame &= !KisLayerUtils::recursiveFindNode(m_d->rootLayer, [profile] (KisNodeSP node) { return *node->colorSpace()->profile() != *profile; }); if (imageProfileIsSame) { dbgImage << "Trying to set the same image profile again" << ppVar(srcColorSpace->profile()->name()) << ppVar(profile->name()); return true; } KUndo2MagicString actionName = kundo2_i18n("Assign Profile"); KisImageSignalVector emitSignals; emitSignals << ProfileChangedSignal; emitSignals << ModifiedSignal; const KoColorSpace *dstColorSpace = KoColorSpaceRegistry::instance()->colorSpace(colorSpace()->colorModelId().id(), colorSpace()->colorDepthId().id(), profile); if (!dstColorSpace) return false; KisProcessingApplicator applicator(this, m_d->rootLayer, KisProcessingApplicator::RECURSIVE | (!blockAllUpdates ? KisProcessingApplicator::NO_UI_UPDATES : KisProcessingApplicator::NO_IMAGE_UPDATES), emitSignals, actionName); applicator.applyCommand( new KisImagePrivate::SetImageProjectionColorSpace(dstColorSpace, KisImageWSP(this), KisCommandUtils::FlipFlopCommand::INITIALIZING), KisStrokeJobData::BARRIER); applicator.applyVisitor( new KisAssignProfileProcessingVisitor( srcColorSpace, dstColorSpace), KisStrokeJobData::CONCURRENT); applicator.applyCommand( new KisImagePrivate::SetImageProjectionColorSpace(srcColorSpace, KisImageWSP(this), KisCommandUtils::FlipFlopCommand::FINALIZING), KisStrokeJobData::BARRIER); applicator.end(); return true; } void KisImage::setProjectionColorSpace(const KoColorSpace * colorSpace) { m_d->colorSpace = colorSpace; } const KoColorSpace * KisImage::colorSpace() const { return m_d->colorSpace; } const KoColorProfile * KisImage::profile() const { return colorSpace()->profile(); } double KisImage::xRes() const { return m_d->xres; } double KisImage::yRes() const { return m_d->yres; } void KisImage::setResolution(double xres, double yres) { m_d->xres = xres; m_d->yres = yres; m_d->signalRouter.emitNotification(ResolutionChangedSignal); } QPointF KisImage::documentToPixel(const QPointF &documentCoord) const { return QPointF(documentCoord.x() * xRes(), documentCoord.y() * yRes()); } QPoint KisImage::documentToImagePixelFloored(const QPointF &documentCoord) const { QPointF pixelCoord = documentToPixel(documentCoord); return QPoint(qFloor(pixelCoord.x()), qFloor(pixelCoord.y())); } QRectF KisImage::documentToPixel(const QRectF &documentRect) const { return QRectF(documentToPixel(documentRect.topLeft()), documentToPixel(documentRect.bottomRight())); } QPointF KisImage::pixelToDocument(const QPointF &pixelCoord) const { return QPointF(pixelCoord.x() / xRes(), pixelCoord.y() / yRes()); } QPointF KisImage::pixelToDocument(const QPoint &pixelCoord) const { return QPointF((pixelCoord.x() + 0.5) / xRes(), (pixelCoord.y() + 0.5) / yRes()); } QRectF KisImage::pixelToDocument(const QRectF &pixelCoord) const { return QRectF(pixelToDocument(pixelCoord.topLeft()), pixelToDocument(pixelCoord.bottomRight())); } qint32 KisImage::width() const { return m_d->width; } qint32 KisImage::height() const { return m_d->height; } KisGroupLayerSP KisImage::rootLayer() const { Q_ASSERT(m_d->rootLayer); return m_d->rootLayer; } KisPaintDeviceSP KisImage::projection() const { if (m_d->isolatedRootNode) { return m_d->isolatedRootNode->projection(); } Q_ASSERT(m_d->rootLayer); KisPaintDeviceSP projection = m_d->rootLayer->projection(); Q_ASSERT(projection); return projection; } qint32 KisImage::nlayers() const { QStringList list; list << "KisLayer"; KisCountVisitor visitor(list, KoProperties()); m_d->rootLayer->accept(visitor); return visitor.count(); } qint32 KisImage::nHiddenLayers() const { QStringList list; list << "KisLayer"; KoProperties properties; properties.setProperty("visible", false); KisCountVisitor visitor(list, properties); m_d->rootLayer->accept(visitor); return visitor.count(); } void KisImage::flatten(KisNodeSP activeNode) { KisLayerUtils::flattenImage(this, activeNode); } void KisImage::mergeMultipleLayers(QList mergedNodes, KisNodeSP putAfter) { if (!KisLayerUtils::tryMergeSelectionMasks(this, mergedNodes, putAfter)) { KisLayerUtils::mergeMultipleLayers(this, mergedNodes, putAfter); } } void KisImage::mergeDown(KisLayerSP layer, const KisMetaData::MergeStrategy* strategy) { KisLayerUtils::mergeDown(this, layer, strategy); } void KisImage::flattenLayer(KisLayerSP layer) { KisLayerUtils::flattenLayer(this, layer); } void KisImage::setModified() { m_d->signalRouter.emitNotification(ModifiedSignal); } QImage KisImage::convertToQImage(QRect imageRect, const KoColorProfile * profile) { qint32 x; qint32 y; qint32 w; qint32 h; imageRect.getRect(&x, &y, &w, &h); return convertToQImage(x, y, w, h, profile); } QImage KisImage::convertToQImage(qint32 x, qint32 y, qint32 w, qint32 h, const KoColorProfile * profile) { KisPaintDeviceSP dev = projection(); if (!dev) return QImage(); QImage image = dev->convertToQImage(const_cast(profile), x, y, w, h, KoColorConversionTransformation::internalRenderingIntent(), KoColorConversionTransformation::internalConversionFlags()); return image; } QImage KisImage::convertToQImage(const QSize& scaledImageSize, const KoColorProfile *profile) { if (scaledImageSize.isEmpty()) { return QImage(); } KisPaintDeviceSP dev = new KisPaintDevice(colorSpace()); KisPainter gc; gc.copyAreaOptimized(QPoint(0, 0), projection(), dev, bounds()); gc.end(); double scaleX = qreal(scaledImageSize.width()) / width(); double scaleY = qreal(scaledImageSize.height()) / height(); QPointer updater = new KoDummyUpdater(); KisTransformWorker worker(dev, scaleX, scaleY, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, updater, KisFilterStrategyRegistry::instance()->value("Bicubic")); worker.run(); delete updater; return dev->convertToQImage(profile); } void KisImage::notifyLayersChanged() { m_d->signalRouter.emitNotification(LayersChangedSignal); } QRect KisImage::bounds() const { return QRect(0, 0, width(), height()); } QRect KisImage::effectiveLodBounds() const { QRect boundRect = bounds(); const int lod = currentLevelOfDetail(); if (lod > 0) { KisLodTransform t(lod); boundRect = t.map(boundRect); } return boundRect; } KisPostExecutionUndoAdapter* KisImage::postExecutionUndoAdapter() const { const int lod = currentLevelOfDetail(); return lod > 0 ? m_d->scheduler.lodNPostExecutionUndoAdapter() : &m_d->postExecutionUndoAdapter; } const KUndo2Command* KisImage::lastExecutedCommand() const { return m_d->undoStore->presentCommand(); } void KisImage::setUndoStore(KisUndoStore *undoStore) { m_d->legacyUndoAdapter.setUndoStore(undoStore); m_d->postExecutionUndoAdapter.setUndoStore(undoStore); m_d->undoStore.reset(undoStore); } KisUndoStore* KisImage::undoStore() { return m_d->undoStore.data(); } KisUndoAdapter* KisImage::undoAdapter() const { return &m_d->legacyUndoAdapter; } void KisImage::setDefaultProjectionColor(const KoColor &color) { KIS_ASSERT_RECOVER_RETURN(m_d->rootLayer); m_d->rootLayer->setDefaultProjectionColor(color); } KoColor KisImage::defaultProjectionColor() const { KIS_ASSERT_RECOVER(m_d->rootLayer) { return KoColor(Qt::transparent, m_d->colorSpace); } return m_d->rootLayer->defaultProjectionColor(); } void KisImage::setRootLayer(KisGroupLayerSP rootLayer) { emit sigInternalStopIsolatedModeRequested(); KoColor defaultProjectionColor(Qt::transparent, m_d->colorSpace); if (m_d->rootLayer) { m_d->rootLayer->setGraphListener(0); m_d->rootLayer->disconnect(); KisPaintDeviceSP original = m_d->rootLayer->original(); defaultProjectionColor = original->defaultPixel(); } m_d->rootLayer = rootLayer; m_d->rootLayer->disconnect(); m_d->rootLayer->setGraphListener(this); m_d->rootLayer->setImage(this); setRoot(m_d->rootLayer.data()); this->setDefaultProjectionColor(defaultProjectionColor); } void KisImage::addAnnotation(KisAnnotationSP annotation) { // Find the icc annotation, if there is one vKisAnnotationSP_it it = m_d->annotations.begin(); while (it != m_d->annotations.end()) { if ((*it)->type() == annotation->type()) { *it = annotation; return; } ++it; } m_d->annotations.push_back(annotation); } KisAnnotationSP KisImage::annotation(const QString& type) { vKisAnnotationSP_it it = m_d->annotations.begin(); while (it != m_d->annotations.end()) { if ((*it)->type() == type) { return *it; } ++it; } return KisAnnotationSP(0); } void KisImage::removeAnnotation(const QString& type) { vKisAnnotationSP_it it = m_d->annotations.begin(); while (it != m_d->annotations.end()) { if ((*it)->type() == type) { m_d->annotations.erase(it); return; } ++it; } } vKisAnnotationSP_it KisImage::beginAnnotations() { return m_d->annotations.begin(); } vKisAnnotationSP_it KisImage::endAnnotations() { return m_d->annotations.end(); } void KisImage::notifyAboutToBeDeleted() { emit sigAboutToBeDeleted(); } KisImageSignalRouter* KisImage::signalRouter() { return &m_d->signalRouter; } void KisImage::waitForDone() { requestStrokeEnd(); m_d->scheduler.waitForDone(); } KisStrokeId KisImage::startStroke(KisStrokeStrategy *strokeStrategy) { /** * Ask open strokes to end gracefully. All the strokes clients * (including the one calling this method right now) will get * a notification that they should probably end their strokes. * However this is purely their choice whether to end a stroke * or not. */ if (strokeStrategy->requestsOtherStrokesToEnd()) { requestStrokeEnd(); } /** * Some of the strokes can cancel their work with undoing all the * changes they did to the paint devices. The problem is that undo * stack will know nothing about it. Therefore, just notify it * explicitly */ if (strokeStrategy->clearsRedoOnStart()) { m_d->undoStore->purgeRedoState(); } return m_d->scheduler.startStroke(strokeStrategy); } void KisImage::KisImagePrivate::notifyProjectionUpdatedInPatches(const QRect &rc, QVector &jobs) { KisImageConfig imageConfig(true); int patchWidth = imageConfig.updatePatchWidth(); int patchHeight = imageConfig.updatePatchHeight(); for (int y = 0; y < rc.height(); y += patchHeight) { for (int x = 0; x < rc.width(); x += patchWidth) { QRect patchRect(x, y, patchWidth, patchHeight); patchRect &= rc; KritaUtils::addJobConcurrent(jobs, std::bind(&KisImage::notifyProjectionUpdated, q, patchRect)); } } } bool KisImage::startIsolatedMode(KisNodeSP node) { struct StartIsolatedModeStroke : public KisRunnableBasedStrokeStrategy { StartIsolatedModeStroke(KisNodeSP node, KisImageSP image) - : KisRunnableBasedStrokeStrategy("start-isolated-mode", kundo2_noi18n("start-isolated-mode")), + : KisRunnableBasedStrokeStrategy(QLatin1String("start-isolated-mode"), + kundo2_noi18n("start-isolated-mode")), m_node(node), m_image(image) { this->enableJob(JOB_INIT, true, KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::EXCLUSIVE); this->enableJob(JOB_DOSTROKE, true); setClearsRedoOnStart(false); } void initStrokeCallback() { // pass-though node don't have any projection prepared, so we should // explicitly regenerate it before activating isolated mode. m_node->projectionLeaf()->explicitlyRegeneratePassThroughProjection(); m_image->m_d->isolatedRootNode = m_node; emit m_image->sigIsolatedModeChanged(); // the GUI uses our thread to do the color space conversion so we // need to emit this signal in multiple threads QVector jobs; m_image->m_d->notifyProjectionUpdatedInPatches(m_image->bounds(), jobs); this->runnableJobsInterface()->addRunnableJobs(jobs); m_image->invalidateAllFrames(); } private: KisNodeSP m_node; KisImageSP m_image; }; KisStrokeId id = startStroke(new StartIsolatedModeStroke(node, this)); endStroke(id); return true; } void KisImage::stopIsolatedMode() { if (!m_d->isolatedRootNode) return; struct StopIsolatedModeStroke : public KisRunnableBasedStrokeStrategy { StopIsolatedModeStroke(KisImageSP image) - : KisRunnableBasedStrokeStrategy("stop-isolated-mode", kundo2_noi18n("stop-isolated-mode")), + : KisRunnableBasedStrokeStrategy(QLatin1String("stop-isolated-mode"), kundo2_noi18n("stop-isolated-mode")), m_image(image) { this->enableJob(JOB_INIT); this->enableJob(JOB_DOSTROKE, true); setClearsRedoOnStart(false); } void initStrokeCallback() { if (!m_image->m_d->isolatedRootNode) return; //KisNodeSP oldRootNode = m_image->m_d->isolatedRootNode; m_image->m_d->isolatedRootNode = 0; emit m_image->sigIsolatedModeChanged(); m_image->invalidateAllFrames(); // the GUI uses our thread to do the color space conversion so we // need to emit this signal in multiple threads QVector jobs; m_image->m_d->notifyProjectionUpdatedInPatches(m_image->bounds(), jobs); this->runnableJobsInterface()->addRunnableJobs(jobs); // TODO: Substitute notifyProjectionUpdated() with this code // when update optimization is implemented // // QRect updateRect = bounds() | oldRootNode->extent(); // oldRootNode->setDirty(updateRect); } private: KisImageSP m_image; }; KisStrokeId id = startStroke(new StopIsolatedModeStroke(this)); endStroke(id); } KisNodeSP KisImage::isolatedModeRoot() const { return m_d->isolatedRootNode; } void KisImage::addJob(KisStrokeId id, KisStrokeJobData *data) { KisUpdateTimeMonitor::instance()->reportJobStarted(data); m_d->scheduler.addJob(id, data); } void KisImage::endStroke(KisStrokeId id) { m_d->scheduler.endStroke(id); } bool KisImage::cancelStroke(KisStrokeId id) { return m_d->scheduler.cancelStroke(id); } bool KisImage::KisImagePrivate::tryCancelCurrentStrokeAsync() { return scheduler.tryCancelCurrentStrokeAsync(); } void KisImage::requestUndoDuringStroke() { emit sigUndoDuringStrokeRequested(); } void KisImage::requestStrokeCancellation() { if (!m_d->tryCancelCurrentStrokeAsync()) { emit sigStrokeCancellationRequested(); } } UndoResult KisImage::tryUndoUnfinishedLod0Stroke() { return m_d->scheduler.tryUndoLastStrokeAsync(); } void KisImage::requestStrokeEnd() { emit sigStrokeEndRequested(); emit sigStrokeEndRequestedActiveNodeFiltered(); } void KisImage::requestStrokeEndActiveNode() { emit sigStrokeEndRequested(); } void KisImage::refreshGraph(KisNodeSP root) { refreshGraph(root, bounds(), bounds()); } void KisImage::refreshGraph(KisNodeSP root, const QRect &rc, const QRect &cropRect) { if (!root) root = m_d->rootLayer; m_d->animationInterface->notifyNodeChanged(root.data(), rc, true); m_d->scheduler.fullRefresh(root, rc, cropRect); } void KisImage::initialRefreshGraph() { /** * NOTE: Tricky part. We set crop rect to null, so the clones * will not rely on precalculated projections of their sources */ refreshGraphAsync(0, bounds(), QRect()); waitForDone(); } void KisImage::refreshGraphAsync(KisNodeSP root) { refreshGraphAsync(root, bounds(), bounds()); } void KisImage::refreshGraphAsync(KisNodeSP root, const QRect &rc) { refreshGraphAsync(root, rc, bounds()); } void KisImage::refreshGraphAsync(KisNodeSP root, const QRect &rc, const QRect &cropRect) { if (!root) root = m_d->rootLayer; m_d->animationInterface->notifyNodeChanged(root.data(), rc, true); m_d->scheduler.fullRefreshAsync(root, rc, cropRect); } void KisImage::requestProjectionUpdateNoFilthy(KisNodeSP pseudoFilthy, const QRect &rc, const QRect &cropRect) { KIS_ASSERT_RECOVER_RETURN(pseudoFilthy); m_d->animationInterface->notifyNodeChanged(pseudoFilthy.data(), rc, false); m_d->scheduler.updateProjectionNoFilthy(pseudoFilthy, rc, cropRect); } void KisImage::addSpontaneousJob(KisSpontaneousJob *spontaneousJob) { m_d->scheduler.addSpontaneousJob(spontaneousJob); } bool KisImage::hasUpdatesRunning() const { return m_d->scheduler.hasUpdatesRunning(); } void KisImage::setProjectionUpdatesFilter(KisProjectionUpdatesFilterSP filter) { // update filters are *not* recursive! KIS_ASSERT_RECOVER_NOOP(!filter || !m_d->projectionUpdatesFilter); m_d->projectionUpdatesFilter = filter; } KisProjectionUpdatesFilterSP KisImage::projectionUpdatesFilter() const { return m_d->projectionUpdatesFilter; } void KisImage::disableDirtyRequests() { setProjectionUpdatesFilter(KisProjectionUpdatesFilterSP(new KisDropAllProjectionUpdatesFilter())); } void KisImage::enableDirtyRequests() { setProjectionUpdatesFilter(KisProjectionUpdatesFilterSP()); } void KisImage::disableUIUpdates() { m_d->disableUIUpdateSignals.ref(); } void KisImage::notifyBatchUpdateStarted() { m_d->signalRouter.emitNotifyBatchUpdateStarted(); } void KisImage::notifyBatchUpdateEnded() { m_d->signalRouter.emitNotifyBatchUpdateEnded(); } void KisImage::notifyUIUpdateCompleted(const QRect &rc) { notifyProjectionUpdated(rc); } QVector KisImage::enableUIUpdates() { m_d->disableUIUpdateSignals.deref(); QRect rect; QVector postponedUpdates; while (m_d->savedDisabledUIUpdates.pop(rect)) { postponedUpdates.append(rect); } return postponedUpdates; } void KisImage::notifyProjectionUpdated(const QRect &rc) { KisUpdateTimeMonitor::instance()->reportUpdateFinished(rc); if (!m_d->disableUIUpdateSignals) { int lod = currentLevelOfDetail(); QRect dirtyRect = !lod ? rc : KisLodTransform::upscaledRect(rc, lod); if (dirtyRect.isEmpty()) return; emit sigImageUpdated(dirtyRect); } else { m_d->savedDisabledUIUpdates.push(rc); } } void KisImage::setWorkingThreadsLimit(int value) { m_d->scheduler.setThreadsLimit(value); } int KisImage::workingThreadsLimit() const { return m_d->scheduler.threadsLimit(); } void KisImage::notifySelectionChanged() { /** * The selection is calculated asynchronously, so it is not * handled by disableUIUpdates() and other special signals of * KisImageSignalRouter */ m_d->legacyUndoAdapter.emitSelectionChanged(); /** * Editing of selection masks doesn't necessary produce a * setDirty() call, so in the end of the stroke we need to request * direct update of the UI's cache. */ if (m_d->isolatedRootNode && dynamic_cast(m_d->isolatedRootNode.data())) { notifyProjectionUpdated(bounds()); } } void KisImage::requestProjectionUpdateImpl(KisNode *node, const QVector &rects, const QRect &cropRect) { if (rects.isEmpty()) return; m_d->scheduler.updateProjection(node, rects, cropRect); } void KisImage::requestProjectionUpdate(KisNode *node, const QVector &rects, bool resetAnimationCache) { if (m_d->projectionUpdatesFilter && m_d->projectionUpdatesFilter->filter(this, node, rects, resetAnimationCache)) { return; } if (resetAnimationCache) { m_d->animationInterface->notifyNodeChanged(node, rects, false); } /** * Here we use 'permitted' instead of 'active' intentively, * because the updates may come after the actual stroke has been * finished. And having some more updates for the stroke not * supporting the wrap-around mode will not make much harm. */ if (m_d->wrapAroundModePermitted) { QVector allSplitRects; const QRect boundRect = effectiveLodBounds(); Q_FOREACH (const QRect &rc, rects) { KisWrappedRect splitRect(rc, boundRect); allSplitRects.append(splitRect); } requestProjectionUpdateImpl(node, allSplitRects, boundRect); } else { requestProjectionUpdateImpl(node, rects, bounds()); } KisNodeGraphListener::requestProjectionUpdate(node, rects, resetAnimationCache); } void KisImage::invalidateFrames(const KisTimeRange &range, const QRect &rect) { m_d->animationInterface->invalidateFrames(range, rect); } void KisImage::requestTimeSwitch(int time) { m_d->animationInterface->requestTimeSwitchNonGUI(time); } KisNode *KisImage::graphOverlayNode() const { return m_d->overlaySelectionMask.data(); } QList KisImage::compositions() { return m_d->compositions; } void KisImage::addComposition(KisLayerCompositionSP composition) { m_d->compositions.append(composition); } void KisImage::removeComposition(KisLayerCompositionSP composition) { m_d->compositions.removeAll(composition); } bool checkMasksNeedConversion(KisNodeSP root, const QRect &bounds) { KisSelectionMask *mask = dynamic_cast(root.data()); if (mask && (!bounds.contains(mask->paintDevice()->exactBounds()) || mask->selection()->hasShapeSelection())) { return true; } KisNodeSP node = root->firstChild(); while (node) { if (checkMasksNeedConversion(node, bounds)) { return true; } node = node->nextSibling(); } return false; } void KisImage::setWrapAroundModePermitted(bool value) { if (m_d->wrapAroundModePermitted != value) { requestStrokeEnd(); } m_d->wrapAroundModePermitted = value; if (m_d->wrapAroundModePermitted && checkMasksNeedConversion(root(), bounds())) { KisProcessingApplicator applicator(this, root(), KisProcessingApplicator::RECURSIVE, KisImageSignalVector() << ModifiedSignal, kundo2_i18n("Crop Selections")); KisProcessingVisitorSP visitor = new KisCropSelectionsProcessingVisitor(bounds()); applicator.applyVisitor(visitor, KisStrokeJobData::CONCURRENT); applicator.end(); } } bool KisImage::wrapAroundModePermitted() const { return m_d->wrapAroundModePermitted; } bool KisImage::wrapAroundModeActive() const { return m_d->wrapAroundModePermitted && m_d->scheduler.wrapAroundModeSupported(); } void KisImage::setDesiredLevelOfDetail(int lod) { if (m_d->blockLevelOfDetail) { qWarning() << "WARNING: KisImage::setDesiredLevelOfDetail()" << "was called while LoD functionality was being blocked!"; return; } m_d->scheduler.setDesiredLevelOfDetail(lod); } int KisImage::currentLevelOfDetail() const { if (m_d->blockLevelOfDetail) { return 0; } return m_d->scheduler.currentLevelOfDetail(); } void KisImage::setLevelOfDetailBlocked(bool value) { KisImageBarrierLockerRaw l(this); if (value && !m_d->blockLevelOfDetail) { m_d->scheduler.setDesiredLevelOfDetail(0); } m_d->blockLevelOfDetail = value; } void KisImage::explicitRegenerateLevelOfDetail() { if (!m_d->blockLevelOfDetail) { m_d->scheduler.explicitRegenerateLevelOfDetail(); } } bool KisImage::levelOfDetailBlocked() const { return m_d->blockLevelOfDetail; } void KisImage::nodeCollapsedChanged(KisNode * node) { Q_UNUSED(node); emit sigNodeCollapsedChanged(); } KisImageAnimationInterface* KisImage::animationInterface() const { return m_d->animationInterface; } void KisImage::setProofingConfiguration(KisProofingConfigurationSP proofingConfig) { m_d->proofingConfig = proofingConfig; emit sigProofingConfigChanged(); } KisProofingConfigurationSP KisImage::proofingConfiguration() const { if (m_d->proofingConfig) { return m_d->proofingConfig; } return KisProofingConfigurationSP(); } QPointF KisImage::mirrorAxesCenter() const { return m_d->axesCenter; } void KisImage::setMirrorAxesCenter(const QPointF &value) const { m_d->axesCenter = value; } void KisImage::setAllowMasksOnRootNode(bool value) { m_d->allowMasksOnRootNode = value; } bool KisImage::allowMasksOnRootNode() const { return m_d->allowMasksOnRootNode; } diff --git a/libs/image/kis_recalculate_generator_layer_job.cpp b/libs/image/kis_recalculate_generator_layer_job.cpp index f0e2b2fab6..9e614b6af4 100644 --- a/libs/image/kis_recalculate_generator_layer_job.cpp +++ b/libs/image/kis_recalculate_generator_layer_job.cpp @@ -1,55 +1,63 @@ /* * Copyright (c) 2014 Dmitry Kazakov * * 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_recalculate_generator_layer_job.h" #include "generator/kis_generator_layer.h" #include "kis_debug.h" #include "kis_layer.h" #include "kis_image.h" KisRecalculateGeneratorLayerJob::KisRecalculateGeneratorLayerJob(KisGeneratorLayerSP layer) : m_layer(layer) { setExclusive(true); } bool KisRecalculateGeneratorLayerJob::overrides(const KisSpontaneousJob *_otherJob) { const KisRecalculateGeneratorLayerJob *otherJob = dynamic_cast(_otherJob); return otherJob && otherJob->m_layer == m_layer; } void KisRecalculateGeneratorLayerJob::run() { /** * The layer might have been deleted from the layers stack. In * such a case, don't try do update it. */ if (!m_layer->parent()) return; m_layer->update(); } int KisRecalculateGeneratorLayerJob::levelOfDetail() const { return 0; } + +QString KisRecalculateGeneratorLayerJob::debugName() const +{ + QString result; + QDebug dbg(&result); + dbg << "KisRecalculateGeneratorLayerJob" << m_layer; + return result; +} diff --git a/libs/image/kis_recalculate_generator_layer_job.h b/libs/image/kis_recalculate_generator_layer_job.h index d98b68777a..3e9fd51515 100644 --- a/libs/image/kis_recalculate_generator_layer_job.h +++ b/libs/image/kis_recalculate_generator_layer_job.h @@ -1,39 +1,41 @@ /* * Copyright (c) 2014 Dmitry Kazakov * * 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. */ #ifndef __KIS_RECALCULATE_GENERATOR_LAYER_JOB_H #define __KIS_RECALCULATE_GENERATOR_LAYER_JOB_H #include "kis_types.h" #include "kis_spontaneous_job.h" class KRITAIMAGE_EXPORT KisRecalculateGeneratorLayerJob : public KisSpontaneousJob { public: KisRecalculateGeneratorLayerJob(KisGeneratorLayerSP layer); bool overrides(const KisSpontaneousJob *otherJob) override; void run() override; int levelOfDetail() const override; + QString debugName() const override; + private: KisGeneratorLayerSP m_layer; }; #endif /* __KIS_RECALCULATE_GENERATOR_LAYER_JOB_H */ diff --git a/libs/image/kis_recalculate_transform_mask_job.cpp b/libs/image/kis_recalculate_transform_mask_job.cpp index 7a448ca78f..a5a492c36f 100644 --- a/libs/image/kis_recalculate_transform_mask_job.cpp +++ b/libs/image/kis_recalculate_transform_mask_job.cpp @@ -1,92 +1,100 @@ /* * Copyright (c) 2014 Dmitry Kazakov * * 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_recalculate_transform_mask_job.h" #include "kis_transform_mask.h" #include "kis_debug.h" #include "kis_layer.h" #include "kis_image.h" #include "kis_abstract_projection_plane.h" #include "kis_transform_mask_params_interface.h" KisRecalculateTransformMaskJob::KisRecalculateTransformMaskJob(KisTransformMaskSP mask) : m_mask(mask) { setExclusive(true); } bool KisRecalculateTransformMaskJob::overrides(const KisSpontaneousJob *_otherJob) { const KisRecalculateTransformMaskJob *otherJob = dynamic_cast(_otherJob); return otherJob && otherJob->m_mask == m_mask; } void KisRecalculateTransformMaskJob::run() { /** * The mask might have been deleted from the layers stack. In * such a case, don't try do update it. */ if (!m_mask->parent()) return; m_mask->recaclulateStaticImage(); KisLayerSP layer = qobject_cast(m_mask->parent().data()); if (!layer) { warnKrita << "WARNING: KisRecalculateTransformMaskJob::run() Mask has no parent layer! Skipping projection update!"; return; } KisImageSP image = layer->image(); Q_ASSERT(image); /** * Depending on whether the mask is hidden we should either * update it entirely via the setDirty() call, or we can use a * lightweight approach by directly regenerating the * precalculated static image using * KisRecalculateTransformMaskJob. */ if (m_mask->transformParams()->isHidden()) { QRect updateRect = m_mask->extent(); if (layer->original()) { updateRect |= layer->original()->defaultBounds()->bounds(); } m_mask->setDirty(updateRect); } else { /** * When we call requestProjectionUpdateNoFilthy() on a layer, * its masks' change rect is not counted, because it is considered * to be N_ABOVE_FILTHY. Therefore, we should expand the dirty * rect manually to get the correct update */ QRect updateRect = layer->projectionPlane()->changeRect(layer->extent(), KisLayer::N_FILTHY); image->requestProjectionUpdateNoFilthy(layer, updateRect, image->bounds()); } } int KisRecalculateTransformMaskJob::levelOfDetail() const { return 0; } + +QString KisRecalculateTransformMaskJob::debugName() const +{ + QString result; + QDebug dbg(&result); + dbg << "KisRecalculateTransformMaskJob" << m_mask; + return result; +} diff --git a/libs/image/kis_recalculate_transform_mask_job.h b/libs/image/kis_recalculate_transform_mask_job.h index f770d5079f..90e4beebea 100644 --- a/libs/image/kis_recalculate_transform_mask_job.h +++ b/libs/image/kis_recalculate_transform_mask_job.h @@ -1,39 +1,41 @@ /* * Copyright (c) 2014 Dmitry Kazakov * * 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. */ #ifndef __KIS_RECALCULATE_TRANSFORM_MASK_JOB_H #define __KIS_RECALCULATE_TRANSFORM_MASK_JOB_H #include "kis_types.h" #include "kis_spontaneous_job.h" class KRITAIMAGE_EXPORT KisRecalculateTransformMaskJob : public KisSpontaneousJob { public: KisRecalculateTransformMaskJob(KisTransformMaskSP mask); bool overrides(const KisSpontaneousJob *otherJob) override; void run() override; int levelOfDetail() const override; + QString debugName() const override; + private: KisTransformMaskSP m_mask; }; #endif /* __KIS_RECALCULATE_TRANSFORM_MASK_JOB_H */ diff --git a/libs/image/kis_regenerate_frame_stroke_strategy.cpp b/libs/image/kis_regenerate_frame_stroke_strategy.cpp index efd43a4471..f8ac05c824 100644 --- a/libs/image/kis_regenerate_frame_stroke_strategy.cpp +++ b/libs/image/kis_regenerate_frame_stroke_strategy.cpp @@ -1,251 +1,251 @@ /* * Copyright (c) 2015 Dmitry Kazakov * * 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_regenerate_frame_stroke_strategy.h" #include #include "kis_image_interfaces.h" #include "kis_image_animation_interface.h" #include "kis_node.h" #include "kis_image.h" #include "krita_utils.h" #include "kis_full_refresh_walker.h" #include "kis_async_merger.h" #include "kis_projection_updates_filter.h" struct KisRegenerateFrameStrokeStrategy::Private { Type type; int frameId; int previousFrameId; QRegion dirtyRegion; KisImageAnimationInterface *interface; KisProjectionUpdatesFilterSP prevUpdatesFilter; class Data : public KisStrokeJobData { public: Data(KisNodeSP _root, const QRect &_rect, const QRect &_cropRect) : KisStrokeJobData(CONCURRENT), root(_root), rect(_rect), cropRect(_cropRect) {} KisStrokeJobData* createLodClone(int levelOfDetail) override { Q_UNUSED(levelOfDetail); return new KisStrokeJobData(CONCURRENT); } KisNodeSP root; QRect rect; QRect cropRect; }; void saveAndResetUpdatesFilter() { KisImageSP image = interface->image().toStrongRef(); if (!image) { return; } prevUpdatesFilter = image->projectionUpdatesFilter(); image->setProjectionUpdatesFilter(KisProjectionUpdatesFilterSP()); } void restoreUpdatesFilter() { KisImageSP image = interface->image().toStrongRef(); if (!image) { return; } image->setProjectionUpdatesFilter(prevUpdatesFilter); prevUpdatesFilter.clear(); } }; KisRegenerateFrameStrokeStrategy::KisRegenerateFrameStrokeStrategy(int frameId, const QRegion &dirtyRegion, KisImageAnimationInterface *interface) - : KisSimpleStrokeStrategy("regenerate_external_frame_stroke"), + : KisSimpleStrokeStrategy(QLatin1String("regenerate_external_frame_stroke")), m_d(new Private) { m_d->type = EXTERNAL_FRAME; m_d->frameId = frameId; m_d->dirtyRegion = dirtyRegion; m_d->interface = interface; enableJob(JOB_INIT); enableJob(JOB_FINISH, true, KisStrokeJobData::BARRIER); enableJob(JOB_CANCEL, true, KisStrokeJobData::BARRIER); enableJob(JOB_DOSTROKE); enableJob(JOB_SUSPEND); enableJob(JOB_RESUME); setRequestsOtherStrokesToEnd(false); setClearsRedoOnStart(false); setCanForgetAboutMe(true); } KisRegenerateFrameStrokeStrategy::KisRegenerateFrameStrokeStrategy(KisImageAnimationInterface *interface) - : KisSimpleStrokeStrategy("regenerate_current_frame_stroke", kundo2_i18n("Render Animation")), + : KisSimpleStrokeStrategy(QLatin1String("regenerate_current_frame_stroke"), kundo2_i18n("Render Animation")), m_d(new Private) { m_d->type = CURRENT_FRAME; m_d->frameId = 0; m_d->dirtyRegion = QRegion(); m_d->interface = interface; enableJob(JOB_INIT); enableJob(JOB_FINISH, true, KisStrokeJobData::BARRIER); enableJob(JOB_CANCEL, true, KisStrokeJobData::BARRIER); enableJob(JOB_SUSPEND); enableJob(JOB_RESUME); // switching frames is a distinct user action, so it should // cancel the playback or any action easily setRequestsOtherStrokesToEnd(true); setClearsRedoOnStart(false); } KisRegenerateFrameStrokeStrategy::~KisRegenerateFrameStrokeStrategy() { } void KisRegenerateFrameStrokeStrategy::initStrokeCallback() { KisImageSP image = m_d->interface->image().toStrongRef(); if (!image) { return; } if (m_d->type == EXTERNAL_FRAME) { m_d->saveAndResetUpdatesFilter(); image->disableUIUpdates(); m_d->interface->saveAndResetCurrentTime(m_d->frameId, &m_d->previousFrameId); } else if (m_d->type == CURRENT_FRAME) { m_d->interface->blockFrameInvalidation(true); m_d->interface->updatesFacade()->refreshGraphAsync(KisNodeSP()); } } void KisRegenerateFrameStrokeStrategy::doStrokeCallback(KisStrokeJobData *data) { Private::Data *d = dynamic_cast(data); KIS_ASSERT(d); KIS_ASSERT(!m_d->dirtyRegion.isEmpty()); KIS_ASSERT(m_d->type == EXTERNAL_FRAME); KisBaseRectsWalkerSP walker = new KisFullRefreshWalker(d->cropRect); walker->collectRects(d->root, d->rect); KisAsyncMerger merger; merger.startMerge(*walker); } void KisRegenerateFrameStrokeStrategy::finishStrokeCallback() { KisImageSP image = m_d->interface->image().toStrongRef(); if (!image) { return; } if (m_d->type == EXTERNAL_FRAME) { m_d->interface->notifyFrameReady(); m_d->interface->restoreCurrentTime(&m_d->previousFrameId); image->enableUIUpdates(); m_d->restoreUpdatesFilter(); } else if (m_d->type == CURRENT_FRAME) { m_d->interface->blockFrameInvalidation(false); } } void KisRegenerateFrameStrokeStrategy::cancelStrokeCallback() { KisImageSP image = m_d->interface->image().toStrongRef(); if (!image) { return; } if (m_d->type == EXTERNAL_FRAME) { m_d->interface->notifyFrameCancelled(); m_d->interface->restoreCurrentTime(&m_d->previousFrameId); image->enableUIUpdates(); m_d->restoreUpdatesFilter(); } else if (m_d->type == CURRENT_FRAME) { m_d->interface->blockFrameInvalidation(false); } } KisStrokeStrategy* KisRegenerateFrameStrokeStrategy::createLodClone(int levelOfDetail) { Q_UNUSED(levelOfDetail); /** * We need to regenerate animation frames on LodN level only if * we are processing current frame. Return dummy stroke otherwise */ return m_d->type == CURRENT_FRAME ? new KisRegenerateFrameStrokeStrategy(m_d->interface) : - new KisSimpleStrokeStrategy("dumb-lodn-KisRegenerateFrameStrokeStrategy"); + new KisSimpleStrokeStrategy(QLatin1String("dumb-lodn-KisRegenerateFrameStrokeStrategy")); } void KisRegenerateFrameStrokeStrategy::suspendStrokeCallback() { KisImageSP image = m_d->interface->image().toStrongRef(); if (!image) { return; } if (m_d->type == EXTERNAL_FRAME) { m_d->interface->restoreCurrentTime(&m_d->previousFrameId); image->enableUIUpdates(); m_d->restoreUpdatesFilter(); } else if (m_d->type == CURRENT_FRAME) { m_d->interface->blockFrameInvalidation(false); } } void KisRegenerateFrameStrokeStrategy::resumeStrokeCallback() { KisImageSP image = m_d->interface->image().toStrongRef(); if (!image) { return; } if (m_d->type == EXTERNAL_FRAME) { m_d->saveAndResetUpdatesFilter(); image->disableUIUpdates(); m_d->interface->saveAndResetCurrentTime(m_d->frameId, &m_d->previousFrameId); } else if (m_d->type == CURRENT_FRAME) { m_d->interface->blockFrameInvalidation(true); } } QList KisRegenerateFrameStrokeStrategy::createJobsData(KisImageWSP _image) { using KritaUtils::splitRectIntoPatches; using KritaUtils::optimalPatchSize; KisImageSP image = _image; const QRect cropRect = image->bounds(); QVector rects = splitRectIntoPatches(image->bounds(), optimalPatchSize()); QList jobsData; Q_FOREACH (const QRect &rc, rects) { jobsData << new Private::Data(image->root(), rc, cropRect); } return jobsData; } diff --git a/libs/image/kis_spontaneous_job.cpp b/libs/image/kis_runnable_with_debug_name.h similarity index 68% rename from libs/image/kis_spontaneous_job.cpp rename to libs/image/kis_runnable_with_debug_name.h index cb612d41a9..f4ecee9263 100644 --- a/libs/image/kis_spontaneous_job.cpp +++ b/libs/image/kis_runnable_with_debug_name.h @@ -1,30 +1,31 @@ /* - * Copyright (c) 2018 Dmitry Kazakov + * Copyright (c) 2019 Dmitry Kazakov * * 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_spontaneous_job.h" +#ifndef KIS_RUNNABLE_WITH_DEBUG_NAME_H +#define KIS_RUNNABLE_WITH_DEBUG_NAME_H +#include "kis_runnable.h" +#include -bool KisSpontaneousJob::isExclusive() const +class KRITAIMAGE_EXPORT KisRunnableWithDebugName : public KisRunnable { - return m_isExclusive; -} +public: + virtual QString debugName() const = 0; +}; -void KisSpontaneousJob::setExclusive(bool value) -{ - -} +#endif // KIS_RUNNABLE_WITH_DEBUG_NAME_H diff --git a/libs/image/kis_selection_mask.cpp b/libs/image/kis_selection_mask.cpp index 6a3474dd26..a735460946 100644 --- a/libs/image/kis_selection_mask.cpp +++ b/libs/image/kis_selection_mask.cpp @@ -1,321 +1,327 @@ /* * Copyright (c) 2006 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_selection_mask.h" #include "kis_image.h" #include "kis_layer.h" #include "kis_selection.h" #include #include #include #include "kis_fill_painter.h" #include #include "kis_node_visitor.h" #include "kis_processing_visitor.h" #include "kis_pixel_selection.h" #include "kis_undo_adapter.h" #include #include #include "kis_thread_safe_signal_compressor.h" #include "kis_layer_properties_icons.h" #include "kis_cached_paint_device.h" #include "kis_image_config.h" #include "KisImageConfigNotifier.h" struct Q_DECL_HIDDEN KisSelectionMask::Private { public: Private(KisSelectionMask *_q) : q(_q) , updatesCompressor(0) , maskColor(Qt::green, KoColorSpaceRegistry::instance()->rgb8()) {} KisSelectionMask *q; KisImageWSP image; KisCachedPaintDevice paintDeviceCache; KisCachedSelection cachedSelection; KisThreadSafeSignalCompressor *updatesCompressor; KoColor maskColor; void slotSelectionChangedCompressed(); + void slotConfigChangedImpl(bool blockUpdates); void slotConfigChanged(); }; KisSelectionMask::KisSelectionMask(KisImageWSP image) : KisEffectMask() , m_d(new Private(this)) { setName("selection"); setActive(false); setSupportsLodMoves(false); m_d->image = image; m_d->updatesCompressor = new KisThreadSafeSignalCompressor(50, KisSignalCompressor::FIRST_ACTIVE); connect(m_d->updatesCompressor, SIGNAL(timeout()), SLOT(slotSelectionChangedCompressed())); this->moveToThread(image->thread()); connect(KisImageConfigNotifier::instance(), SIGNAL(configChanged()), SLOT(slotConfigChanged())); - m_d->slotConfigChanged(); + m_d->slotConfigChangedImpl(false); } KisSelectionMask::KisSelectionMask(const KisSelectionMask& rhs) : KisEffectMask(rhs) , m_d(new Private(this)) { m_d->image = rhs.image(); m_d->updatesCompressor = new KisThreadSafeSignalCompressor(300, KisSignalCompressor::POSTPONE); connect(m_d->updatesCompressor, SIGNAL(timeout()), SLOT(slotSelectionChangedCompressed())); this->moveToThread(m_d->image->thread()); connect(KisImageConfigNotifier::instance(), SIGNAL(configChanged()), SLOT(slotConfigChanged())); - m_d->slotConfigChanged(); + m_d->slotConfigChangedImpl(false); } KisSelectionMask::~KisSelectionMask() { m_d->updatesCompressor->deleteLater(); delete m_d; } QIcon KisSelectionMask::icon() const { return KisIconUtils::loadIcon("selectionMask"); } void KisSelectionMask::mergeInMaskInternal(KisPaintDeviceSP projection, KisSelectionSP effectiveSelection, const QRect &applyRect, const QRect &preparedNeedRect, KisNode::PositionToFilthy maskPos) const { Q_UNUSED(maskPos); Q_UNUSED(preparedNeedRect); if (!effectiveSelection) return; { KisSelectionSP mainMaskSelection = this->selection(); if (mainMaskSelection && (!mainMaskSelection->isVisible() || mainMaskSelection->pixelSelection()->defaultBounds()->externalFrameActive())) { return; } } KisCachedPaintDevice::Guard d1(projection, m_d->paintDeviceCache); KisPaintDeviceSP fillDevice = d1.device(); fillDevice->setDefaultPixel(m_d->maskColor); const QRect selectionExtent = effectiveSelection->selectedRect(); if (selectionExtent.contains(applyRect) || selectionExtent.intersects(applyRect)) { KisCachedSelection::Guard s1(m_d->cachedSelection); KisSelectionSP invertedSelection = s1.selection(); invertedSelection->pixelSelection()->makeCloneFromRough(effectiveSelection->pixelSelection(), applyRect); invertedSelection->pixelSelection()->invert(); KisPainter gc(projection); gc.setSelection(invertedSelection); gc.bitBlt(applyRect.topLeft(), fillDevice, applyRect); } else { KisPainter gc(projection); gc.bitBlt(applyRect.topLeft(), fillDevice, applyRect); } } bool KisSelectionMask::paintsOutsideSelection() const { return true; } void KisSelectionMask::setSelection(KisSelectionSP selection) { if (selection) { KisEffectMask::setSelection(selection); } else { KisEffectMask::setSelection(new KisSelection()); const KoColorSpace * cs = KoColorSpaceRegistry::instance()->alpha8(); KisFillPainter gc(KisPaintDeviceSP(this->selection()->pixelSelection().data())); gc.fillRect(image()->bounds(), KoColor(Qt::white, cs), MAX_SELECTED); gc.end(); } setDirty(); } KisImageWSP KisSelectionMask::image() const { return m_d->image; } bool KisSelectionMask::accept(KisNodeVisitor &v) { return v.visit(this); } void KisSelectionMask::accept(KisProcessingVisitor &visitor, KisUndoAdapter *undoAdapter) { return visitor.visit(this, undoAdapter); } KisBaseNode::PropertyList KisSelectionMask::sectionModelProperties() const { KisBaseNode::PropertyList l = KisBaseNode::sectionModelProperties(); l << KisLayerPropertiesIcons::getProperty(KisLayerPropertiesIcons::selectionActive, active()); return l; } void KisSelectionMask::setSectionModelProperties(const KisBaseNode::PropertyList &properties) { KisEffectMask::setSectionModelProperties(properties); setActive(properties.at(2).state.toBool()); } void KisSelectionMask::setVisible(bool visible, bool isLoading) { const bool oldVisible = this->visible(false); setNodeProperty("visible", visible); if (!isLoading && visible != oldVisible) { if (selection()) selection()->setVisible(visible); } } bool KisSelectionMask::active() const { return nodeProperties().boolProperty("active", true); } void KisSelectionMask::setActive(bool active) { KisImageWSP image = this->image(); KisLayerSP parentLayer = qobject_cast(parent().data()); if (active && parentLayer) { KisSelectionMaskSP activeMask = parentLayer->selectionMask(); if (activeMask && activeMask != this) { activeMask->setActive(false); } } const bool oldActive = this->active(); setNodeProperty("active", active); /** * WARNING: we have a direct link to the image here, but we * must not use it for notification until we are a part of * the nore graph! Notifications should be emitted iff we * have graph listener link set up. */ if (graphListener() && image && oldActive != active) { baseNodeChangedCallback(); image->undoAdapter()->emitSelectionChanged(); } } QRect KisSelectionMask::needRect(const QRect &rect, KisNode::PositionToFilthy pos) const { Q_UNUSED(pos); // selection masks just add an overlay, so the needed rect is simply passed through return rect; } QRect KisSelectionMask::changeRect(const QRect &rect, KisNode::PositionToFilthy pos) const { Q_UNUSED(pos); // selection masks just add an overlay, so the changed rect is simply passed through return rect; } QRect KisSelectionMask::extent() const { // since mask overlay is inverted, the mask paints over // the entire image bounds QRect resultRect; KisSelectionSP selection = this->selection(); if (selection) { resultRect = selection->pixelSelection()->defaultBounds()->bounds(); } else if (KisNodeSP parent = this->parent()) { KisPaintDeviceSP dev = parent->projection(); if (dev) { resultRect = dev->defaultBounds()->bounds(); } } return resultRect; } QRect KisSelectionMask::exactBounds() const { return extent(); } void KisSelectionMask::notifySelectionChangedCompressed() { m_d->updatesCompressor->start(); } void KisSelectionMask::flattenSelectionProjection(KisSelectionSP selection, const QRect &dirtyRect) const { Q_UNUSED(selection); Q_UNUSED(dirtyRect); } void KisSelectionMask::Private::slotSelectionChangedCompressed() { KisSelectionSP currentSelection = q->selection(); if (!currentSelection) return; currentSelection->notifySelectionChanged(); } -void KisSelectionMask::Private::slotConfigChanged() +void KisSelectionMask::Private::slotConfigChangedImpl(bool doUpdates) { const KoColorSpace *cs = image ? image->colorSpace() : KoColorSpaceRegistry::instance()->rgb8(); KisImageConfig cfg(true); maskColor = KoColor(cfg.selectionOverlayMaskColor(), cs); - if (image && image->overlaySelectionMask() == q) { + if (doUpdates && image && image->overlaySelectionMask() == q) { q->setDirty(); } } +void KisSelectionMask::Private::slotConfigChanged() +{ + slotConfigChangedImpl(true); +} + #include "moc_kis_selection_mask.cpp" diff --git a/libs/image/kis_simple_stroke_strategy.cpp b/libs/image/kis_simple_stroke_strategy.cpp index 253462a07b..5fc12ede4c 100644 --- a/libs/image/kis_simple_stroke_strategy.cpp +++ b/libs/image/kis_simple_stroke_strategy.cpp @@ -1,197 +1,233 @@ /* * Copyright (c) 2011 Dmitry Kazakov * * 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_simple_stroke_strategy.h" /***************************************************************/ /* private class: SimpleStrokeJobStrategy */ /***************************************************************/ class SimpleStrokeJobStrategy : public KisStrokeJobStrategy { public: SimpleStrokeJobStrategy(KisSimpleStrokeStrategy::JobType type, KisSimpleStrokeStrategy *parentStroke) : m_type(type), m_parentStroke(parentStroke) { } void run(KisStrokeJobData *data) override { switch(m_type) { case KisSimpleStrokeStrategy::JOB_INIT: Q_UNUSED(data); m_parentStroke->initStrokeCallback(); break; case KisSimpleStrokeStrategy::JOB_FINISH: Q_UNUSED(data); m_parentStroke->finishStrokeCallback(); break; case KisSimpleStrokeStrategy::JOB_CANCEL: Q_UNUSED(data); m_parentStroke->cancelStrokeCallback(); break; case KisSimpleStrokeStrategy::JOB_DOSTROKE: m_parentStroke->doStrokeCallback(data); break; case KisSimpleStrokeStrategy::JOB_SUSPEND: m_parentStroke->suspendStrokeCallback(); break; case KisSimpleStrokeStrategy::JOB_RESUME: m_parentStroke->resumeStrokeCallback(); break; default: break; } } + QString debugId() const override { + return QString("%1/%2") + .arg(m_parentStroke->id()) + .arg(KisSimpleStrokeStrategy::jobTypeToString(m_type)); + } + private: KisSimpleStrokeStrategy::JobType m_type; KisSimpleStrokeStrategy *m_parentStroke; }; /***************************************************************/ /* KisSimpleStrokeStrategy */ /***************************************************************/ -KisSimpleStrokeStrategy::KisSimpleStrokeStrategy(QString id, const KUndo2MagicString &name) +KisSimpleStrokeStrategy::KisSimpleStrokeStrategy(const QLatin1String &id, const KUndo2MagicString &name) : KisStrokeStrategy(id, name), m_jobEnabled(NJOBS, false), m_jobSequentiality(NJOBS, KisStrokeJobData::SEQUENTIAL), m_jobExclusivity(NJOBS, KisStrokeJobData::NORMAL) { } KisSimpleStrokeStrategy::KisSimpleStrokeStrategy(const KisSimpleStrokeStrategy &rhs) : KisStrokeStrategy(rhs), m_jobEnabled(rhs.m_jobEnabled), m_jobSequentiality(rhs.m_jobSequentiality), m_jobExclusivity(rhs.m_jobExclusivity) { } void KisSimpleStrokeStrategy::enableJob(JobType type, bool enable, KisStrokeJobData::Sequentiality sequentiality, KisStrokeJobData::Exclusivity exclusivity) { m_jobEnabled[(int)type] = enable; m_jobSequentiality[(int)type] = sequentiality; m_jobExclusivity[(int)type] = exclusivity; } KisStrokeJobStrategy* KisSimpleStrokeStrategy::createStrategy(JobType type) { KisStrokeJobStrategy *strategy = 0; if(m_jobEnabled[(int)type]) { strategy = new SimpleStrokeJobStrategy(type, this); } return strategy; } KisStrokeJobStrategy* KisSimpleStrokeStrategy::createInitStrategy() { return createStrategy(JOB_INIT); } KisStrokeJobStrategy* KisSimpleStrokeStrategy::createFinishStrategy() { return createStrategy(JOB_FINISH); } KisStrokeJobStrategy* KisSimpleStrokeStrategy::createCancelStrategy() { return createStrategy(JOB_CANCEL); } KisStrokeJobStrategy* KisSimpleStrokeStrategy::createDabStrategy() { return createStrategy(JOB_DOSTROKE); } KisStrokeJobStrategy* KisSimpleStrokeStrategy::createSuspendStrategy() { return createStrategy(JOB_SUSPEND); } KisStrokeJobStrategy* KisSimpleStrokeStrategy::createResumeStrategy() { return createStrategy(JOB_RESUME); } KisStrokeJobData* KisSimpleStrokeStrategy::createData(JobType type) { KisStrokeJobData::Sequentiality sequentiality = m_jobSequentiality[(int)type]; KisStrokeJobData::Exclusivity exclusivity = m_jobExclusivity[(int)type]; return new KisStrokeJobData(sequentiality, exclusivity); } KisStrokeJobData* KisSimpleStrokeStrategy::createInitData() { return createData(JOB_INIT); } KisStrokeJobData* KisSimpleStrokeStrategy::createFinishData() { return createData(JOB_FINISH); } KisStrokeJobData* KisSimpleStrokeStrategy::createCancelData() { return createData(JOB_CANCEL); } KisStrokeJobData* KisSimpleStrokeStrategy::createSuspendData() { return createData(JOB_SUSPEND); } KisStrokeJobData* KisSimpleStrokeStrategy::createResumeData() { return createData(JOB_RESUME); } void KisSimpleStrokeStrategy::initStrokeCallback() { } void KisSimpleStrokeStrategy::finishStrokeCallback() { } void KisSimpleStrokeStrategy::cancelStrokeCallback() { } void KisSimpleStrokeStrategy::doStrokeCallback(KisStrokeJobData *data) { Q_UNUSED(data); } void KisSimpleStrokeStrategy::suspendStrokeCallback() { } void KisSimpleStrokeStrategy::resumeStrokeCallback() { } + +QLatin1String KisSimpleStrokeStrategy::jobTypeToString(KisSimpleStrokeStrategy::JobType type) +{ + QLatin1String result; + + switch (type) { + case JOB_INIT: + result = QLatin1String("init"); + break; + case JOB_DOSTROKE: + result = QLatin1String("dostroke"); + break; + case JOB_FINISH: + result = QLatin1String("finish"); + break; + case JOB_CANCEL: + result = QLatin1String("cancel"); + break; + case JOB_SUSPEND: + result = QLatin1String("suspend"); + break; + case JOB_RESUME: + result = QLatin1String("resume"); + break; + case NJOBS: + qFatal("Undefined stroke job type: %d", type); + } + + return result; +} diff --git a/libs/image/kis_simple_stroke_strategy.h b/libs/image/kis_simple_stroke_strategy.h index de599f6a6a..f97cef60bb 100644 --- a/libs/image/kis_simple_stroke_strategy.h +++ b/libs/image/kis_simple_stroke_strategy.h @@ -1,82 +1,84 @@ /* * Copyright (c) 2011 Dmitry Kazakov * * 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. */ #ifndef __KIS_SIMPLE_STROKE_STRATEGY_H #define __KIS_SIMPLE_STROKE_STRATEGY_H #include #include "kis_stroke_strategy.h" #include "kis_stroke_job_strategy.h" class KRITAIMAGE_EXPORT KisSimpleStrokeStrategy : public KisStrokeStrategy { public: enum JobType { JOB_INIT = 0, JOB_CANCEL, JOB_FINISH, JOB_DOSTROKE, JOB_SUSPEND, JOB_RESUME, NJOBS }; public: - KisSimpleStrokeStrategy(QString id = QString(), const KUndo2MagicString &name = KUndo2MagicString()); + KisSimpleStrokeStrategy(const QLatin1String &id, const KUndo2MagicString &name = KUndo2MagicString()); KisStrokeJobStrategy* createInitStrategy() override; KisStrokeJobStrategy* createFinishStrategy() override; KisStrokeJobStrategy* createCancelStrategy() override; KisStrokeJobStrategy* createDabStrategy() override; KisStrokeJobStrategy* createSuspendStrategy() override; KisStrokeJobStrategy* createResumeStrategy() override; KisStrokeJobData* createInitData() override; KisStrokeJobData* createFinishData() override; KisStrokeJobData* createCancelData() override; KisStrokeJobData* createSuspendData() override; KisStrokeJobData* createResumeData() override; virtual void initStrokeCallback(); virtual void finishStrokeCallback(); virtual void cancelStrokeCallback(); virtual void doStrokeCallback(KisStrokeJobData *data); virtual void suspendStrokeCallback(); virtual void resumeStrokeCallback(); + static QLatin1String jobTypeToString(JobType type); + protected: void enableJob(JobType type, bool enable = true, KisStrokeJobData::Sequentiality sequentiality = KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::Exclusivity exclusivity = KisStrokeJobData::NORMAL); protected: KisSimpleStrokeStrategy(const KisSimpleStrokeStrategy &rhs); private: KisStrokeJobStrategy* createStrategy(JobType type); KisStrokeJobData* createData(JobType type); private: QVector m_jobEnabled; QVector m_jobSequentiality; QVector m_jobExclusivity; }; #endif /* __KIS_SIMPLE_STROKE_STRATEGY_H */ diff --git a/libs/image/kis_spontaneous_job.h b/libs/image/kis_spontaneous_job.h index 033eac24e6..594ac73485 100644 --- a/libs/image/kis_spontaneous_job.h +++ b/libs/image/kis_spontaneous_job.h @@ -1,48 +1,48 @@ /* * Copyright (c) 2013 Dmitry Kazakov * * 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. */ #ifndef __KIS_SPONTANEOUS_JOB_H #define __KIS_SPONTANEOUS_JOB_H -#include "kis_runnable.h" +#include "kis_runnable_with_debug_name.h" /** * This class represents a simple update just that should be * executed by the updates system from time to time, without * any undo support. Just some useful update that * can be run concurrently with other types updates. */ -class KRITAIMAGE_EXPORT KisSpontaneousJob : public KisRunnable +class KRITAIMAGE_EXPORT KisSpontaneousJob : public KisRunnableWithDebugName { public: virtual bool overrides(const KisSpontaneousJob *otherJob) = 0; virtual int levelOfDetail() const = 0; bool isExclusive() const { return m_isExclusive; } protected: void setExclusive(bool value) { m_isExclusive = value; } private: bool m_isExclusive = false; }; #endif /* __KIS_SPONTANEOUS_JOB_H */ diff --git a/libs/image/kis_stroke_job.h b/libs/image/kis_stroke_job.h index 13cb7f7a2b..55a0c9e5df 100644 --- a/libs/image/kis_stroke_job.h +++ b/libs/image/kis_stroke_job.h @@ -1,104 +1,108 @@ /* * Copyright (c) 2011 Dmitry Kazakov * * 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. */ #ifndef __KIS_STROKE_JOB_H #define __KIS_STROKE_JOB_H -#include "kis_runnable.h" +#include "kis_runnable_with_debug_name.h" #include "kis_stroke_job_strategy.h" -class KisStrokeJob : public KisRunnable +class KisStrokeJob : public KisRunnableWithDebugName { public: KisStrokeJob(KisStrokeJobStrategy *strategy, KisStrokeJobData *data, int levelOfDetail, bool isOwnJob) : m_dabStrategy(strategy), m_dabData(data), m_levelOfDetail(levelOfDetail), m_isOwnJob(isOwnJob) { } ~KisStrokeJob() override { delete m_dabData; } void run() override { m_dabStrategy->run(m_dabData); } KisStrokeJobData::Sequentiality sequentiality() const { return m_dabData ? m_dabData->sequentiality() : KisStrokeJobData::SEQUENTIAL; } bool isSequential() const { // Default value is 'SEQUENTIAL' return m_dabData ? m_dabData->isSequential() : true; } bool isBarrier() const { // Default value is simply 'SEQUENTIAL', *not* 'BARRIER' return m_dabData ? m_dabData->isBarrier() : false; } bool isExclusive() const { // Default value is 'NORMAL' return m_dabData ? m_dabData->isExclusive() : false; } int levelOfDetail() const { return m_levelOfDetail; } bool isCancellable() const { return m_isOwnJob && (!m_dabData || m_dabData->isCancellable()); } bool isOwnJob() const { return m_isOwnJob; } + QString debugName() const override { + return m_dabStrategy->debugId(); + } + private: // for testing use only, do not use in real code friend QString getJobName(KisStrokeJob *job); friend QString getCommandName(KisStrokeJob *job); friend int cancelSeqNo(KisStrokeJob *job); KisStrokeJobStrategy* testingGetDabStrategy() { return m_dabStrategy; } KisStrokeJobData* testingGetDabData() { return m_dabData; } private: // Shared between different jobs KisStrokeJobStrategy *m_dabStrategy; // Owned by the job KisStrokeJobData *m_dabData; int m_levelOfDetail; bool m_isOwnJob; }; #endif /* __KIS_STROKE_JOB_H */ diff --git a/libs/image/kis_stroke_job_strategy.h b/libs/image/kis_stroke_job_strategy.h index 9f344b0ff5..28031deb84 100644 --- a/libs/image/kis_stroke_job_strategy.h +++ b/libs/image/kis_stroke_job_strategy.h @@ -1,79 +1,81 @@ /* * Copyright (c) 2011 Dmitry Kazakov * * 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. */ #ifndef __KIS_STROKE_JOB_STRATEGY_H #define __KIS_STROKE_JOB_STRATEGY_H #include "kritaimage_export.h" +#include class KRITAIMAGE_EXPORT KisStrokeJobData { public: enum Sequentiality { CONCURRENT, SEQUENTIAL, BARRIER, UNIQUELY_CONCURRENT }; enum Exclusivity { NORMAL, EXCLUSIVE }; public: KisStrokeJobData(Sequentiality sequentiality = SEQUENTIAL, Exclusivity exclusivity = NORMAL); virtual ~KisStrokeJobData(); bool isBarrier() const; bool isSequential() const; bool isExclusive() const; Sequentiality sequentiality() { return m_sequentiality; } Exclusivity exclusivity() { return m_exclusivity; } virtual KisStrokeJobData* createLodClone(int levelOfDetail); bool isCancellable() const; void setCancellable(bool value); protected: KisStrokeJobData(const KisStrokeJobData &rhs); private: Sequentiality m_sequentiality; Exclusivity m_exclusivity; bool m_isCancellable; }; class KRITAIMAGE_EXPORT KisStrokeJobStrategy { public: KisStrokeJobStrategy(); virtual ~KisStrokeJobStrategy(); virtual void run(KisStrokeJobData *data) = 0; + virtual QString debugId() const = 0; private: }; #endif /* __KIS_STROKE_JOB_STRATEGY_H */ diff --git a/libs/image/kis_stroke_strategy.cpp b/libs/image/kis_stroke_strategy.cpp index 51fc84ca3c..cec29bd135 100644 --- a/libs/image/kis_stroke_strategy.cpp +++ b/libs/image/kis_stroke_strategy.cpp @@ -1,226 +1,226 @@ /* * Copyright (c) 2011 Dmitry Kazakov * * 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_stroke_strategy.h" #include #include "kis_stroke_job_strategy.h" #include "KisStrokesQueueMutatedJobInterface.h" -KisStrokeStrategy::KisStrokeStrategy(QString id, const KUndo2MagicString &name) +KisStrokeStrategy::KisStrokeStrategy(const QLatin1String &id, const KUndo2MagicString &name) : m_exclusive(false), m_supportsWrapAroundMode(false), m_clearsRedoOnStart(true), m_requestsOtherStrokesToEnd(true), m_canForgetAboutMe(false), m_needsExplicitCancel(false), m_balancingRatioOverride(-1.0), m_id(id), m_name(name), m_mutatedJobsInterface(0) { } KisStrokeStrategy::KisStrokeStrategy(const KisStrokeStrategy &rhs) : m_exclusive(rhs.m_exclusive), m_supportsWrapAroundMode(rhs.m_supportsWrapAroundMode), m_clearsRedoOnStart(rhs.m_clearsRedoOnStart), m_requestsOtherStrokesToEnd(rhs.m_requestsOtherStrokesToEnd), m_canForgetAboutMe(rhs.m_canForgetAboutMe), m_needsExplicitCancel(rhs.m_needsExplicitCancel), m_balancingRatioOverride(rhs.m_balancingRatioOverride), m_id(rhs.m_id), m_name(rhs.m_name), m_mutatedJobsInterface(0) { KIS_ASSERT_RECOVER_NOOP(!rhs.m_cancelStrokeId && !m_mutatedJobsInterface && "After the stroke has been started, no copying must happen"); } KisStrokeStrategy::~KisStrokeStrategy() { } void KisStrokeStrategy::notifyUserStartedStroke() { } void KisStrokeStrategy::notifyUserEndedStroke() { } KisStrokeJobStrategy* KisStrokeStrategy::createInitStrategy() { return 0; } KisStrokeJobStrategy* KisStrokeStrategy::createFinishStrategy() { return 0; } KisStrokeJobStrategy* KisStrokeStrategy::createCancelStrategy() { return 0; } KisStrokeJobStrategy* KisStrokeStrategy::createDabStrategy() { return 0; } KisStrokeJobStrategy* KisStrokeStrategy::createSuspendStrategy() { return 0; } KisStrokeJobStrategy* KisStrokeStrategy::createResumeStrategy() { return 0; } KisStrokeJobData* KisStrokeStrategy::createInitData() { return 0; } KisStrokeJobData* KisStrokeStrategy::createFinishData() { return 0; } KisStrokeJobData* KisStrokeStrategy::createCancelData() { return 0; } KisStrokeJobData* KisStrokeStrategy::createSuspendData() { return 0; } KisStrokeJobData* KisStrokeStrategy::createResumeData() { return 0; } KisStrokeStrategy* KisStrokeStrategy::createLodClone(int levelOfDetail) { Q_UNUSED(levelOfDetail); return 0; } bool KisStrokeStrategy::isExclusive() const { return m_exclusive; } bool KisStrokeStrategy::supportsWrapAroundMode() const { return m_supportsWrapAroundMode; } QString KisStrokeStrategy::id() const { return m_id; } KUndo2MagicString KisStrokeStrategy::name() const { return m_name; } void KisStrokeStrategy::setMutatedJobsInterface(KisStrokesQueueMutatedJobInterface *mutatedJobsInterface) { m_mutatedJobsInterface = mutatedJobsInterface; } void KisStrokeStrategy::addMutatedJobs(const QVector list) { KIS_SAFE_ASSERT_RECOVER(m_mutatedJobsInterface && m_cancelStrokeId) { qDeleteAll(list); return; } m_mutatedJobsInterface->addMutatedJobs(m_cancelStrokeId, list); } void KisStrokeStrategy::addMutatedJob(KisStrokeJobData *data) { addMutatedJobs({data}); } void KisStrokeStrategy::setExclusive(bool value) { m_exclusive = value; } void KisStrokeStrategy::setSupportsWrapAroundMode(bool value) { m_supportsWrapAroundMode = value; } bool KisStrokeStrategy::clearsRedoOnStart() const { return m_clearsRedoOnStart; } void KisStrokeStrategy::setClearsRedoOnStart(bool value) { m_clearsRedoOnStart = value; } bool KisStrokeStrategy::requestsOtherStrokesToEnd() const { return m_requestsOtherStrokesToEnd; } void KisStrokeStrategy::setRequestsOtherStrokesToEnd(bool value) { m_requestsOtherStrokesToEnd = value; } bool KisStrokeStrategy::canForgetAboutMe() const { return m_canForgetAboutMe; } void KisStrokeStrategy::setCanForgetAboutMe(bool value) { m_canForgetAboutMe = value; } bool KisStrokeStrategy::needsExplicitCancel() const { return m_needsExplicitCancel; } void KisStrokeStrategy::setNeedsExplicitCancel(bool value) { m_needsExplicitCancel = value; } qreal KisStrokeStrategy::balancingRatioOverride() const { return m_balancingRatioOverride; } void KisStrokeStrategy::setBalancingRatioOverride(qreal value) { m_balancingRatioOverride = value; } diff --git a/libs/image/kis_stroke_strategy.h b/libs/image/kis_stroke_strategy.h index b3cb98aa7c..b5dce84c68 100644 --- a/libs/image/kis_stroke_strategy.h +++ b/libs/image/kis_stroke_strategy.h @@ -1,209 +1,209 @@ /* * Copyright (c) 2011 Dmitry Kazakov * * 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. */ #ifndef __KIS_STROKE_STRATEGY_H #define __KIS_STROKE_STRATEGY_H #include #include "kis_types.h" #include "kundo2magicstring.h" #include "kritaimage_export.h" class KisStrokeJobStrategy; class KisStrokeJobData; class KisStrokesQueueMutatedJobInterface; class KRITAIMAGE_EXPORT KisStrokeStrategy { public: - KisStrokeStrategy(QString id = QString(), const KUndo2MagicString &name = KUndo2MagicString()); + KisStrokeStrategy(const QLatin1String &id, const KUndo2MagicString &name = KUndo2MagicString()); virtual ~KisStrokeStrategy(); /** * notifyUserStartedStroke() is a callback used by the strokes system to notify * when the user adds the stroke to the strokes queue. That moment corresponds * to the user calling strokesFacade->startStroke(strategy) and might happen much * earlier than the first job being executed. * * NOTE: this method will be executed in the context of the GUI thread! */ virtual void notifyUserStartedStroke(); /** * notifyUserEndedStroke() is a callback used by the strokes system to notify * when the user ends the stroke. That moment corresponds to the user calling * strokesFacade->endStroke(id) and might happen much earlier when the stroke * even started its execution. * * NOTE: this method will be executed in the context of the GUI thread! */ virtual void notifyUserEndedStroke(); virtual KisStrokeJobStrategy* createInitStrategy(); virtual KisStrokeJobStrategy* createFinishStrategy(); virtual KisStrokeJobStrategy* createCancelStrategy(); virtual KisStrokeJobStrategy* createDabStrategy(); virtual KisStrokeJobStrategy* createSuspendStrategy(); virtual KisStrokeJobStrategy* createResumeStrategy(); virtual KisStrokeJobData* createInitData(); virtual KisStrokeJobData* createFinishData(); virtual KisStrokeJobData* createCancelData(); virtual KisStrokeJobData* createSuspendData(); virtual KisStrokeJobData* createResumeData(); virtual KisStrokeStrategy* createLodClone(int levelOfDetail); bool isExclusive() const; bool supportsWrapAroundMode() const; /** * Returns true if mere start of the stroke should cancel all the * pending redo tasks. * * This method should return true in almost all circumstances * except if we are running an undo or redo stroke. */ bool clearsRedoOnStart() const; /** * Returns true if the other currently running strokes should be * politely asked to exit. The default value is 'true'. * * The only known exception right now is * KisRegenerateFrameStrokeStrategy which does not requests ending * of any actions, since it performs purely background action. */ bool requestsOtherStrokesToEnd() const; /** * Returns true if the update scheduler can cancel this stroke * when some other stroke is going to be started. This makes the * "forgettable" stroke very low priority. * * Default is 'false'. */ bool canForgetAboutMe() const; bool needsExplicitCancel() const; /** * \see setBalancingRatioOverride() for details */ qreal balancingRatioOverride() const; QString id() const; KUndo2MagicString name() const; /** * Set up by the strokes queue during the stroke initialization */ void setCancelStrokeId(KisStrokeId id) { m_cancelStrokeId = id; } void setMutatedJobsInterface(KisStrokesQueueMutatedJobInterface *mutatedJobsInterface); protected: // testing surrogate class friend class KisMutatableDabStrategy; /** * The cancel job may populate the stroke with some new jobs * for cancelling. To achieve this it needs the stroke id. * * WARNING: you can't add new jobs in any places other than * cancel job, because the stroke may be ended in any moment * by the user and the sequence of jobs will be broken */ KisStrokeId cancelStrokeId() { return m_cancelStrokeId; } /** * This function is supposed to be called by internal asynchronous * jobs. It allows adding subtasks that may be executed concurrently. * * Requirements: * * must be called *only* from within the context of the strokes * worker thread during execution of one of its jobs * * Guarantees: * * the added job is guaranteed to be executed in some time after * the currently executed job, *before* the next SEQUENTIAL or * BARRIER job * * if the currently executed job is CUNCURRENTthe mutated job *may* * start execution right after adding to the queue without waiting for * its parent to complete. Though this behavior is *not* guaranteed, * because addMutatedJob does not initiate processQueues(), because * it may lead to a deadlock. */ void addMutatedJobs(const QVector list); /** * Convenience override for addMutatedJobs() */ void addMutatedJob(KisStrokeJobData *data); // you are not supposed to change these parameters // after the KisStroke object has been created void setExclusive(bool value); void setSupportsWrapAroundMode(bool value); void setClearsRedoOnStart(bool value); void setRequestsOtherStrokesToEnd(bool value); void setCanForgetAboutMe(bool value); void setNeedsExplicitCancel(bool value); /** * Set override for the desired scheduler balancing ratio: * * ratio = stroke jobs / update jobs * * If \p value < 1.0, then the priority is given to updates, if * the value is higher than 1.0, then the priority is given * to stroke jobs. * * Special value -1.0, suggests the scheduler to use the default value * set by the user's config file (which is 100.0 by default). */ void setBalancingRatioOverride(qreal value); protected: /** * Protected c-tor, used for cloning of hi-level strategies */ KisStrokeStrategy(const KisStrokeStrategy &rhs); private: bool m_exclusive; bool m_supportsWrapAroundMode; bool m_clearsRedoOnStart; bool m_requestsOtherStrokesToEnd; bool m_canForgetAboutMe; bool m_needsExplicitCancel; qreal m_balancingRatioOverride; - QString m_id; + QLatin1String m_id; KUndo2MagicString m_name; KisStrokeId m_cancelStrokeId; KisStrokesQueueMutatedJobInterface *m_mutatedJobsInterface; }; #endif /* __KIS_STROKE_STRATEGY_H */ diff --git a/libs/image/kis_stroke_strategy_undo_command_based.cpp b/libs/image/kis_stroke_strategy_undo_command_based.cpp index d50d79eb5d..32ef12c95c 100644 --- a/libs/image/kis_stroke_strategy_undo_command_based.cpp +++ b/libs/image/kis_stroke_strategy_undo_command_based.cpp @@ -1,189 +1,189 @@ /* * Copyright (c) 2011 Dmitry Kazakov * * 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_stroke_strategy_undo_command_based.h" #include #include "kis_image_interfaces.h" #include "kis_post_execution_undo_adapter.h" #include "commands_new/kis_saved_commands.h" KisStrokeStrategyUndoCommandBased:: KisStrokeStrategyUndoCommandBased(const KUndo2MagicString &name, bool undo, KisStrokeUndoFacade *undoFacade, KUndo2CommandSP initCommand, KUndo2CommandSP finishCommand) - : KisRunnableBasedStrokeStrategy("STROKE_UNDO_COMMAND_BASED", name), + : KisRunnableBasedStrokeStrategy(QLatin1String("STROKE_UNDO_COMMAND_BASED"), name), m_undo(undo), m_initCommand(initCommand), m_finishCommand(finishCommand), m_undoFacade(undoFacade), m_macroId(-1), m_macroCommand(0) { enableJob(KisSimpleStrokeStrategy::JOB_INIT); enableJob(KisSimpleStrokeStrategy::JOB_FINISH); enableJob(KisSimpleStrokeStrategy::JOB_CANCEL); enableJob(KisSimpleStrokeStrategy::JOB_DOSTROKE); } KisStrokeStrategyUndoCommandBased:: KisStrokeStrategyUndoCommandBased(const KisStrokeStrategyUndoCommandBased &rhs) : KisRunnableBasedStrokeStrategy(rhs), m_undo(false), m_initCommand(rhs.m_initCommand), m_finishCommand(rhs.m_finishCommand), m_undoFacade(rhs.m_undoFacade), m_macroCommand(0) { KIS_ASSERT_RECOVER_NOOP(!rhs.m_macroCommand && !rhs.m_undo && "After the stroke has been started, no copying must happen"); } void KisStrokeStrategyUndoCommandBased::setUsedWhileUndoRedo(bool value) { setClearsRedoOnStart(!value); } void KisStrokeStrategyUndoCommandBased::executeCommand(KUndo2CommandSP command, bool undo) { if(!command) return; if (MutatedCommandInterface *mutatedCommand = dynamic_cast(command.data())) { mutatedCommand->setRunnableJobsInterface(this->runnableJobsInterface()); } if(undo) { command->undo(); } else { command->redo(); } } void KisStrokeStrategyUndoCommandBased::initStrokeCallback() { if(m_undoFacade) { m_macroCommand = m_undoFacade->postExecutionUndoAdapter()->createMacro(name()); } executeCommand(m_initCommand, m_undo); notifyCommandDone(m_initCommand, KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::NORMAL); } void KisStrokeStrategyUndoCommandBased::finishStrokeCallback() { executeCommand(m_finishCommand, m_undo); notifyCommandDone(m_finishCommand, KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::NORMAL); QMutexLocker locker(&m_mutex); if(m_macroCommand) { Q_ASSERT(m_undoFacade); postProcessToplevelCommand(m_macroCommand); m_undoFacade->postExecutionUndoAdapter()->addMacro(m_macroCommand); m_macroCommand = 0; } } void KisStrokeStrategyUndoCommandBased::cancelStrokeCallback() { QMutexLocker locker(&m_mutex); if(m_macroCommand) { m_macroCommand->performCancel(cancelStrokeId(), m_undo); delete m_macroCommand; m_macroCommand = 0; } } void KisStrokeStrategyUndoCommandBased::doStrokeCallback(KisStrokeJobData *data) { Data *d = dynamic_cast(data); if (d) { executeCommand(d->command, d->undo); if (d->shouldGoToHistory) { notifyCommandDone(d->command, d->sequentiality(), d->exclusivity()); } } else { KisRunnableBasedStrokeStrategy::doStrokeCallback(data); } } void KisStrokeStrategyUndoCommandBased::runAndSaveCommand(KUndo2CommandSP command, KisStrokeJobData::Sequentiality sequentiality, KisStrokeJobData::Exclusivity exclusivity) { if (!command) return; executeCommand(command, false); notifyCommandDone(command, sequentiality, exclusivity); } void KisStrokeStrategyUndoCommandBased::notifyCommandDone(KUndo2CommandSP command, KisStrokeJobData::Sequentiality sequentiality, KisStrokeJobData::Exclusivity exclusivity) { if(!command) return; QMutexLocker locker(&m_mutex); if(m_macroCommand) { m_macroCommand->addCommand(command, sequentiality, exclusivity); } } void KisStrokeStrategyUndoCommandBased::setCommandExtraData(KUndo2CommandExtraData *data) { if (m_undoFacade && m_macroCommand) { warnKrita << "WARNING: KisStrokeStrategyUndoCommandBased::setCommandExtraData():" << "the extra data is set while the stroke has already been started!" << "The result is undefined, continued actions may not work!"; } m_commandExtraData.reset(data); } void KisStrokeStrategyUndoCommandBased::setMacroId(int value) { m_macroId = value; } void KisStrokeStrategyUndoCommandBased::postProcessToplevelCommand(KUndo2Command *command) { if (m_commandExtraData) { command->setExtraData(m_commandExtraData.take()); } KisSavedMacroCommand *savedCommand = dynamic_cast(command); if (savedCommand) { savedCommand->setMacroId(m_macroId); } } KisStrokeUndoFacade* KisStrokeStrategyUndoCommandBased::undoFacade() const { return m_undoFacade; } diff --git a/libs/image/kis_suspend_projection_updates_stroke_strategy.cpp b/libs/image/kis_suspend_projection_updates_stroke_strategy.cpp index aff271b066..e95792849c 100644 --- a/libs/image/kis_suspend_projection_updates_stroke_strategy.cpp +++ b/libs/image/kis_suspend_projection_updates_stroke_strategy.cpp @@ -1,555 +1,557 @@ /* * Copyright (c) 2014 Dmitry Kazakov * * 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_suspend_projection_updates_stroke_strategy.h" #include #include #include #include "kis_image_signal_router.h" #include "kundo2command.h" #include "KisRunnableStrokeJobDataBase.h" #include "KisRunnableStrokeJobsInterface.h" #include "kis_paintop_utils.h" inline uint qHash(const QRect &rc) { return rc.x() + (rc.y() << 16) + (rc.width() << 8) + (rc.height() << 24); } struct KisSuspendProjectionUpdatesStrokeStrategy::Private { KisImageWSP image; bool suspend; QVector accumulatedDirtyRects; bool sanityResumingFinished = false; int updatesEpoch = 0; bool haveDisabledGUILodSync = false; void tryFetchUsedUpdatesFilter(KisImageSP image); void tryIssueRecordedDirtyRequests(KisImageSP image); class SuspendLod0Updates : public KisProjectionUpdatesFilter { struct Request { Request() : resetAnimationCache(false) {} Request(const QRect &_rect, bool _resetAnimationCache) : rect(_rect), resetAnimationCache(_resetAnimationCache) { } QRect rect; bool resetAnimationCache; }; typedef QHash > RectsHash; public: SuspendLod0Updates() { } bool filter(KisImage *image, KisNode *node, const QVector &rects, bool resetAnimationCache) override { if (image->currentLevelOfDetail() > 0) return false; QMutexLocker l(&m_mutex); Q_FOREACH(const QRect &rc, rects) { m_requestsHash[KisNodeSP(node)].append(Request(rc, resetAnimationCache)); } return true; } static inline QRect alignRect(const QRect &rc, const int step) { static const int decstep = step - 1; static const int invstep = ~decstep; int x0, y0, x1, y1; rc.getCoords(&x0, &y0, &x1, &y1); x0 &= invstep; y0 &= invstep; x1 |= decstep; y1 |= decstep; QRect result; result.setCoords(x0, y0, x1, y1); return result; } void notifyUpdates(KisNodeGraphListener *listener) { RectsHash::const_iterator it = m_requestsHash.constBegin(); RectsHash::const_iterator end = m_requestsHash.constEnd(); const int step = 64; for (; it != end; ++it) { KisNodeSP node = it.key(); QRegion region; bool resetAnimationCache = false; Q_FOREACH (const Request &req, it.value()) { region += alignRect(req.rect, step); resetAnimationCache |= req.resetAnimationCache; } // FIXME: constness: port rPU to SP listener->requestProjectionUpdate(const_cast(node.data()), region.rects(), resetAnimationCache); } } private: RectsHash m_requestsHash; QMutex m_mutex; }; QVector> usedFilters; struct StrokeJobCommand : public KUndo2Command { StrokeJobCommand(KisStrokeJobData::Sequentiality sequentiality = KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::Exclusivity exclusivity = KisStrokeJobData::NORMAL) : m_sequentiality(sequentiality), m_exclusivity(exclusivity) {} KisStrokeJobData::Sequentiality m_sequentiality; KisStrokeJobData::Exclusivity m_exclusivity; }; struct UndoableData : public KisRunnableStrokeJobDataBase { UndoableData(StrokeJobCommand *command) : KisRunnableStrokeJobDataBase(command->m_sequentiality, command->m_exclusivity), m_command(command) { } void run() override { KIS_SAFE_ASSERT_RECOVER_RETURN(m_command); m_command->redo(); } QScopedPointer m_command; }; // Suspend job should be a barrier to ensure all // previous lodN strokes reach the GUI. Otherwise, // they will be blocked in // KisImage::notifyProjectionUpdated() struct SuspendUpdatesCommand : public StrokeJobCommand { SuspendUpdatesCommand(Private *d) : StrokeJobCommand(KisStrokeJobData::BARRIER), m_d(d) {} void redo() override { KisImageSP image = m_d->image.toStrongRef(); KIS_SAFE_ASSERT_RECOVER_RETURN(image); KIS_SAFE_ASSERT_RECOVER_RETURN(!image->projectionUpdatesFilter()); image->setProjectionUpdatesFilter( KisProjectionUpdatesFilterSP(new Private::SuspendLod0Updates())); } void undo() override { KisImageSP image = m_d->image.toStrongRef(); KIS_SAFE_ASSERT_RECOVER_RETURN(image); KIS_SAFE_ASSERT_RECOVER_RETURN(image->projectionUpdatesFilter()); m_d->tryFetchUsedUpdatesFilter(image); } Private *m_d; }; struct ResumeAndIssueGraphUpdatesCommand : public StrokeJobCommand { ResumeAndIssueGraphUpdatesCommand(Private *d) : StrokeJobCommand(KisStrokeJobData::BARRIER), m_d(d) {} void redo() override { KisImageSP image = m_d->image.toStrongRef(); KIS_SAFE_ASSERT_RECOVER_RETURN(image); KIS_SAFE_ASSERT_RECOVER_RETURN(image->projectionUpdatesFilter()); image->disableUIUpdates(); m_d->tryFetchUsedUpdatesFilter(image); m_d->tryIssueRecordedDirtyRequests(image); } void undo() override { KisImageSP image = m_d->image.toStrongRef(); KIS_SAFE_ASSERT_RECOVER_RETURN(image); KIS_SAFE_ASSERT_RECOVER_RETURN(!image->projectionUpdatesFilter()); image->setProjectionUpdatesFilter( KisProjectionUpdatesFilterSP(new Private::SuspendLod0Updates())); image->enableUIUpdates(); } Private *m_d; }; struct UploadDataToUIData : public KisRunnableStrokeJobDataBase { UploadDataToUIData(const QRect &rc, int updateEpoch, KisSuspendProjectionUpdatesStrokeStrategy *strategy) : KisRunnableStrokeJobDataBase(KisStrokeJobData::CONCURRENT), m_strategy(strategy), m_rc(rc), m_updateEpoch(updateEpoch) { } void run() override { // check if we've already started stinking... if (m_strategy->m_d->updatesEpoch > m_updateEpoch) { return; } KisImageSP image = m_strategy->m_d->image.toStrongRef(); KIS_SAFE_ASSERT_RECOVER_RETURN(image); image->notifyProjectionUpdated(m_rc); } KisSuspendProjectionUpdatesStrokeStrategy *m_strategy; QRect m_rc; int m_updateEpoch; }; struct BlockUILodSync : public KisRunnableStrokeJobDataBase { BlockUILodSync(bool block, KisSuspendProjectionUpdatesStrokeStrategy *strategy) : KisRunnableStrokeJobDataBase(KisStrokeJobData::BARRIER), m_strategy(strategy), m_block(block) {} void run() override { KisImageSP image = m_strategy->m_d->image.toStrongRef(); KIS_SAFE_ASSERT_RECOVER_RETURN(image); image->signalRouter()->emitRequestLodPlanesSyncBlocked(m_block); m_strategy->m_d->haveDisabledGUILodSync = m_block; } KisSuspendProjectionUpdatesStrokeStrategy *m_strategy; bool m_block; }; struct StartBatchUIUpdatesCommand : public StrokeJobCommand { StartBatchUIUpdatesCommand(KisSuspendProjectionUpdatesStrokeStrategy *strategy) : StrokeJobCommand(KisStrokeJobData::BARRIER), m_strategy(strategy) {} void redo() override { KisImageSP image = m_strategy->m_d->image.toStrongRef(); KIS_SAFE_ASSERT_RECOVER_RETURN(image); /** * We accumulate dirty rects from all(!) epochs, because some updates of the * previous epochs might have been cancelled without doing any real work. */ const QVector totalDirtyRects = image->enableUIUpdates() + m_strategy->m_d->accumulatedDirtyRects; const QRect totalRect = image->bounds() & std::accumulate(totalDirtyRects.begin(), totalDirtyRects.end(), QRect(), std::bit_or()); m_strategy->m_d->accumulatedDirtyRects = KisPaintOpUtils::splitAndFilterDabRect(totalRect, totalDirtyRects, KritaUtils::optimalPatchSize().width()); image->signalRouter()->emitNotifyBatchUpdateStarted(); QVector jobsData; Q_FOREACH (const QRect &rc, m_strategy->m_d->accumulatedDirtyRects) { jobsData << new Private::UploadDataToUIData(rc, m_strategy->m_d->updatesEpoch, m_strategy); } m_strategy->runnableJobsInterface()->addRunnableJobs(jobsData); } void undo() override { KisImageSP image = m_strategy->m_d->image.toStrongRef(); KIS_SAFE_ASSERT_RECOVER_RETURN(image); image->signalRouter()->emitNotifyBatchUpdateEnded(); image->disableUIUpdates(); } KisSuspendProjectionUpdatesStrokeStrategy *m_strategy; }; struct EndBatchUIUpdatesCommand : public StrokeJobCommand { EndBatchUIUpdatesCommand(KisSuspendProjectionUpdatesStrokeStrategy *strategy) : StrokeJobCommand(KisStrokeJobData::BARRIER), m_strategy(strategy) {} void redo() override { KisImageSP image = m_strategy->m_d->image.toStrongRef(); KIS_SAFE_ASSERT_RECOVER_RETURN(image); image->signalRouter()->emitNotifyBatchUpdateEnded(); m_strategy->m_d->sanityResumingFinished = true; m_strategy->m_d->accumulatedDirtyRects.clear(); KIS_SAFE_ASSERT_RECOVER_NOOP(m_strategy->m_d->usedFilters.isEmpty()); } void undo() override { /** * Even though this comand is the last command of the stroke is can * still be undone by suspendStrokeCallback(). It happens when a LodN * stroke is started right after the last job of resume strategy was * being executed. In such a case new stroke is placed right in front * of our resume strategy and all the resuming work is undone (mimicing * a normal suspend strategy). * * The only thing we should control here is whether the state of the * stroke is reset to default. Otherwise we'll do all the updates twice. */ KIS_SAFE_ASSERT_RECOVER_NOOP(m_strategy->m_d->usedFilters.isEmpty()); KIS_SAFE_ASSERT_RECOVER_NOOP(m_strategy->m_d->accumulatedDirtyRects.isEmpty()); m_strategy->m_d->sanityResumingFinished = false; KisImageSP image = m_strategy->m_d->image.toStrongRef(); KIS_SAFE_ASSERT_RECOVER_RETURN(image); image->signalRouter()->emitNotifyBatchUpdateStarted(); } KisSuspendProjectionUpdatesStrokeStrategy *m_strategy; }; QVector executedCommands; }; KisSuspendProjectionUpdatesStrokeStrategy::KisSuspendProjectionUpdatesStrokeStrategy(KisImageWSP image, bool suspend) - : KisRunnableBasedStrokeStrategy(suspend ? "suspend_stroke_strategy" : "resume_stroke_strategy"), + : KisRunnableBasedStrokeStrategy(suspend ? + QLatin1String("suspend_stroke_strategy") : + QLatin1String("resume_stroke_strategy")), m_d(new Private) { m_d->image = image; m_d->suspend = suspend; /** * Here we add a dumb INIT job so that KisStrokesQueue would know that the * stroke has already started or not. When the queue reaches the resume * stroke and starts its execution, no Lod0 can execute anymore. So all the * new Lod0 strokes should go to the end of the queue and wrapped into * their own Suspend/Resume pair. */ enableJob(JOB_INIT, true); enableJob(JOB_DOSTROKE, true); enableJob(JOB_CANCEL, true); enableJob(JOB_SUSPEND, true, KisStrokeJobData::BARRIER); enableJob(JOB_RESUME, true, KisStrokeJobData::BARRIER); setNeedsExplicitCancel(true); } KisSuspendProjectionUpdatesStrokeStrategy::~KisSuspendProjectionUpdatesStrokeStrategy() { qDeleteAll(m_d->executedCommands); } void KisSuspendProjectionUpdatesStrokeStrategy::initStrokeCallback() { QVector jobs; if (m_d->suspend) { jobs << new Private::UndoableData(new Private::SuspendUpdatesCommand(m_d.data())); } else { jobs << new Private::UndoableData(new Private::ResumeAndIssueGraphUpdatesCommand(m_d.data())); jobs << new Private::BlockUILodSync(true, this); jobs << new Private::UndoableData(new Private::StartBatchUIUpdatesCommand(this)); jobs << new Private::UndoableData(new Private::EndBatchUIUpdatesCommand(this)); jobs << new Private::BlockUILodSync(false, this); } runnableJobsInterface()->addRunnableJobs(jobs); } /** * When the Lod0 stroke is being recalculated in the background we * should block all the updates it issues to avoid user distraction. * The result of the final stroke should be shown to the user in the * very end when everything is fully ready. Ideally the use should not * notice that the image has changed :) * * (Don't mix this process with suspend/resume capabilities of a * single stroke. That is a different system!) * * The process of the Lod0 regeneration consists of the following: * * 1) Suspend stroke executes. It sets a special updates filter on the * image. The filter blocks all the updates and saves them in an * internal structure to be emitted in the future. * * 2) Lod0 strokes are being recalculated. All their updates are * blocked and saved in the filter. * * 3) Resume stroke starts: * * 3.1) First it disables emitting of sigImageUpdated() so the gui * will not get any update notifications. * * 3.2) Then it enables updates themselves. * * 3.3) Initiates all the updates that were requested by the Lod0 * stroke. The node graph is regenerated, but the GUI does * not get this change. * * 3.4) Special barrier job waits for all the updates to finish * and, when they are done, enables GUI notifications again. * * 3.5) In a multithreaded way emits the GUI notifications for the * entire image. Multithreaded way is used to conform the * double-stage update principle of KisCanvas2. */ void KisSuspendProjectionUpdatesStrokeStrategy::doStrokeCallback(KisStrokeJobData *data) { KisRunnableStrokeJobDataBase *runnable = dynamic_cast(data); if (runnable) { runnable->run(); if (Private::UndoableData *undoable = dynamic_cast(data)) { Private::StrokeJobCommand *command = undoable->m_command.take(); m_d->executedCommands.append(command); } } } QList KisSuspendProjectionUpdatesStrokeStrategy::createSuspendJobsData(KisImageWSP /*image*/) { return QList(); } QList KisSuspendProjectionUpdatesStrokeStrategy::createResumeJobsData(KisImageWSP /*_image*/) { return QList(); } void KisSuspendProjectionUpdatesStrokeStrategy::Private::tryFetchUsedUpdatesFilter(KisImageSP image) { KisProjectionUpdatesFilterSP filter = image->projectionUpdatesFilter(); if (!filter) return; QSharedPointer localFilter = filter.dynamicCast(); if (localFilter) { image->setProjectionUpdatesFilter(KisProjectionUpdatesFilterSP()); this->usedFilters.append(localFilter); } } void KisSuspendProjectionUpdatesStrokeStrategy::Private::tryIssueRecordedDirtyRequests(KisImageSP image) { Q_FOREACH (QSharedPointer filter, usedFilters) { filter->notifyUpdates(image.data()); } usedFilters.clear(); } void KisSuspendProjectionUpdatesStrokeStrategy::cancelStrokeCallback() { KisImageSP image = m_d->image.toStrongRef(); if (!image) { return; } for (auto it = m_d->executedCommands.rbegin(); it != m_d->executedCommands.rend(); ++it) { (*it)->undo(); } m_d->tryFetchUsedUpdatesFilter(image); if (m_d->haveDisabledGUILodSync) { image->signalRouter()->emitRequestLodPlanesSyncBlocked(false); } /** * We shouldn't emit any ad-hoc updates when cancelling the * stroke. It generates weird temporary holes on the canvas, * making the user feel awful, thinking his image got * corrupted. We will just emit a common refreshGraphAsync() that * will do all the work in a beautiful way */ if (!m_d->suspend) { // FIXME: optimize image->refreshGraphAsync(); } } void KisSuspendProjectionUpdatesStrokeStrategy::suspendStrokeCallback() { /** * The resume stroke can be suspended even when all its jobs are completed. * In such a case, we should just ensure that all the internal state is reset * to default. */ KIS_SAFE_ASSERT_RECOVER_NOOP(m_d->suspend || !m_d->sanityResumingFinished || (m_d->sanityResumingFinished && m_d->usedFilters.isEmpty() && m_d->accumulatedDirtyRects.isEmpty())); for (auto it = m_d->executedCommands.rbegin(); it != m_d->executedCommands.rend(); ++it) { (*it)->undo(); } // reset all the issued updates m_d->updatesEpoch++; } void KisSuspendProjectionUpdatesStrokeStrategy::resumeStrokeCallback() { QVector jobs; Q_FOREACH (Private::StrokeJobCommand *command, m_d->executedCommands) { jobs << new Private::UndoableData(command); } m_d->executedCommands.clear(); runnableJobsInterface()->addRunnableJobs(jobs); } diff --git a/libs/image/kis_switch_time_stroke_strategy.cpp b/libs/image/kis_switch_time_stroke_strategy.cpp index 16c71098f3..76c12208af 100644 --- a/libs/image/kis_switch_time_stroke_strategy.cpp +++ b/libs/image/kis_switch_time_stroke_strategy.cpp @@ -1,150 +1,150 @@ /* * Copyright (c) 2015 Dmitry Kazakov * * 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_switch_time_stroke_strategy.h" #include #include "kis_image_animation_interface.h" #include "kis_post_execution_undo_adapter.h" #include "commands_new/kis_switch_current_time_command.h" struct KisSwitchTimeStrokeStrategy::Private { Private(int frameId, bool needsRegeneration) : token(new SharedToken(frameId, needsRegeneration)) { } KisImageAnimationInterface *interface; KisPostExecutionUndoAdapter *undoAdapter; SharedTokenSP token; }; KisSwitchTimeStrokeStrategy::KisSwitchTimeStrokeStrategy(int frameId, bool needsRegeneration, KisImageAnimationInterface *interface, KisPostExecutionUndoAdapter *undoAdapter) - : KisSimpleStrokeStrategy("switch_current_frame_stroke", kundo2_i18n("Switch Frames")), + : KisSimpleStrokeStrategy(QLatin1String("switch_current_frame_stroke"), kundo2_i18n("Switch Frames")), m_d(new Private(frameId, needsRegeneration)) { m_d->interface = interface; m_d->undoAdapter = undoAdapter; enableJob(JOB_INIT, true, KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::EXCLUSIVE); // switching frames is a distinct user action, so it should // cancel the playback or any action easily setRequestsOtherStrokesToEnd(true); setClearsRedoOnStart(false); } KisSwitchTimeStrokeStrategy::~KisSwitchTimeStrokeStrategy() { } void KisSwitchTimeStrokeStrategy::initStrokeCallback() { const int frameId = m_d->token->fetchTime(); if (frameId == m_d->interface->currentTime()) return; const int oldTime = m_d->interface->currentTime(); m_d->interface->explicitlySetCurrentTime(frameId); if (m_d->undoAdapter) { KUndo2CommandSP cmd( new KisSwitchCurrentTimeCommand(m_d->interface, oldTime, frameId)); m_d->undoAdapter->addCommand(cmd); } } KisStrokeStrategy* KisSwitchTimeStrokeStrategy::createLodClone(int levelOfDetail) { Q_UNUSED(levelOfDetail); // This stroke is explicitly made LEGACY, so that it could create a barrier for // time switch. Theoretically, we can have separate time ids for Lod0 and LodN, // but currently this idea doesn't wotk for some reason. The consequences of // making this stroke a barrier: // // * LodSync stroke is started after every time switch (not slow, but still...) // * The frame switches only *after* all the pending strokes are finished // * LodSync stroke is started even when the image is not changed (not regeneration needed)! return 0; } KisSwitchTimeStrokeStrategy::SharedTokenSP KisSwitchTimeStrokeStrategy::token() const { return m_d->token; } /*****************************************************************/ /* KisSwitchTimeStrokeStrategy::SharedToken */ /*****************************************************************/ struct KisSwitchTimeStrokeStrategy::SharedToken::Private { Private(int _time, bool _needsRegeneration) : time(_time), needsRegeneration(_needsRegeneration), isCompleted(false) { } QMutex mutex; int time; bool needsRegeneration; bool isCompleted; }; KisSwitchTimeStrokeStrategy::SharedToken::SharedToken(int initialTime, bool needsRegeneration) : m_d(new Private(initialTime, needsRegeneration)) { } KisSwitchTimeStrokeStrategy::SharedToken::~SharedToken() { } bool KisSwitchTimeStrokeStrategy::SharedToken::tryResetDestinationTime(int time, bool needsRegeneration) { QMutexLocker l(&m_d->mutex); const bool result = !m_d->isCompleted && (m_d->needsRegeneration || !needsRegeneration); if (result) { m_d->time = time; } return result; } int KisSwitchTimeStrokeStrategy::SharedToken::fetchTime() const { QMutexLocker l(&m_d->mutex); KIS_SAFE_ASSERT_RECOVER_NOOP(!m_d->isCompleted); m_d->isCompleted = true; return m_d->time; } diff --git a/libs/image/kis_sync_lod_cache_stroke_strategy.cpp b/libs/image/kis_sync_lod_cache_stroke_strategy.cpp index 5715b7cd0f..8daf958387 100644 --- a/libs/image/kis_sync_lod_cache_stroke_strategy.cpp +++ b/libs/image/kis_sync_lod_cache_stroke_strategy.cpp @@ -1,171 +1,171 @@ /* * Copyright (c) 2014 Dmitry Kazakov * * 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_sync_lod_cache_stroke_strategy.h" #include #include #include "krita_utils.h" #include "kis_layer_utils.h" struct KisSyncLodCacheStrokeStrategy::Private { KisImageWSP image; QHash dataObjects; ~Private() { qDeleteAll(dataObjects); dataObjects.clear(); } class InitData : public KisStrokeJobData { public: InitData(KisPaintDeviceSP _device) : KisStrokeJobData(SEQUENTIAL), device(_device) {} KisPaintDeviceSP device; }; class ProcessData : public KisStrokeJobData { public: ProcessData(KisPaintDeviceSP _device, const QRect &_rect) : KisStrokeJobData(CONCURRENT), device(_device), rect(_rect) {} KisPaintDeviceSP device; QRect rect; }; class AdditionalProcessNode : public KisStrokeJobData { public: AdditionalProcessNode(KisNodeSP _node) : KisStrokeJobData(SEQUENTIAL), node(_node) {} KisNodeSP node; }; }; KisSyncLodCacheStrokeStrategy::KisSyncLodCacheStrokeStrategy(KisImageWSP image, bool forgettable) - : KisSimpleStrokeStrategy("SyncLodCacheStroke", kundo2_i18n("Instant Preview")), + : KisSimpleStrokeStrategy(QLatin1String("SyncLodCacheStroke"), kundo2_i18n("Instant Preview")), m_d(new Private) { m_d->image = image; /** * We shouldn't start syncing before all the updates are * done. Otherwise we might get artifacts! */ enableJob(KisSimpleStrokeStrategy::JOB_INIT, true, KisStrokeJobData::BARRIER, KisStrokeJobData::EXCLUSIVE); enableJob(KisSimpleStrokeStrategy::JOB_DOSTROKE); enableJob(KisSimpleStrokeStrategy::JOB_FINISH); enableJob(KisSimpleStrokeStrategy::JOB_CANCEL, true, KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::EXCLUSIVE); setRequestsOtherStrokesToEnd(false); setClearsRedoOnStart(false); setCanForgetAboutMe(forgettable); } KisSyncLodCacheStrokeStrategy::~KisSyncLodCacheStrokeStrategy() { } void KisSyncLodCacheStrokeStrategy::doStrokeCallback(KisStrokeJobData *data) { Private::InitData *initData = dynamic_cast(data); Private::ProcessData *processData = dynamic_cast(data); Private::AdditionalProcessNode *additionalProcessNode = dynamic_cast(data); if (initData) { KisPaintDeviceSP dev = initData->device; const int lod = dev->defaultBounds()->currentLevelOfDetail(); m_d->dataObjects.insert(dev, dev->createLodDataStruct(lod)); } else if (processData) { KisPaintDeviceSP dev = processData->device; KIS_ASSERT(m_d->dataObjects.contains(dev)); KisPaintDevice::LodDataStruct *data = m_d->dataObjects.value(dev); dev->updateLodDataStruct(data, processData->rect); } else if (additionalProcessNode) { additionalProcessNode->node->syncLodCache(); } } void KisSyncLodCacheStrokeStrategy::finishStrokeCallback() { auto it = m_d->dataObjects.begin(); auto end = m_d->dataObjects.end(); for (; it != end; ++it) { KisPaintDeviceSP dev = it.key(); dev->uploadLodDataStruct(it.value()); } qDeleteAll(m_d->dataObjects); m_d->dataObjects.clear(); } void KisSyncLodCacheStrokeStrategy::cancelStrokeCallback() { qDeleteAll(m_d->dataObjects); m_d->dataObjects.clear(); } QList KisSyncLodCacheStrokeStrategy::createJobsData(KisImageWSP _image) { using KisLayerUtils::recursiveApplyNodes; using KritaUtils::splitRegionIntoPatches; using KritaUtils::optimalPatchSize; KisImageSP image = _image; KisPaintDeviceList deviceList; QList jobsData; recursiveApplyNodes(image->root(), [&deviceList](KisNodeSP node) { deviceList << node->getLodCapableDevices(); }); KritaUtils::makeContainerUnique(deviceList); Q_FOREACH (KisPaintDeviceSP device, deviceList) { jobsData << new Private::InitData(device); } Q_FOREACH (KisPaintDeviceSP device, deviceList) { QRegion region = device->regionForLodSyncing(); QVector rects = splitRegionIntoPatches(region, optimalPatchSize()); Q_FOREACH (const QRect &rc, rects) { jobsData << new Private::ProcessData(device, rc); } } recursiveApplyNodes(image->root(), [&jobsData](KisNodeSP node) { jobsData << new Private::AdditionalProcessNode(node); }); return jobsData; } diff --git a/libs/image/kis_update_job_item.h b/libs/image/kis_update_job_item.h index ba68ab70c4..3a6f41b27c 100644 --- a/libs/image/kis_update_job_item.h +++ b/libs/image/kis_update_job_item.h @@ -1,251 +1,271 @@ /* * Copyright (c) 2011 Dmitry Kazakov * * 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. */ #ifndef __KIS_UPDATE_JOB_ITEM_H #define __KIS_UPDATE_JOB_ITEM_H #include #include #include #include "kis_stroke_job.h" #include "kis_spontaneous_job.h" #include "kis_base_rects_walker.h" #include "kis_async_merger.h" #include "kis_updater_context.h" +//#define DEBUG_JOBS_SEQUENCE + class KisUpdateJobItem : public QObject, public QRunnable { Q_OBJECT public: enum class Type : int { EMPTY = 0, WAITING, MERGE, STROKE, SPONTANEOUS }; public: KisUpdateJobItem(KisUpdaterContext *updaterContext) : m_updaterContext(updaterContext), m_atomicType(Type::EMPTY), m_runnableJob(0) { setAutoDelete(false); KIS_SAFE_ASSERT_RECOVER_NOOP(m_atomicType.is_lock_free()); } ~KisUpdateJobItem() override { delete m_runnableJob; } void run() override { if (!isRunning()) return; /** * Here we break the idea of QThreadPool a bit. Ideally, we should split the * jobs into distinct QRunnable objects and pass all of them to QThreadPool. * That is a nice idea, but it doesn't work well when the jobs are small enough * and the number of available cores is high (>4 cores). It this case the * threads just tend to execute the job very quickly and go to sleep, which is * an expensive operation. * * To overcome this problem we try to bulk-process the jobs. In sigJobFinished() * signal (which is DirectConnection), the context may add the job to ourselves(!!!), * so we switch from "done" state into "running" again. */ while (1) { KIS_SAFE_ASSERT_RECOVER_RETURN(isRunning()); if(m_exclusive) { m_updaterContext->m_exclusiveJobLock.lockForWrite(); } else { m_updaterContext->m_exclusiveJobLock.lockForRead(); } if(m_atomicType == Type::MERGE) { runMergeJob(); } else { KIS_ASSERT(m_atomicType == Type::STROKE || m_atomicType == Type::SPONTANEOUS); - if (m_runnableJob) m_runnableJob->run(); + if (m_runnableJob) { +#ifdef DEBUG_JOBS_SEQUENCE + if (m_atomicType == Type::STROKE) { + qDebug() << "running: stroke" << m_runnableJob->debugName(); + } else if (m_atomicType == Type::SPONTANEOUS) { + qDebug() << "running: spont " << m_runnableJob->debugName(); + } else { + qDebug() << "running: unkn. " << m_runnableJob->debugName(); + } +#endif + + m_runnableJob->run(); + } } setDone(); m_updaterContext->doSomeUsefulWork(); // may flip the current state from Waiting -> Running again m_updaterContext->jobFinished(); m_updaterContext->m_exclusiveJobLock.unlock(); // try to exit the loop. Please note, that no one can flip the state from // WAITING to EMPTY except ourselves! Type expectedValue = Type::WAITING; if (m_atomicType.compare_exchange_strong(expectedValue, Type::EMPTY)) { break; } } } inline void runMergeJob() { KIS_SAFE_ASSERT_RECOVER_RETURN(m_atomicType == Type::MERGE); KIS_SAFE_ASSERT_RECOVER_RETURN(m_walker); // dbgKrita << "Executing merge job" << m_walker->changeRect() // << "on thread" << QThread::currentThreadId(); + +#ifdef DEBUG_JOBS_SEQUENCE + qDebug() << "running: merge " << m_walker->startNode() << m_walker->changeRect(); + +#endif + m_merger.startMerge(*m_walker); QRect changeRect = m_walker->changeRect(); m_updaterContext->continueUpdate(changeRect); } // return true if the thread should actually be started inline bool setWalker(KisBaseRectsWalkerSP walker) { KIS_ASSERT(m_atomicType <= Type::WAITING); m_accessRect = walker->accessRect(); m_changeRect = walker->changeRect(); m_walker = walker; m_exclusive = false; m_runnableJob = 0; const Type oldState = m_atomicType.exchange(Type::MERGE); return oldState == Type::EMPTY; } // return true if the thread should actually be started inline bool setStrokeJob(KisStrokeJob *strokeJob) { KIS_ASSERT(m_atomicType <= Type::WAITING); m_runnableJob = strokeJob; m_strokeJobSequentiality = strokeJob->sequentiality(); m_exclusive = strokeJob->isExclusive(); m_walker = 0; m_accessRect = m_changeRect = QRect(); const Type oldState = m_atomicType.exchange(Type::STROKE); return oldState == Type::EMPTY; } // return true if the thread should actually be started inline bool setSpontaneousJob(KisSpontaneousJob *spontaneousJob) { KIS_ASSERT(m_atomicType <= Type::WAITING); m_runnableJob = spontaneousJob; m_exclusive = spontaneousJob->isExclusive(); m_walker = 0; m_accessRect = m_changeRect = QRect(); const Type oldState = m_atomicType.exchange(Type::SPONTANEOUS); return oldState == Type::EMPTY; } inline void setDone() { m_walker = 0; delete m_runnableJob; m_runnableJob = 0; m_atomicType = Type::WAITING; } inline bool isRunning() const { return m_atomicType >= Type::MERGE; } inline Type type() const { return m_atomicType; } inline const QRect& accessRect() const { return m_accessRect; } inline const QRect& changeRect() const { return m_changeRect; } inline KisStrokeJobData::Sequentiality strokeJobSequentiality() const { return m_strokeJobSequentiality; } private: /** * Open walker and stroke job for the testing suite. * Please, do not use it in production code. */ friend class KisTestableUpdaterContext; friend class KisSimpleUpdateQueueTest; friend class KisStrokesQueueTest; friend class KisUpdateSchedulerTest; friend class KisUpdaterContext; inline KisBaseRectsWalkerSP walker() const { return m_walker; } inline KisStrokeJob* strokeJob() const { KisStrokeJob *job = dynamic_cast(m_runnableJob); Q_ASSERT(job); return job; } inline void testingSetDone() { setDone(); } private: KisUpdaterContext *m_updaterContext; bool m_exclusive; std::atomic m_atomicType; volatile KisStrokeJobData::Sequentiality m_strokeJobSequentiality; /** * Runnable jobs part * The job is owned by the context and deleted after completion */ - KisRunnable *m_runnableJob; + KisRunnableWithDebugName *m_runnableJob; /** * Merge jobs part */ KisBaseRectsWalkerSP m_walker; KisAsyncMerger m_merger; /** * These rects cache actual values from the walker * to eliminate concurrent access to a walker structure */ QRect m_accessRect; QRect m_changeRect; }; #endif /* __KIS_UPDATE_JOB_ITEM_H */ diff --git a/libs/image/kis_update_outline_job.cpp b/libs/image/kis_update_outline_job.cpp index 6f1122aa88..dbe877690d 100644 --- a/libs/image/kis_update_outline_job.cpp +++ b/libs/image/kis_update_outline_job.cpp @@ -1,46 +1,51 @@ /* * Copyright (c) 2013 Dmitry Kazakov * * 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_update_outline_job.h" KisUpdateOutlineJob::KisUpdateOutlineJob(KisSelectionSP selection, bool updateThumbnail, const QColor &maskColor) : m_selection(selection), m_updateThumbnail(updateThumbnail), m_maskColor(maskColor) { } bool KisUpdateOutlineJob::overrides(const KisSpontaneousJob *otherJob) { return dynamic_cast(otherJob); } void KisUpdateOutlineJob::run() { m_selection->recalculateOutlineCache(); if (m_updateThumbnail) { m_selection->recalculateThumbnailImage(m_maskColor); } m_selection->notifySelectionChanged(); } int KisUpdateOutlineJob::levelOfDetail() const { return 0; } + +QString KisUpdateOutlineJob::debugName() const +{ + return "KisUpdateOutlineJob"; +} diff --git a/libs/image/kis_update_outline_job.h b/libs/image/kis_update_outline_job.h index e64dc01bf0..071f8d5b85 100644 --- a/libs/image/kis_update_outline_job.h +++ b/libs/image/kis_update_outline_job.h @@ -1,42 +1,43 @@ /* * Copyright (c) 2013 Dmitry Kazakov * * 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. */ #ifndef __KIS_UPDATE_OUTLINE_JOB_H #define __KIS_UPDATE_OUTLINE_JOB_H #include #include "kis_spontaneous_job.h" #include "kis_selection.h" class KRITAIMAGE_EXPORT KisUpdateOutlineJob : public KisSpontaneousJob { public: KisUpdateOutlineJob(KisSelectionSP selection, bool updateThumbnail, const QColor &maskColor); bool overrides(const KisSpontaneousJob *otherJob) override; void run() override; int levelOfDetail() const override; + QString debugName() const override; private: KisSelectionSP m_selection; bool m_updateThumbnail; QColor m_maskColor; }; #endif /* __KIS_UPDATE_OUTLINE_JOB_H */ diff --git a/libs/image/kis_update_selection_job.cpp b/libs/image/kis_update_selection_job.cpp index f555c72d8d..6b328a6211 100644 --- a/libs/image/kis_update_selection_job.cpp +++ b/libs/image/kis_update_selection_job.cpp @@ -1,88 +1,93 @@ /* * Copyright (c) 2013 Dmitry Kazakov * * 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_update_selection_job.h" #include "kis_image.h" #include "kis_projection_leaf.h" KisUpdateSelectionJob::KisUpdateSelectionJob(KisSelectionSP selection, const QRect &updateRect) : m_selection(selection), m_updateRect(updateRect) { /** * TODO: we should implement correct KisShapeSelectionCanvas for * projection. See a comment in KisUpdateSelectionJob::run(). * * Right now, since this job accesses some projections for write, we * should declare it as exclusive */ setExclusive(true); } bool KisUpdateSelectionJob::overrides(const KisSpontaneousJob *_otherJob) { const KisUpdateSelectionJob *otherJob = dynamic_cast(_otherJob); bool retval = false; if (otherJob && otherJob->m_selection == m_selection) { if (!m_updateRect.isEmpty()) { m_updateRect |= otherJob->m_updateRect; } retval = true; } return retval; } void KisUpdateSelectionJob::run() { QRect dirtyRect; KisNodeSP parentNode = m_selection->parentNode(); if (parentNode) { dirtyRect = parentNode->extent(); } if (!m_updateRect.isEmpty()) { m_selection->updateProjection(m_updateRect); } else { m_selection->updateProjection(); } m_selection->notifySelectionChanged(); /** * TODO: in the future we should remove selection projection calculation * from this job and to reuse a fully-featured shape layer canvas * from KisShapeLayer. Then projection calculation will be a little * bit more efficient. */ if (parentNode && parentNode->projectionLeaf()->isOverlayProjectionLeaf()) { dirtyRect |= parentNode->extent(); parentNode->setDirty(dirtyRect); } } int KisUpdateSelectionJob::levelOfDetail() const { return 0; } + +QString KisUpdateSelectionJob::debugName() const +{ + return "KisUpdateSelectionJob"; +} diff --git a/libs/image/kis_update_selection_job.h b/libs/image/kis_update_selection_job.h index 60401491b7..16aba0ac5a 100644 --- a/libs/image/kis_update_selection_job.h +++ b/libs/image/kis_update_selection_job.h @@ -1,39 +1,40 @@ /* * Copyright (c) 2013 Dmitry Kazakov * * 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. */ #ifndef __KIS_UPDATE_SELECTION_JOB_H #define __KIS_UPDATE_SELECTION_JOB_H #include "kis_spontaneous_job.h" #include "kis_selection.h" class KRITAIMAGE_EXPORT KisUpdateSelectionJob : public KisSpontaneousJob { public: KisUpdateSelectionJob(KisSelectionSP selection, const QRect &updateRect = QRect()); bool overrides(const KisSpontaneousJob *otherJob) override; void run() override; int levelOfDetail() const override; + QString debugName() const override; private: KisSelectionSP m_selection; QRect m_updateRect; }; #endif /* __KIS_UPDATE_SELECTION_JOB_H */ diff --git a/libs/image/lazybrush/kis_colorize_stroke_strategy.cpp b/libs/image/lazybrush/kis_colorize_stroke_strategy.cpp index ba301df874..66fcc6d8a7 100644 --- a/libs/image/lazybrush/kis_colorize_stroke_strategy.cpp +++ b/libs/image/lazybrush/kis_colorize_stroke_strategy.cpp @@ -1,282 +1,282 @@ /* * Copyright (c) 2016 Dmitry Kazakov * * 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_colorize_stroke_strategy.h" #include #include "krita_utils.h" #include "kis_paint_device.h" #include "kis_lazy_fill_tools.h" #include "kis_gaussian_kernel.h" #include "kis_painter.h" #include "kis_default_bounds_base.h" #include "kis_lod_transform.h" #include "kis_node.h" #include "kis_image_config.h" #include "KisWatershedWorker.h" #include "kis_processing_visitor.h" #include "kis_transaction.h" #include #include #include using namespace KisLazyFillTools; struct KisColorizeStrokeStrategy::Private { Private() : filteredSourceValid(false) {} Private(const Private &rhs, int _levelOfDetail) : progressNode(rhs.progressNode) , src(rhs.src) , dst(rhs.dst) , filteredSource(rhs.filteredSource) , internalFilteredSource(rhs.internalFilteredSource) , filteredSourceValid(rhs.filteredSourceValid) , boundingRect(rhs.boundingRect) , prefilterOnly(rhs.prefilterOnly) , levelOfDetail(_levelOfDetail) , keyStrokes(rhs.keyStrokes) , filteringOptions(rhs.filteringOptions) {} KisNodeSP progressNode; KisPaintDeviceSP src; KisPaintDeviceSP dst; KisPaintDeviceSP filteredSource; KisPaintDeviceSP heightMap; KisPaintDeviceSP internalFilteredSource; bool filteredSourceValid; QRect boundingRect; bool prefilterOnly = false; int levelOfDetail = 0; QVector keyStrokes; // default values: disabled FilteringOptions filteringOptions; }; KisColorizeStrokeStrategy::KisColorizeStrokeStrategy(KisPaintDeviceSP src, KisPaintDeviceSP dst, KisPaintDeviceSP filteredSource, bool filteredSourceValid, const QRect &boundingRect, KisNodeSP progressNode, bool prefilterOnly) - : KisRunnableBasedStrokeStrategy("colorize-stroke", prefilterOnly ? kundo2_i18n("Prefilter Colorize Mask") : kundo2_i18n("Colorize")), + : KisRunnableBasedStrokeStrategy(QLatin1String("colorize-stroke"), prefilterOnly ? kundo2_i18n("Prefilter Colorize Mask") : kundo2_i18n("Colorize")), m_d(new Private) { m_d->progressNode = progressNode; m_d->src = src; m_d->dst = dst; m_d->filteredSource = filteredSource; m_d->boundingRect = boundingRect; m_d->filteredSourceValid = filteredSourceValid; m_d->prefilterOnly = prefilterOnly; enableJob(JOB_INIT, true, KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::EXCLUSIVE); enableJob(JOB_DOSTROKE, true, KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::EXCLUSIVE); enableJob(JOB_CANCEL, true, KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::EXCLUSIVE); setNeedsExplicitCancel(true); } KisColorizeStrokeStrategy::KisColorizeStrokeStrategy(const KisColorizeStrokeStrategy &rhs, int levelOfDetail) : KisRunnableBasedStrokeStrategy(rhs), m_d(new Private(*rhs.m_d, levelOfDetail)) { KisLodTransform t(levelOfDetail); m_d->boundingRect = t.map(rhs.m_d->boundingRect); } KisColorizeStrokeStrategy::~KisColorizeStrokeStrategy() { } void KisColorizeStrokeStrategy::setFilteringOptions(const FilteringOptions &value) { m_d->filteringOptions = value; } FilteringOptions KisColorizeStrokeStrategy::filteringOptions() const { return m_d->filteringOptions; } void KisColorizeStrokeStrategy::addKeyStroke(KisPaintDeviceSP dev, const KoColor &color) { KoColor convertedColor(color); convertedColor.convertTo(m_d->dst->colorSpace()); m_d->keyStrokes << KeyStroke(dev, convertedColor); } void KisColorizeStrokeStrategy::initStrokeCallback() { using namespace KritaUtils; QVector jobs; const QVector patchRects = splitRectIntoPatches(m_d->boundingRect, optimalPatchSize()); if (!m_d->filteredSourceValid) { // TODO: make this conversion concurrent!!! KisPaintDeviceSP filteredMainDev = KisPainter::convertToAlphaAsAlpha(m_d->src); filteredMainDev->setDefaultBounds(m_d->src->defaultBounds()); struct PrefilterSharedState { QRect boundingRect; KisPaintDeviceSP filteredMainDev; KisPaintDeviceSP filteredMainDevSavedCopy; QScopedPointer activeTransaction; FilteringOptions filteringOptions; }; QSharedPointer state(new PrefilterSharedState()); state->boundingRect = m_d->boundingRect; state->filteredMainDev = filteredMainDev; state->filteringOptions = m_d->filteringOptions; if (m_d->filteringOptions.useEdgeDetection && m_d->filteringOptions.edgeDetectionSize > 0.0) { addJobSequential(jobs, [state] () { state->activeTransaction.reset(new KisTransaction(state->filteredMainDev)); }); Q_FOREACH (const QRect &rc, patchRects) { addJobConcurrent(jobs, [state, rc] () { KisLodTransformScalar t(state->filteredMainDev); KisGaussianKernel::applyLoG(state->filteredMainDev, rc, t.scale(0.5 * state->filteringOptions.edgeDetectionSize), -1.0, QBitArray(), 0); }); } addJobSequential(jobs, [state] () { state->activeTransaction.reset(); normalizeAlpha8Device(state->filteredMainDev, state->boundingRect); state->activeTransaction.reset(new KisTransaction(state->filteredMainDev)); }); Q_FOREACH (const QRect &rc, patchRects) { addJobConcurrent(jobs, [state, rc] () { KisLodTransformScalar t(state->filteredMainDev); KisGaussianKernel::applyGaussian(state->filteredMainDev, rc, t.scale(state->filteringOptions.edgeDetectionSize), t.scale(state->filteringOptions.edgeDetectionSize), QBitArray(), 0); }); } addJobSequential(jobs, [state] () { state->activeTransaction.reset(); }); } if (m_d->filteringOptions.fuzzyRadius > 0) { addJobSequential(jobs, [state] () { state->filteredMainDevSavedCopy = new KisPaintDevice(*state->filteredMainDev); state->activeTransaction.reset(new KisTransaction(state->filteredMainDev)); }); Q_FOREACH (const QRect &rc, patchRects) { addJobConcurrent(jobs, [state, rc] () { KisLodTransformScalar t(state->filteredMainDev); KisGaussianKernel::applyGaussian(state->filteredMainDev, rc, t.scale(state->filteringOptions.fuzzyRadius), t.scale(state->filteringOptions.fuzzyRadius), QBitArray(), 0); KisPainter gc(state->filteredMainDev); gc.bitBlt(rc.topLeft(), state->filteredMainDevSavedCopy, rc); }); } addJobSequential(jobs, [state] () { state->activeTransaction.reset(); }); } addJobSequential(jobs, [this, state] () { normalizeAndInvertAlpha8Device(state->filteredMainDev, state->boundingRect); KisDefaultBoundsBaseSP oldBounds = m_d->filteredSource->defaultBounds(); m_d->filteredSource->makeCloneFrom(state->filteredMainDev, m_d->boundingRect); m_d->filteredSource->setDefaultBounds(oldBounds); m_d->filteredSourceValid = true; }); } if (!m_d->prefilterOnly) { addJobSequential(jobs, [this] () { m_d->heightMap = new KisPaintDevice(*m_d->filteredSource); }); Q_FOREACH (const QRect &rc, patchRects) { addJobConcurrent(jobs, [this, rc] () { KritaUtils::filterAlpha8Device(m_d->heightMap, rc, [](quint8 pixel) { return quint8(255 - pixel); }); }); } addJobSequential(jobs, [this] () { KisProcessingVisitor::ProgressHelper helper(m_d->progressNode); KisWatershedWorker worker(m_d->heightMap, m_d->dst, m_d->boundingRect, helper.updater()); Q_FOREACH (const KeyStroke &stroke, m_d->keyStrokes) { KoColor color = !stroke.isTransparent ? stroke.color : KoColor(Qt::transparent, m_d->dst->colorSpace()); worker.addKeyStroke(stroke.dev, color); } worker.run(m_d->filteringOptions.cleanUpAmount); }); } addJobSequential(jobs, [this] () { emit sigFinished(m_d->prefilterOnly); }); runnableJobsInterface()->addRunnableJobs(jobs); } void KisColorizeStrokeStrategy::cancelStrokeCallback() { emit sigCancelled(); } KisStrokeStrategy* KisColorizeStrokeStrategy::createLodClone(int levelOfDetail) { KisImageConfig cfg(true); if (!cfg.useLodForColorizeMask()) return 0; KisColorizeStrokeStrategy *clone = new KisColorizeStrokeStrategy(*this, levelOfDetail); return clone; } diff --git a/libs/image/tests/kis_image_test.cpp b/libs/image/tests/kis_image_test.cpp index d9145a3696..e6e70feb8b 100644 --- a/libs/image/tests/kis_image_test.cpp +++ b/libs/image/tests/kis_image_test.cpp @@ -1,1287 +1,1293 @@ /* * Copyright (c) 2005 Adrian Page * * 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_image_test.h" #include #include #include #include #include "filter/kis_filter.h" #include "filter/kis_filter_configuration.h" #include "filter/kis_filter_registry.h" #include "kis_image.h" #include "kis_paint_layer.h" #include "kis_group_layer.h" #include "kis_adjustment_layer.h" #include "kis_selection.h" #include #include #include "kis_keyframe_channel.h" #include "kis_selection_mask.h" #include "kis_layer_utils.h" #include "kis_annotation.h" #include "KisProofingConfiguration.h" #include "kis_undo_stores.h" #define IMAGE_WIDTH 128 #define IMAGE_HEIGHT 128 void KisImageTest::layerTests() { KisImageSP image = new KisImage(0, IMAGE_WIDTH, IMAGE_WIDTH, 0, "layer tests"); QVERIFY(image->rootLayer() != 0); QVERIFY(image->rootLayer()->firstChild() == 0); KisLayerSP layer = new KisPaintLayer(image, "layer 1", OPACITY_OPAQUE_U8); image->addNode(layer); QVERIFY(image->rootLayer()->firstChild()->objectName() == layer->objectName()); } void KisImageTest::benchmarkCreation() { const QRect imageRect(0,0,3000,2000); const KoColorSpace * cs = KoColorSpaceRegistry::instance()->rgb8(); QList images; QList stores; QBENCHMARK { for (int i = 0; i < 10; i++) { stores << new KisSurrogateUndoStore(); } for (int i = 0; i < 10; i++) { KisImageSP image = new KisImage(stores.takeLast(), imageRect.width(), imageRect.height(), cs, "test image"); images << image; } } } #include "testutil.h" #include "kis_stroke_strategy.h" #include class ForbiddenLodStrokeStrategy : public KisStrokeStrategy { public: ForbiddenLodStrokeStrategy(std::function lodCallback) - : m_lodCallback(lodCallback) + : KisStrokeStrategy(QLatin1String("ForbiddenLodStrokeStrategy")), + m_lodCallback(lodCallback) { } KisStrokeStrategy* createLodClone(int levelOfDetail) override { Q_UNUSED(levelOfDetail); m_lodCallback(); return 0; } private: std::function m_lodCallback; }; void notifyVar(bool *value) { *value = true; } void KisImageTest::testBlockLevelOfDetail() { TestUtil::MaskParent p; QCOMPARE(p.image->currentLevelOfDetail(), 0); p.image->setDesiredLevelOfDetail(1); p.image->waitForDone(); QCOMPARE(p.image->currentLevelOfDetail(), 0); { bool lodCreated = false; KisStrokeId id = p.image->startStroke( new ForbiddenLodStrokeStrategy( std::bind(¬ifyVar, &lodCreated))); p.image->endStroke(id); p.image->waitForDone(); QVERIFY(lodCreated); } p.image->setLevelOfDetailBlocked(true); { bool lodCreated = false; KisStrokeId id = p.image->startStroke( new ForbiddenLodStrokeStrategy( std::bind(¬ifyVar, &lodCreated))); p.image->endStroke(id); p.image->waitForDone(); QVERIFY(!lodCreated); } p.image->setLevelOfDetailBlocked(false); p.image->setDesiredLevelOfDetail(1); { bool lodCreated = false; KisStrokeId id = p.image->startStroke( new ForbiddenLodStrokeStrategy( std::bind(¬ifyVar, &lodCreated))); p.image->endStroke(id); p.image->waitForDone(); QVERIFY(lodCreated); } } void KisImageTest::testConvertImageColorSpace() { const KoColorSpace *cs8 = KoColorSpaceRegistry::instance()->rgb8(); KisImageSP image = new KisImage(0, 1000, 1000, cs8, "stest"); KisPaintDeviceSP device1 = new KisPaintDevice(cs8); KisLayerSP paint1 = new KisPaintLayer(image, "paint1", OPACITY_OPAQUE_U8, device1); KisFilterSP filter = KisFilterRegistry::instance()->value("blur"); Q_ASSERT(filter); KisFilterConfigurationSP configuration = filter->defaultConfiguration(); Q_ASSERT(configuration); KisLayerSP blur1 = new KisAdjustmentLayer(image, "blur1", configuration, 0); image->addNode(paint1, image->root()); image->addNode(blur1, image->root()); image->refreshGraph(); const KoColorSpace *cs16 = KoColorSpaceRegistry::instance()->rgb16(); image->convertImageColorSpace(cs16, KoColorConversionTransformation::internalRenderingIntent(), KoColorConversionTransformation::internalConversionFlags()); image->waitForDone(); QVERIFY(*cs16 == *image->colorSpace()); QVERIFY(*cs16 == *image->root()->colorSpace()); QVERIFY(*cs16 == *paint1->colorSpace()); QVERIFY(*cs16 == *blur1->colorSpace()); QVERIFY(!image->root()->compositeOp()); QVERIFY(*cs16 == *paint1->compositeOp()->colorSpace()); QVERIFY(*cs16 == *blur1->compositeOp()->colorSpace()); image->refreshGraph(); } void KisImageTest::testAssignImageProfile() { const KoColorSpace *rgb8 = KoColorSpaceRegistry::instance()->rgb8(); const KoColorSpace *gray8 = KoColorSpaceRegistry::instance()->graya8(); KisImageSP image = new KisImage(0, 1000, 1000, rgb8, "stest"); KisPaintDeviceSP device1 = new KisPaintDevice(rgb8); KisLayerSP paint1 = new KisPaintLayer(image, "paint1", OPACITY_OPAQUE_U8, device1); KisPaintDeviceSP device2 = new KisPaintDevice(gray8); KisLayerSP paint2 = new KisPaintLayer(image, "paint2", OPACITY_OPAQUE_U8, device2); KisFilterSP filter = KisFilterRegistry::instance()->value("blur"); Q_ASSERT(filter); KisFilterConfigurationSP configuration = filter->defaultConfiguration(); Q_ASSERT(configuration); KisLayerSP blur1 = new KisAdjustmentLayer(image, "blur1", configuration, 0); image->addNode(paint1, image->root()); image->addNode(paint2, image->root()); image->addNode(blur1, image->root()); QCOMPARE(*image->colorSpace(), *rgb8); QCOMPARE(*image->colorSpace()->profile(), *KoColorSpaceRegistry::instance()->p709SRGBProfile()); QCOMPARE(*paint1->colorSpace(), *rgb8); QCOMPARE(*paint1->colorSpace()->profile(), *KoColorSpaceRegistry::instance()->p709SRGBProfile()); QCOMPARE(*paint2->colorSpace(), *gray8); QCOMPARE(*blur1->colorSpace(), *rgb8); QCOMPARE(*blur1->colorSpace()->profile(), *KoColorSpaceRegistry::instance()->p709SRGBProfile()); image->assignImageProfile(KoColorSpaceRegistry::instance()->p2020G10Profile()); image->waitForDone(); QVERIFY(*image->colorSpace() != *rgb8); QCOMPARE(*image->colorSpace()->profile(), *KoColorSpaceRegistry::instance()->p2020G10Profile()); QVERIFY(*paint1->colorSpace() != *rgb8); QCOMPARE(*paint1->colorSpace()->profile(), *KoColorSpaceRegistry::instance()->p2020G10Profile()); QCOMPARE(*paint2->colorSpace(), *gray8); QVERIFY(*blur1->colorSpace() != *rgb8); QCOMPARE(*blur1->colorSpace()->profile(), *KoColorSpaceRegistry::instance()->p2020G10Profile()); } void KisImageTest::testGlobalSelection() { const KoColorSpace *cs8 = KoColorSpaceRegistry::instance()->rgb8(); KisImageSP image = new KisImage(0, 1000, 1000, cs8, "stest"); QCOMPARE(image->globalSelection(), KisSelectionSP(0)); QCOMPARE(image->canReselectGlobalSelection(), false); QCOMPARE(image->root()->childCount(), 0U); KisSelectionSP selection1 = new KisSelection(new KisDefaultBounds(image)); KisSelectionSP selection2 = new KisSelection(new KisDefaultBounds(image)); image->setGlobalSelection(selection1); QCOMPARE(image->globalSelection(), selection1); QCOMPARE(image->canReselectGlobalSelection(), false); QCOMPARE(image->root()->childCount(), 1U); image->setGlobalSelection(selection2); QCOMPARE(image->globalSelection(), selection2); QCOMPARE(image->canReselectGlobalSelection(), false); QCOMPARE(image->root()->childCount(), 1U); image->deselectGlobalSelection(); QCOMPARE(image->globalSelection(), KisSelectionSP(0)); QCOMPARE(image->canReselectGlobalSelection(), true); QCOMPARE(image->root()->childCount(), 0U); image->reselectGlobalSelection(); QCOMPARE(image->globalSelection(), selection2); QCOMPARE(image->canReselectGlobalSelection(), false); QCOMPARE(image->root()->childCount(), 1U); // mixed deselecting/setting/reselecting image->deselectGlobalSelection(); QCOMPARE(image->globalSelection(), KisSelectionSP(0)); QCOMPARE(image->canReselectGlobalSelection(), true); QCOMPARE(image->root()->childCount(), 0U); image->setGlobalSelection(selection1); QCOMPARE(image->globalSelection(), selection1); QCOMPARE(image->canReselectGlobalSelection(), false); QCOMPARE(image->root()->childCount(), 1U); } void KisImageTest::testCloneImage() { KisImageSP image = new KisImage(0, IMAGE_WIDTH, IMAGE_WIDTH, 0, "layer tests"); QVERIFY(image->rootLayer() != 0); QVERIFY(image->rootLayer()->firstChild() == 0); KisAnnotationSP annotation = new KisAnnotation("mytype", "mydescription", QByteArray()); image->addAnnotation(annotation); QVERIFY(image->annotation("mytype")); KisProofingConfigurationSP proofing = toQShared(new KisProofingConfiguration()); image->setProofingConfiguration(proofing); QVERIFY(image->proofingConfiguration()); const KoColor defaultColor(Qt::green, image->colorSpace()); image->setDefaultProjectionColor(defaultColor); QCOMPARE(image->defaultProjectionColor(), defaultColor); KisLayerSP layer = new KisPaintLayer(image, "layer1", OPACITY_OPAQUE_U8); image->addNode(layer); KisLayerSP layer2 = new KisPaintLayer(image, "layer2", OPACITY_OPAQUE_U8); image->addNode(layer2); QVERIFY(layer->visible()); QVERIFY(layer2->visible()); QVERIFY(TestUtil::findNode(image->root(), "layer1")); QVERIFY(TestUtil::findNode(image->root(), "layer2")); QUuid uuid1 = layer->uuid(); QUuid uuid2 = layer2->uuid(); { KisImageSP newImage = image->clone(); KisNodeSP newLayer1 = TestUtil::findNode(newImage->root(), "layer1"); KisNodeSP newLayer2 = TestUtil::findNode(newImage->root(), "layer2"); QVERIFY(newLayer1); QVERIFY(newLayer2); QVERIFY(newLayer1->uuid() != uuid1); QVERIFY(newLayer2->uuid() != uuid2); KisAnnotationSP newAnnotation = newImage->annotation("mytype"); QVERIFY(newAnnotation); QVERIFY(newAnnotation != annotation); KisProofingConfigurationSP newProofing = newImage->proofingConfiguration(); QVERIFY(newProofing); QVERIFY(newProofing != proofing); QCOMPARE(newImage->defaultProjectionColor(), defaultColor); } { KisImageSP newImage = image->clone(true); KisNodeSP newLayer1 = TestUtil::findNode(newImage->root(), "layer1"); KisNodeSP newLayer2 = TestUtil::findNode(newImage->root(), "layer2"); QVERIFY(newLayer1); QVERIFY(newLayer2); QVERIFY(newLayer1->uuid() == uuid1); QVERIFY(newLayer2->uuid() == uuid2); } } void KisImageTest::testLayerComposition() { KisImageSP image = new KisImage(0, IMAGE_WIDTH, IMAGE_WIDTH, 0, "layer tests"); QVERIFY(image->rootLayer() != 0); QVERIFY(image->rootLayer()->firstChild() == 0); KisLayerSP layer = new KisPaintLayer(image, "layer1", OPACITY_OPAQUE_U8); image->addNode(layer); KisLayerSP layer2 = new KisPaintLayer(image, "layer2", OPACITY_OPAQUE_U8); image->addNode(layer2); QVERIFY(layer->visible()); QVERIFY(layer2->visible()); KisLayerComposition comp(image, "comp 1"); comp.store(); layer2->setVisible(false); QVERIFY(layer->visible()); QVERIFY(!layer2->visible()); KisLayerComposition comp2(image, "comp 2"); comp2.store(); KisLayerCompositionSP comp3 = toQShared(new KisLayerComposition(image, "comp 3")); comp3->store(); image->addComposition(comp3); comp.apply(); QVERIFY(layer->visible()); QVERIFY(layer2->visible()); comp2.apply(); QVERIFY(layer->visible()); QVERIFY(!layer2->visible()); comp.apply(); QVERIFY(layer->visible()); QVERIFY(layer2->visible()); KisImageSP newImage = image->clone(); KisNodeSP newLayer1 = TestUtil::findNode(newImage->root(), "layer1"); KisNodeSP newLayer2 = TestUtil::findNode(newImage->root(), "layer2"); QVERIFY(newLayer1); QVERIFY(newLayer2); QVERIFY(newLayer1->visible()); QVERIFY(newLayer2->visible()); KisLayerComposition newComp1(comp, newImage); newComp1.apply(); QVERIFY(newLayer1->visible()); QVERIFY(newLayer2->visible()); KisLayerComposition newComp2(comp2, newImage); newComp2.apply(); QVERIFY(newLayer1->visible()); QVERIFY(!newLayer2->visible()); newComp1.apply(); QVERIFY(newLayer1->visible()); QVERIFY(newLayer2->visible()); QVERIFY(!newImage->compositions().isEmpty()); KisLayerCompositionSP newComp3 = newImage->compositions().first(); newComp3->apply(); QVERIFY(newLayer1->visible()); QVERIFY(!newLayer2->visible()); } #include "kis_transparency_mask.h" #include "kis_psd_layer_style.h" struct FlattenTestImage { FlattenTestImage() : refRect(0,0,512,512) , p(refRect) { image = p.image; undoStore = p.undoStore; layer1 = p.layer; layer5 = new KisPaintLayer(p.image, "paint5", 0.4 * OPACITY_OPAQUE_U8); layer5->disableAlphaChannel(true); layer2 = new KisPaintLayer(p.image, "paint2", OPACITY_OPAQUE_U8); tmask = new KisTransparencyMask(); // check channel flags // make addition composite op group1 = new KisGroupLayer(p.image, "group1", OPACITY_OPAQUE_U8); layer3 = new KisPaintLayer(p.image, "paint3", OPACITY_OPAQUE_U8); layer4 = new KisPaintLayer(p.image, "paint4", OPACITY_OPAQUE_U8); layer6 = new KisPaintLayer(p.image, "paint6", OPACITY_OPAQUE_U8); layer7 = new KisPaintLayer(p.image, "paint7", OPACITY_OPAQUE_U8); layer8 = new KisPaintLayer(p.image, "paint8", OPACITY_OPAQUE_U8); layer7->setCompositeOpId(COMPOSITE_ADD); layer8->setCompositeOpId(COMPOSITE_ADD); QRect rect1(100, 100, 100, 100); QRect rect2(150, 150, 150, 150); QRect tmaskRect(200,200,100,100); QRect rect3(400, 100, 100, 100); QRect rect4(500, 100, 100, 100); QRect rect5(50, 50, 100, 100); QRect rect6(50, 250, 100, 100); QRect rect7(50, 350, 50, 50); QRect rect8(50, 400, 50, 50); layer1->paintDevice()->fill(rect1, KoColor(Qt::red, p.image->colorSpace())); layer2->paintDevice()->fill(rect2, KoColor(Qt::green, p.image->colorSpace())); tmask->testingInitSelection(tmaskRect, layer2); layer3->paintDevice()->fill(rect3, KoColor(Qt::blue, p.image->colorSpace())); layer4->paintDevice()->fill(rect4, KoColor(Qt::yellow, p.image->colorSpace())); layer5->paintDevice()->fill(rect5, KoColor(Qt::green, p.image->colorSpace())); layer6->paintDevice()->fill(rect6, KoColor(Qt::cyan, p.image->colorSpace())); layer7->paintDevice()->fill(rect7, KoColor(Qt::red, p.image->colorSpace())); layer8->paintDevice()->fill(rect8, KoColor(Qt::green, p.image->colorSpace())); KisPSDLayerStyleSP style(new KisPSDLayerStyle()); style->dropShadow()->setEffectEnabled(true); style->dropShadow()->setDistance(10.0); style->dropShadow()->setSpread(80.0); style->dropShadow()->setSize(10); style->dropShadow()->setNoise(0); style->dropShadow()->setKnocksOut(false); style->dropShadow()->setOpacity(80.0); layer2->setLayerStyle(style); layer2->setCompositeOpId(COMPOSITE_ADD); group1->setCompositeOpId(COMPOSITE_ADD); p.image->addNode(layer5); p.image->addNode(layer2); p.image->addNode(tmask, layer2); p.image->addNode(group1); p.image->addNode(layer3, group1); p.image->addNode(layer4, group1); p.image->addNode(layer6); p.image->addNode(layer7); p.image->addNode(layer8); p.image->initialRefreshGraph(); // dbgKrita << ppVar(layer1->exactBounds()); // dbgKrita << ppVar(layer5->exactBounds()); // dbgKrita << ppVar(layer2->exactBounds()); // dbgKrita << ppVar(group1->exactBounds()); // dbgKrita << ppVar(layer3->exactBounds()); // dbgKrita << ppVar(layer4->exactBounds()); TestUtil::ReferenceImageChecker chk("flatten", "imagetest"); QVERIFY(chk.checkDevice(p.image->projection(), p.image, "00_initial")); } QRect refRect; TestUtil::MaskParent p; KisImageSP image; KisSurrogateUndoStore *undoStore; KisPaintLayerSP layer1; KisPaintLayerSP layer2; KisTransparencyMaskSP tmask; KisGroupLayerSP group1; KisPaintLayerSP layer3; KisPaintLayerSP layer4; KisPaintLayerSP layer5; KisPaintLayerSP layer6; KisPaintLayerSP layer7; KisPaintLayerSP layer8; }; template KisLayerSP flattenLayerHelper(ContainerTest &p, KisLayerSP layer, bool nothingHappens = false) { QSignalSpy spy(p.image.data(), SIGNAL(sigNodeAddedAsync(KisNodeSP))); //p.image->flattenLayer(layer); KisLayerUtils::flattenLayer(p.image, layer); p.image->waitForDone(); if (nothingHappens) { Q_ASSERT(!spy.count()); return layer; } Q_ASSERT(spy.count() == 1); QList arguments = spy.takeFirst(); KisNodeSP newNode = arguments.first().value(); KisLayerSP newLayer = qobject_cast(newNode.data()); return newLayer; } void KisImageTest::testFlattenLayer() { FlattenTestImage p; TestUtil::ReferenceImageChecker chk("flatten", "imagetest"); { QCOMPARE(p.layer2->compositeOpId(), COMPOSITE_ADD); KisLayerSP newLayer = flattenLayerHelper(p, p.layer2); //KisLayerSP newLayer = p.image->flattenLayer(p.layer2); //p.image->waitForDone(); QVERIFY(chk.checkDevice(p.image->projection(), p.image, "00_initial")); QVERIFY(chk.checkDevice(newLayer->projection(), p.image, "01_layer2_layerproj")); QCOMPARE(newLayer->compositeOpId(), COMPOSITE_OVER); } { QCOMPARE(p.group1->compositeOpId(), COMPOSITE_ADD); KisLayerSP newLayer = flattenLayerHelper(p, p.group1); //KisLayerSP newLayer = p.image->flattenLayer(p.group1); //p.image->waitForDone(); QVERIFY(chk.checkDevice(p.image->projection(), p.image, "00_initial")); QVERIFY(chk.checkDevice(newLayer->projection(), p.image, "02_group1_layerproj")); QCOMPARE(newLayer->compositeOpId(), COMPOSITE_ADD); QCOMPARE(newLayer->exactBounds(), QRect(400, 100, 200, 100)); } { QCOMPARE(p.layer5->compositeOpId(), COMPOSITE_OVER); QCOMPARE(p.layer5->alphaChannelDisabled(), true); KisLayerSP newLayer = flattenLayerHelper(p, p.layer5, true); //KisLayerSP newLayer = p.image->flattenLayer(p.layer5); //p.image->waitForDone(); QVERIFY(chk.checkDevice(p.image->projection(), p.image, "00_initial")); QVERIFY(chk.checkDevice(newLayer->projection(), p.image, "03_layer5_layerproj")); QCOMPARE(newLayer->compositeOpId(), COMPOSITE_OVER); QCOMPARE(newLayer->exactBounds(), QRect(50, 50, 100, 100)); QCOMPARE(newLayer->alphaChannelDisabled(), true); } } #include template KisLayerSP mergeHelper(ContainerTest &p, KisLayerSP layer) { KisNodeSP parent = layer->parent(); const int newIndex = parent->index(layer) - 1; p.image->mergeDown(layer, KisMetaData::MergeStrategyRegistry::instance()->get("Drop")); //KisLayerUtils::mergeDown(p.image, layer, KisMetaData::MergeStrategyRegistry::instance()->get("Drop")); p.image->waitForDone(); KisLayerSP newLayer = qobject_cast(parent->at(newIndex).data()); return newLayer; } void KisImageTest::testMergeDown() { FlattenTestImage p; TestUtil::ReferenceImageChecker img("flatten", "imagetest"); TestUtil::ReferenceImageChecker chk("mergedown_simple", "imagetest"); { QCOMPARE(p.layer5->compositeOpId(), COMPOSITE_OVER); QCOMPARE(p.layer5->alphaChannelDisabled(), true); KisLayerSP newLayer = mergeHelper(p, p.layer5); QVERIFY(img.checkDevice(p.image->projection(), p.image, "00_initial")); QVERIFY(chk.checkDevice(newLayer->projection(), p.image, "01_layer5_layerproj")); QCOMPARE(newLayer->compositeOpId(), COMPOSITE_OVER); QCOMPARE(newLayer->alphaChannelDisabled(), false); } { QCOMPARE(p.layer2->compositeOpId(), COMPOSITE_ADD); QCOMPARE(p.layer2->alphaChannelDisabled(), false); KisLayerSP newLayer = mergeHelper(p, p.layer2); QVERIFY(img.checkDevice(p.image->projection(), p.image, "00_initial")); QVERIFY(chk.checkDevice(newLayer->projection(), p.image, "02_layer2_layerproj")); QCOMPARE(newLayer->compositeOpId(), COMPOSITE_OVER); QCOMPARE(newLayer->exactBounds(), QRect(100, 100, 213, 217)); QCOMPARE(newLayer->alphaChannelDisabled(), false); } { QCOMPARE(p.group1->compositeOpId(), COMPOSITE_ADD); QCOMPARE(p.group1->alphaChannelDisabled(), false); KisLayerSP newLayer = mergeHelper(p, p.group1); QVERIFY(img.checkDevice(p.image->projection(), p.image, "00_initial")); QVERIFY(chk.checkDevice(newLayer->projection(), p.image, "03_group1_mergedown_layerproj")); QCOMPARE(newLayer->compositeOpId(), COMPOSITE_OVER); QCOMPARE(newLayer->exactBounds(), QRect(100, 100, 500, 217)); QCOMPARE(newLayer->alphaChannelDisabled(), false); } } void KisImageTest::testMergeDownDestinationInheritsAlpha() { FlattenTestImage p; TestUtil::ReferenceImageChecker img("flatten", "imagetest"); TestUtil::ReferenceImageChecker chk("mergedown_dst_inheritsalpha", "imagetest"); { QCOMPARE(p.layer2->compositeOpId(), COMPOSITE_ADD); QCOMPARE(p.layer2->alphaChannelDisabled(), false); KisLayerSP newLayer = mergeHelper(p, p.layer2); // WARN: this check is suspicious! QVERIFY(img.checkDevice(p.image->projection(), p.image, "00_proj_merged_layer2_over_layer5_IA")); QVERIFY(chk.checkDevice(newLayer->projection(), p.image, "01_layer2_layerproj")); QCOMPARE(newLayer->compositeOpId(), COMPOSITE_OVER); QCOMPARE(newLayer->exactBounds(), QRect(50,50, 263, 267)); QCOMPARE(newLayer->alphaChannelDisabled(), false); } } void KisImageTest::testMergeDownDestinationCustomCompositeOp() { FlattenTestImage p; TestUtil::ReferenceImageChecker img("flatten", "imagetest"); TestUtil::ReferenceImageChecker chk("mergedown_dst_customop", "imagetest"); { QCOMPARE(p.layer6->compositeOpId(), COMPOSITE_OVER); QCOMPARE(p.layer6->alphaChannelDisabled(), false); QCOMPARE(p.group1->compositeOpId(), COMPOSITE_ADD); QCOMPARE(p.group1->alphaChannelDisabled(), false); KisLayerSP newLayer = mergeHelper(p, p.layer6); QVERIFY(img.checkDevice(p.image->projection(), p.image, "00_initial")); QVERIFY(chk.checkDevice(newLayer->projection(), p.image, "01_layer6_layerproj")); QCOMPARE(newLayer->compositeOpId(), COMPOSITE_OVER); QCOMPARE(newLayer->exactBounds(), QRect(50, 100, 550, 250)); QCOMPARE(newLayer->alphaChannelDisabled(), false); } } void KisImageTest::testMergeDownDestinationSameCompositeOpLayerStyle() { FlattenTestImage p; TestUtil::ReferenceImageChecker img("flatten", "imagetest"); TestUtil::ReferenceImageChecker chk("mergedown_sameop_ls", "imagetest"); { QCOMPARE(p.group1->compositeOpId(), COMPOSITE_ADD); QCOMPARE(p.group1->alphaChannelDisabled(), false); QCOMPARE(p.layer2->compositeOpId(), COMPOSITE_ADD); QCOMPARE(p.layer2->alphaChannelDisabled(), false); KisLayerSP newLayer = mergeHelper(p, p.group1); QVERIFY(img.checkDevice(p.image->projection(), p.image, "00_initial")); QVERIFY(chk.checkDevice(newLayer->projection(), p.image, "01_group1_layerproj")); QCOMPARE(newLayer->compositeOpId(), COMPOSITE_OVER); QCOMPARE(newLayer->exactBounds(), QRect(197, 100, 403, 217)); QCOMPARE(newLayer->alphaChannelDisabled(), false); } } void KisImageTest::testMergeDownDestinationSameCompositeOp() { FlattenTestImage p; TestUtil::ReferenceImageChecker img("flatten", "imagetest"); TestUtil::ReferenceImageChecker chk("mergedown_sameop_fastpath", "imagetest"); { QCOMPARE(p.layer8->compositeOpId(), COMPOSITE_ADD); QCOMPARE(p.layer8->alphaChannelDisabled(), false); QCOMPARE(p.layer7->compositeOpId(), COMPOSITE_ADD); QCOMPARE(p.layer7->alphaChannelDisabled(), false); KisLayerSP newLayer = mergeHelper(p, p.layer8); QVERIFY(img.checkDevice(p.image->projection(), p.image, "00_initial")); QVERIFY(chk.checkDevice(newLayer->projection(), p.image, "01_layer8_layerproj")); QCOMPARE(newLayer->compositeOpId(), COMPOSITE_ADD); QCOMPARE(newLayer->exactBounds(), QRect(50, 350, 50, 100)); QCOMPARE(newLayer->alphaChannelDisabled(), false); } } #include "kis_image_animation_interface.h" void KisImageTest::testMergeDownMultipleFrames() { FlattenTestImage p; TestUtil::ReferenceImageChecker img("flatten", "imagetest"); TestUtil::ReferenceImageChecker chk("mergedown_simple", "imagetest"); QSet initialFrames; { KisLayerSP l = p.layer5; l->enableAnimation(); KisKeyframeChannel *channel = l->getKeyframeChannel(KisKeyframeChannel::Content.id(), true); channel->addKeyframe(10); channel->addKeyframe(20); channel->addKeyframe(30); QCOMPARE(channel->keyframeCount(), 4); initialFrames = KisLayerUtils::fetchLayerFramesRecursive(l); QCOMPARE(initialFrames.size(), 4); } { QCOMPARE(p.layer5->compositeOpId(), COMPOSITE_OVER); QCOMPARE(p.layer5->alphaChannelDisabled(), true); KisLayerSP newLayer = mergeHelper(p, p.layer5); QVERIFY(img.checkDevice(p.image->projection(), p.image, "00_initial")); QVERIFY(chk.checkDevice(newLayer->projection(), p.image, "01_layer5_layerproj")); QCOMPARE(newLayer->compositeOpId(), COMPOSITE_OVER); QCOMPARE(newLayer->alphaChannelDisabled(), false); QVERIFY(newLayer->isAnimated()); QSet newFrames = KisLayerUtils::fetchLayerFramesRecursive(newLayer); QCOMPARE(newFrames, initialFrames); foreach (int frame, newFrames) { KisImageAnimationInterface *interface = p.image->animationInterface(); int savedSwitchedTime = 0; interface->saveAndResetCurrentTime(frame, &savedSwitchedTime); QCOMPARE(newLayer->exactBounds(), QRect(100,100,100,100)); interface->restoreCurrentTime(&savedSwitchedTime); } p.undoStore->undo(); p.image->waitForDone(); QVERIFY(img.checkDevice(p.image->projection(), p.image, "00_initial")); } } template KisNodeSP mergeMultipleHelper(ContainerTest &p, QList selectedNodes, KisNodeSP putAfter) { QSignalSpy spy(p.image.data(), SIGNAL(sigNodeAddedAsync(KisNodeSP))); p.image->mergeMultipleLayers(selectedNodes, putAfter); //KisLayerUtils::mergeMultipleLayers(p.image, selectedNodes, putAfter); p.image->waitForDone(); Q_ASSERT(spy.count() == 1); QList arguments = spy.takeFirst(); KisNodeSP newNode = arguments.first().value(); return newNode; } void KisImageTest::testMergeMultiple() { FlattenTestImage p; TestUtil::ReferenceImageChecker img("flatten", "imagetest"); TestUtil::ReferenceImageChecker chk("mergemultiple", "imagetest"); { QList selectedNodes; selectedNodes << p.layer2 << p.group1 << p.layer6; { KisNodeSP newLayer = mergeMultipleHelper(p, selectedNodes, 0); //KisNodeSP newLayer = p.image->mergeMultipleLayers(selectedNodes, 0); //p.image->waitForDone(); QVERIFY(img.checkDevice(p.image->projection(), p.image, "00_initial")); QVERIFY(chk.checkDevice(newLayer->projection(), p.image, "01_layer8_layerproj")); QCOMPARE(newLayer->compositeOpId(), COMPOSITE_OVER); QCOMPARE(newLayer->exactBounds(), QRect(50, 100, 550, 250)); } } p.p.undoStore->undo(); p.image->waitForDone(); // Test reversed order, the result must be the same { QList selectedNodes; selectedNodes << p.layer6 << p.group1 << p.layer2; { KisNodeSP newLayer = mergeMultipleHelper(p, selectedNodes, 0); //KisNodeSP newLayer = p.image->mergeMultipleLayers(selectedNodes, 0); //p.image->waitForDone(); QVERIFY(img.checkDevice(p.image->projection(), p.image, "00_initial")); QVERIFY(chk.checkDevice(newLayer->projection(), p.image, "01_layer8_layerproj")); QCOMPARE(newLayer->compositeOpId(), COMPOSITE_OVER); QCOMPARE(newLayer->exactBounds(), QRect(50, 100, 550, 250)); } } } void testMergeCrossColorSpaceImpl(bool useProjectionColorSpace, bool swapSpaces) { TestUtil::MaskParent p; KisPaintLayerSP layer1; KisPaintLayerSP layer2; KisPaintLayerSP layer3; const KoColorSpace *cs2 = useProjectionColorSpace ? p.image->colorSpace() : KoColorSpaceRegistry::instance()->lab16(); const KoColorSpace *cs3 = KoColorSpaceRegistry::instance()->rgb16(); if (swapSpaces) { std::swap(cs2, cs3); } dbgKrita << "Testing testMergeCrossColorSpaceImpl:"; dbgKrita << " " << ppVar(cs2); dbgKrita << " " << ppVar(cs3); layer1 = p.layer; layer2 = new KisPaintLayer(p.image, "paint2", OPACITY_OPAQUE_U8, cs2); layer3 = new KisPaintLayer(p.image, "paint3", OPACITY_OPAQUE_U8, cs3); QRect rect1(100, 100, 100, 100); QRect rect2(150, 150, 150, 150); QRect rect3(250, 250, 200, 200); layer1->paintDevice()->fill(rect1, KoColor(Qt::red, layer1->colorSpace())); layer2->paintDevice()->fill(rect2, KoColor(Qt::green, layer2->colorSpace())); layer3->paintDevice()->fill(rect3, KoColor(Qt::blue, layer3->colorSpace())); p.image->addNode(layer2); p.image->addNode(layer3); p.image->initialRefreshGraph(); { KisLayerSP newLayer = mergeHelper(p, layer3); QCOMPARE(newLayer->colorSpace(), p.image->colorSpace()); p.undoStore->undo(); p.image->waitForDone(); } { layer2->disableAlphaChannel(true); KisLayerSP newLayer = mergeHelper(p, layer3); QCOMPARE(newLayer->colorSpace(), p.image->colorSpace()); p.undoStore->undo(); p.image->waitForDone(); } } void KisImageTest::testMergeCrossColorSpace() { testMergeCrossColorSpaceImpl(true, false); testMergeCrossColorSpaceImpl(true, true); testMergeCrossColorSpaceImpl(false, false); testMergeCrossColorSpaceImpl(false, true); } void KisImageTest::testMergeSelectionMasks() { TestUtil::MaskParent p; QRect rect1(100, 100, 100, 100); QRect rect2(150, 150, 150, 150); QRect rect3(50, 50, 100, 100); KisPaintLayerSP layer1 = p.layer; layer1->paintDevice()->fill(rect1, KoColor(Qt::red, layer1->colorSpace())); p.image->initialRefreshGraph(); KisSelectionSP sel = new KisSelection(layer1->paintDevice()->defaultBounds()); sel->pixelSelection()->select(rect2, MAX_SELECTED); KisSelectionMaskSP mask1 = new KisSelectionMask(p.image); mask1->initSelection(sel, layer1); p.image->addNode(mask1, layer1); QVERIFY(!layer1->selection()); mask1->setActive(true); QCOMPARE(layer1->selection()->selectedExactRect(), QRect(150,150,150,150)); sel->pixelSelection()->select(rect3, MAX_SELECTED); KisSelectionMaskSP mask2 = new KisSelectionMask(p.image); mask2->initSelection(sel, layer1); p.image->addNode(mask2, layer1); QCOMPARE(layer1->selection()->selectedExactRect(), QRect(150,150,150,150)); mask2->setActive(true); QCOMPARE(layer1->selection()->selectedExactRect(), QRect(50,50,250,250)); QList selectedNodes; selectedNodes << mask2 << mask1; { KisNodeSP newLayer = mergeMultipleHelper(p, selectedNodes, 0); QCOMPARE(newLayer->parent(), KisNodeSP(layer1)); QCOMPARE((int)layer1->childCount(), 1); QCOMPARE(layer1->selection()->selectedExactRect(), QRect(50,50,250,250)); } } void KisImageTest::testFlattenImage() { FlattenTestImage p; KisImageSP image = p.image; TestUtil::ReferenceImageChecker img("flatten", "imagetest"); { KisLayerUtils::flattenImage(p.image, 0); p.image->waitForDone(); QVERIFY(img.checkDevice(p.image->projection(), p.image, "00_initial")); p.undoStore->undo(); p.image->waitForDone(); QVERIFY(img.checkDevice(p.image->projection(), p.image, "00_initial")); } { KisLayerUtils::flattenImage(p.image, p.layer5); // flatten with active layer just under the root (not inside any group) p.image->waitForDone(); QVERIFY(img.checkDevice(p.image->projection(), p.image, "00_initial")); p.undoStore->undo(); p.image->waitForDone(); QVERIFY(img.checkDevice(p.image->projection(), p.image, "00_initial")); } { KisLayerUtils::flattenImage(p.image, p.layer2); // flatten with active layer just under the root (not inside any group), but with a mask p.image->waitForDone(); QVERIFY(img.checkDevice(p.image->projection(), p.image, "00_initial")); p.undoStore->undo(); p.image->waitForDone(); QVERIFY(img.checkDevice(p.image->projection(), p.image, "00_initial")); } { KisLayerUtils::flattenImage(p.image, p.layer3); // flatten with active layer inside of a group p.image->waitForDone(); QVERIFY(img.checkDevice(p.image->projection(), p.image, "00_initial")); p.undoStore->undo(); p.image->waitForDone(); QVERIFY(img.checkDevice(p.image->projection(), p.image, "00_initial")); } } struct FlattenPassThroughTestImage { FlattenPassThroughTestImage() : refRect(0,0,512,512) , p(refRect) { image = p.image; undoStore = p.undoStore; group1 = new KisGroupLayer(p.image, "group1", OPACITY_OPAQUE_U8); layer2 = new KisPaintLayer(p.image, "paint2", OPACITY_OPAQUE_U8); layer3 = new KisPaintLayer(p.image, "paint3", OPACITY_OPAQUE_U8); group4 = new KisGroupLayer(p.image, "group4", OPACITY_OPAQUE_U8); layer5 = new KisPaintLayer(p.image, "paint5", OPACITY_OPAQUE_U8); layer6 = new KisPaintLayer(p.image, "paint6", OPACITY_OPAQUE_U8); QRect rect2(100, 100, 100, 100); QRect rect3(150, 150, 100, 100); QRect rect5(200, 200, 100, 100); QRect rect6(250, 250, 100, 100); group1->setPassThroughMode(true); layer2->paintDevice()->fill(rect2, KoColor(Qt::red, p.image->colorSpace())); layer3->paintDevice()->fill(rect3, KoColor(Qt::green, p.image->colorSpace())); group4->setPassThroughMode(true); layer5->paintDevice()->fill(rect5, KoColor(Qt::blue, p.image->colorSpace())); layer6->paintDevice()->fill(rect6, KoColor(Qt::yellow, p.image->colorSpace())); p.image->addNode(group1); p.image->addNode(layer2, group1); p.image->addNode(layer3, group1); p.image->addNode(group4); p.image->addNode(layer5, group4); p.image->addNode(layer6, group4); p.image->initialRefreshGraph(); TestUtil::ReferenceImageChecker chk("passthrough", "imagetest"); QVERIFY(chk.checkDevice(p.image->projection(), p.image, "00_initial")); } QRect refRect; TestUtil::MaskParent p; KisImageSP image; KisSurrogateUndoStore *undoStore; KisGroupLayerSP group1; KisPaintLayerSP layer2; KisPaintLayerSP layer3; KisGroupLayerSP group4; KisPaintLayerSP layer5; KisPaintLayerSP layer6; }; void KisImageTest::testFlattenPassThroughLayer() { FlattenPassThroughTestImage p; TestUtil::ReferenceImageChecker chk("passthrough", "imagetest"); { QCOMPARE(p.group1->compositeOpId(), COMPOSITE_OVER); QCOMPARE(p.group1->passThroughMode(), true); KisLayerSP newLayer = flattenLayerHelper(p, p.group1); QVERIFY(chk.checkDevice(p.image->projection(), p.image, "00_initial")); QVERIFY(chk.checkDevice(newLayer->projection(), p.image, "01_group1_layerproj")); QCOMPARE(newLayer->compositeOpId(), COMPOSITE_OVER); QVERIFY(newLayer->inherits("KisPaintLayer")); } } void KisImageTest::testMergeTwoPassThroughLayers() { FlattenPassThroughTestImage p; TestUtil::ReferenceImageChecker chk("passthrough", "imagetest"); { QCOMPARE(p.group1->compositeOpId(), COMPOSITE_OVER); QCOMPARE(p.group1->passThroughMode(), true); KisLayerSP newLayer = mergeHelper(p, p.group4); QVERIFY(chk.checkDevice(p.image->projection(), p.image, "00_initial")); QCOMPARE(newLayer->compositeOpId(), COMPOSITE_OVER); QVERIFY(newLayer->inherits("KisGroupLayer")); } } void KisImageTest::testMergePaintOverPassThroughLayer() { FlattenPassThroughTestImage p; TestUtil::ReferenceImageChecker chk("passthrough", "imagetest"); { QCOMPARE(p.group1->compositeOpId(), COMPOSITE_OVER); QCOMPARE(p.group1->passThroughMode(), true); KisLayerSP newLayer = flattenLayerHelper(p, p.group4); QVERIFY(chk.checkDevice(p.image->projection(), p.image, "00_initial")); QVERIFY(newLayer->inherits("KisPaintLayer")); newLayer = mergeHelper(p, newLayer); QVERIFY(chk.checkDevice(p.image->projection(), p.image, "00_initial")); QVERIFY(newLayer->inherits("KisPaintLayer")); } } void KisImageTest::testMergePassThroughOverPaintLayer() { FlattenPassThroughTestImage p; TestUtil::ReferenceImageChecker chk("passthrough", "imagetest"); { QCOMPARE(p.group1->compositeOpId(), COMPOSITE_OVER); QCOMPARE(p.group1->passThroughMode(), true); KisLayerSP newLayer = flattenLayerHelper(p, p.group1); QVERIFY(chk.checkDevice(p.image->projection(), p.image, "00_initial")); QVERIFY(newLayer->inherits("KisPaintLayer")); newLayer = mergeHelper(p, p.group4); QVERIFY(chk.checkDevice(p.image->projection(), p.image, "00_initial")); QVERIFY(newLayer->inherits("KisPaintLayer")); } } #include "kis_paint_device_debug_utils.h" #include "kis_algebra_2d.h" void KisImageTest::testPaintOverlayMask() { QRect refRect(0, 0, 512, 512); TestUtil::MaskParent p(refRect); QRect fillRect(50, 50, 412, 412); QRect selectionRect(200, 200, 100, 50); KisPaintLayerSP layer1 = p.layer; layer1->paintDevice()->fill(fillRect, KoColor(Qt::yellow, layer1->colorSpace())); KisSelectionMaskSP mask = new KisSelectionMask(p.image); KisSelectionSP selection = new KisSelection(new KisSelectionDefaultBounds(layer1->paintDevice())); selection->pixelSelection()->select(selectionRect, 128); selection->pixelSelection()->select(KisAlgebra2D::blowRect(selectionRect,-0.3), 255); mask->setSelection(selection); //mask->setVisible(false); //mask->setActive(false); p.image->addNode(mask, layer1); // a simple layer to disable oblidge child mechanism KisPaintLayerSP layer2 = new KisPaintLayer(p.image, "layer2", OPACITY_OPAQUE_U8); p.image->addNode(layer2); p.image->initialRefreshGraph(); KIS_DUMP_DEVICE_2(p.image->projection(), refRect, "00_initial", "dd"); p.image->setOverlaySelectionMask(mask); p.image->waitForDone(); - KIS_DUMP_DEVICE_2(p.image->projection(), refRect, "01_activated", "dd"); + KIS_DUMP_DEVICE_2(p.image->projection(), refRect, "01_activated_00_image", "dd"); + KIS_DUMP_DEVICE_2(p.image->root()->original(), refRect, "01_activated_01_root_original", "dd"); + KIS_DUMP_DEVICE_2(p.image->root()->projection(), refRect, "01_activated_02_root_projection", "dd"); + + KisImageSP clonedImage = p.image->clone(); + clonedImage->waitForDone(); + KIS_DUMP_DEVICE_2(clonedImage->projection(), refRect, "02_cloned_when_activated_00_image", "dd"); + KIS_DUMP_DEVICE_2(clonedImage->root()->original(), refRect, "02_cloned_when_activated_01_root_original", "dd"); + KIS_DUMP_DEVICE_2(clonedImage->root()->projection(), refRect, "02_cloned_when_activated_02_root_projection", "dd"); p.image->setOverlaySelectionMask(0); p.image->waitForDone(); - KIS_DUMP_DEVICE_2(p.image->projection(), refRect, "02_deactivated", "dd"); - - - + KIS_DUMP_DEVICE_2(p.image->projection(), refRect, "03_deactivated", "dd"); } KISTEST_MAIN(KisImageTest) diff --git a/libs/image/tests/kis_simple_stroke_strategy_test.cpp b/libs/image/tests/kis_simple_stroke_strategy_test.cpp index 4e6d332f42..7d17d68d97 100644 --- a/libs/image/tests/kis_simple_stroke_strategy_test.cpp +++ b/libs/image/tests/kis_simple_stroke_strategy_test.cpp @@ -1,95 +1,96 @@ /* * Copyright (c) 2011 Dmitry Kazakov * * 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_simple_stroke_strategy_test.h" #include #include "kis_image.h" #include "kis_simple_stroke_strategy.h" class TestingSimpleStrokeStrategy : public KisSimpleStrokeStrategy { public: TestingSimpleStrokeStrategy() - : m_stageCounter(0) + : KisSimpleStrokeStrategy(QLatin1String("TestingSimpleStrokeStrategy")), + m_stageCounter(0) { enableJob(KisSimpleStrokeStrategy::JOB_INIT); enableJob(KisSimpleStrokeStrategy::JOB_FINISH); enableJob(KisSimpleStrokeStrategy::JOB_CANCEL); enableJob(KisSimpleStrokeStrategy::JOB_DOSTROKE); } ~TestingSimpleStrokeStrategy() override { QCOMPARE(m_stageCounter, 3); } void initStrokeCallback() override { QCOMPARE(m_stageCounter, 0); m_stageCounter++; } void finishStrokeCallback() override { QCOMPARE(m_stageCounter, 2); m_stageCounter++; } void cancelStrokeCallback() override { QCOMPARE(m_stageCounter, 2); m_stageCounter++; } void doStrokeCallback(KisStrokeJobData *data) override { Q_UNUSED(data); QCOMPARE(m_stageCounter, 1); m_stageCounter++; } private: int m_stageCounter; }; void KisSimpleStrokeStrategyTest::testFinish() { KisImageSP image = new KisImage(0, 100, 100, 0, "test executor image"); KisStrokeId id = image->startStroke(new TestingSimpleStrokeStrategy()); image->addJob(id, 0); image->endStroke(id); image->waitForDone(); } void KisSimpleStrokeStrategyTest::testCancel() { KisImageSP image = new KisImage(0, 100, 100, 0, "test executor image"); KisStrokeId id = image->startStroke(new TestingSimpleStrokeStrategy()); image->addJob(id, 0); /** * We add a delay to ensure the job is finished before the cancel * is requested, the cancel job will abort it otherwise */ QTest::qSleep(500); image->cancelStroke(id); image->waitForDone(); } QTEST_MAIN(KisSimpleStrokeStrategyTest) diff --git a/libs/image/tests/kis_strokes_queue_test.cpp b/libs/image/tests/kis_strokes_queue_test.cpp index 55ac7a8de2..67d130401e 100644 --- a/libs/image/tests/kis_strokes_queue_test.cpp +++ b/libs/image/tests/kis_strokes_queue_test.cpp @@ -1,839 +1,839 @@ /* * Copyright (c) 2011 Dmitry Kazakov * * 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_strokes_queue_test.h" #include #include "scheduler_utils.h" #include "kis_strokes_queue.h" #include "kis_updater_context.h" #include "kis_update_job_item.h" #include "kis_merge_walker.h" void KisStrokesQueueTest::testSequentialJobs() { KisStrokesQueue queue; - KisStrokeId id = queue.startStroke(new KisTestingStrokeStrategy("tri_", false)); + KisStrokeId id = queue.startStroke(new KisTestingStrokeStrategy(QLatin1String("tri_"), false)); queue.addJob(id, new KisStrokeJobData(KisStrokeJobData::CONCURRENT)); queue.addJob(id, new KisStrokeJobData(KisStrokeJobData::CONCURRENT)); queue.addJob(id, new KisStrokeJobData(KisStrokeJobData::CONCURRENT)); queue.endStroke(id); KisTestableUpdaterContext context(2); QVector jobs; queue.processQueue(context, false); jobs = context.getJobs(); COMPARE_NAME(jobs[0], "tri_init"); VERIFY_EMPTY(jobs[1]); context.clear(); queue.processQueue(context, false); jobs = context.getJobs(); COMPARE_NAME(jobs[0], "tri_dab"); COMPARE_NAME(jobs[1], "tri_dab"); context.clear(); queue.processQueue(context, false); jobs = context.getJobs(); COMPARE_NAME(jobs[0], "tri_dab"); VERIFY_EMPTY(jobs[1]); context.clear(); queue.processQueue(context, false); jobs = context.getJobs(); COMPARE_NAME(jobs[0], "tri_finish"); VERIFY_EMPTY(jobs[1]); } void KisStrokesQueueTest::testConcurrentSequentialBarrier() { KisStrokesQueue queue; - KisStrokeId id = queue.startStroke(new KisTestingStrokeStrategy("tri_", false)); + KisStrokeId id = queue.startStroke(new KisTestingStrokeStrategy(QLatin1String("tri_"), false)); queue.addJob(id, new KisStrokeJobData(KisStrokeJobData::CONCURRENT)); queue.addJob(id, new KisStrokeJobData(KisStrokeJobData::CONCURRENT)); queue.endStroke(id); // make the number of threads higher KisTestableUpdaterContext context(3); QVector jobs; queue.processQueue(context, false); jobs = context.getJobs(); COMPARE_NAME(jobs[0], "tri_init"); VERIFY_EMPTY(jobs[1]); context.clear(); queue.processQueue(context, false); jobs = context.getJobs(); COMPARE_NAME(jobs[0], "tri_dab"); COMPARE_NAME(jobs[1], "tri_dab"); context.clear(); queue.processQueue(context, false); jobs = context.getJobs(); COMPARE_NAME(jobs[0], "tri_finish"); VERIFY_EMPTY(jobs[1]); } void KisStrokesQueueTest::testExclusiveStrokes() { KisStrokesQueue queue; - KisStrokeId id = queue.startStroke(new KisTestingStrokeStrategy("excl_", true)); + KisStrokeId id = queue.startStroke(new KisTestingStrokeStrategy(QLatin1String("excl_"), true)); queue.addJob(id, new KisStrokeJobData(KisStrokeJobData::CONCURRENT)); queue.addJob(id, new KisStrokeJobData(KisStrokeJobData::CONCURRENT)); queue.addJob(id, new KisStrokeJobData(KisStrokeJobData::CONCURRENT)); queue.endStroke(id); // well, this walker is not initialized... but who cares? KisBaseRectsWalkerSP walker = new KisMergeWalker(QRect()); KisTestableUpdaterContext context(2); QVector jobs; context.addMergeJob(walker); queue.processQueue(context, false); jobs = context.getJobs(); COMPARE_WALKER(jobs[0], walker); VERIFY_EMPTY(jobs[1]); QCOMPARE(queue.needsExclusiveAccess(), true); context.clear(); queue.processQueue(context, false); jobs = context.getJobs(); COMPARE_NAME(jobs[0], "excl_init"); VERIFY_EMPTY(jobs[1]); QCOMPARE(queue.needsExclusiveAccess(), true); context.clear(); queue.processQueue(context, false); jobs = context.getJobs(); COMPARE_NAME(jobs[0], "excl_dab"); COMPARE_NAME(jobs[1], "excl_dab"); QCOMPARE(queue.needsExclusiveAccess(), true); context.clear(); context.addMergeJob(walker); queue.processQueue(context, false); COMPARE_WALKER(jobs[0], walker); VERIFY_EMPTY(jobs[1]); QCOMPARE(queue.needsExclusiveAccess(), true); context.clear(); queue.processQueue(context, false); jobs = context.getJobs(); COMPARE_NAME(jobs[0], "excl_dab"); VERIFY_EMPTY(jobs[1]); QCOMPARE(queue.needsExclusiveAccess(), true); context.clear(); queue.processQueue(context, false); jobs = context.getJobs(); COMPARE_NAME(jobs[0], "excl_finish"); VERIFY_EMPTY(jobs[1]); QCOMPARE(queue.needsExclusiveAccess(), true); context.clear(); queue.processQueue(context, false); QCOMPARE(queue.needsExclusiveAccess(), false); } void KisStrokesQueueTest::testBarrierStrokeJobs() { KisStrokesQueue queue; - KisStrokeId id = queue.startStroke(new KisTestingStrokeStrategy("nor_", false)); + KisStrokeId id = queue.startStroke(new KisTestingStrokeStrategy(QLatin1String("nor_"), false)); queue.addJob(id, new KisStrokeJobData(KisStrokeJobData::CONCURRENT)); queue.addJob(id, new KisStrokeJobData(KisStrokeJobData::BARRIER)); queue.addJob(id, new KisStrokeJobData(KisStrokeJobData::CONCURRENT)); queue.endStroke(id); // yes, this walker is not initialized again... but who cares? KisBaseRectsWalkerSP walker = new KisMergeWalker(QRect()); bool externalJobsPending = false; KisTestableUpdaterContext context(3); QVector jobs; queue.processQueue(context, externalJobsPending); jobs = context.getJobs(); COMPARE_NAME(jobs[0], "nor_init"); VERIFY_EMPTY(jobs[1]); VERIFY_EMPTY(jobs[2]); context.clear(); queue.processQueue(context, externalJobsPending); jobs = context.getJobs(); COMPARE_NAME(jobs[0], "nor_dab"); VERIFY_EMPTY(jobs[1]); VERIFY_EMPTY(jobs[2]); // Now some updates has come... context.addMergeJob(walker); jobs = context.getJobs(); COMPARE_NAME(jobs[0], "nor_dab"); COMPARE_WALKER(jobs[1], walker); VERIFY_EMPTY(jobs[2]); // No difference for the queue queue.processQueue(context, externalJobsPending); jobs = context.getJobs(); COMPARE_NAME(jobs[0], "nor_dab"); COMPARE_WALKER(jobs[1], walker); VERIFY_EMPTY(jobs[2]); // Even more updates has come... externalJobsPending = true; // Still no difference for the queue queue.processQueue(context, externalJobsPending); jobs = context.getJobs(); COMPARE_NAME(jobs[0], "nor_dab"); COMPARE_WALKER(jobs[1], walker); VERIFY_EMPTY(jobs[2]); // Now clear the context context.clear(); // And still no difference for the queue queue.processQueue(context, externalJobsPending); jobs = context.getJobs(); VERIFY_EMPTY(jobs[0]); VERIFY_EMPTY(jobs[1]); VERIFY_EMPTY(jobs[2]); // Process the last update... context.addMergeJob(walker); externalJobsPending = false; // Yep, the queue is still waiting queue.processQueue(context, externalJobsPending); jobs = context.getJobs(); COMPARE_WALKER(jobs[0], walker); VERIFY_EMPTY(jobs[1]); VERIFY_EMPTY(jobs[2]); context.clear(); // Finally, we can do our work. Barrier job is executed alone queue.processQueue(context, externalJobsPending); jobs = context.getJobs(); COMPARE_NAME(jobs[0], "nor_dab"); VERIFY_EMPTY(jobs[1]); VERIFY_EMPTY(jobs[2]); // Barrier job has finished context.clear(); jobs = context.getJobs(); VERIFY_EMPTY(jobs[0]); VERIFY_EMPTY(jobs[1]); VERIFY_EMPTY(jobs[2]); // fetch the last (concurrent) one queue.processQueue(context, externalJobsPending); jobs = context.getJobs(); COMPARE_NAME(jobs[0], "nor_dab"); VERIFY_EMPTY(jobs[1]); VERIFY_EMPTY(jobs[2]); context.clear(); // finish the stroke queue.processQueue(context, externalJobsPending); jobs = context.getJobs(); COMPARE_NAME(jobs[0], "nor_finish"); VERIFY_EMPTY(jobs[1]); VERIFY_EMPTY(jobs[2]); context.clear(); } void KisStrokesQueueTest::testStrokesOverlapping() { KisStrokesQueue queue; - KisStrokeId id = queue.startStroke(new KisTestingStrokeStrategy("1_", false, true)); + KisStrokeId id = queue.startStroke(new KisTestingStrokeStrategy(QLatin1String("1_"), false, true)); queue.addJob(id, 0); // comment out this line to catch an assert queue.endStroke(id); - id = queue.startStroke(new KisTestingStrokeStrategy("2_", false, true)); + id = queue.startStroke(new KisTestingStrokeStrategy(QLatin1String("2_"), false, true)); queue.addJob(id, 0); queue.endStroke(id); // uncomment this line to catch an assert // queue.addJob(id, 0); KisTestableUpdaterContext context(2); QVector jobs; queue.processQueue(context, false); jobs = context.getJobs(); COMPARE_NAME(jobs[0], "1_dab"); VERIFY_EMPTY(jobs[1]); context.clear(); queue.processQueue(context, false); jobs = context.getJobs(); COMPARE_NAME(jobs[0], "2_dab"); VERIFY_EMPTY(jobs[1]); } void KisStrokesQueueTest::testImmediateCancel() { KisStrokesQueue queue; KisTestableUpdaterContext context(2); - KisStrokeId id = queue.startStroke(new KisTestingStrokeStrategy("1_", false, false)); + KisStrokeId id = queue.startStroke(new KisTestingStrokeStrategy(QLatin1String("1_"), false, false)); queue.cancelStroke(id); // this should not crash queue.processQueue(context, false); } void KisStrokesQueueTest::testOpenedStrokeCounter() { KisStrokesQueue queue; QVERIFY(!queue.hasOpenedStrokes()); - KisStrokeId id0 = queue.startStroke(new KisTestingStrokeStrategy("0")); + KisStrokeId id0 = queue.startStroke(new KisTestingStrokeStrategy(QLatin1String("0"))); QVERIFY(queue.hasOpenedStrokes()); - KisStrokeId id1 = queue.startStroke(new KisTestingStrokeStrategy("1")); + KisStrokeId id1 = queue.startStroke(new KisTestingStrokeStrategy(QLatin1String("1"))); QVERIFY(queue.hasOpenedStrokes()); queue.endStroke(id0); QVERIFY(queue.hasOpenedStrokes()); queue.endStroke(id1); QVERIFY(!queue.hasOpenedStrokes()); KisTestableUpdaterContext context(2); queue.processQueue(context, false); context.clear(); queue.processQueue(context, false); context.clear(); queue.processQueue(context, false); context.clear(); queue.processQueue(context, false); context.clear(); } void KisStrokesQueueTest::testAsyncCancelWhileOpenedStroke() { KisStrokesQueue queue; - KisStrokeId id = queue.startStroke(new KisTestingStrokeStrategy("nor_", false)); + KisStrokeId id = queue.startStroke(new KisTestingStrokeStrategy(QLatin1String("nor_"), false)); queue.addJob(id, 0); queue.addJob(id, 0); queue.addJob(id, 0); // no async cancelling until the stroke is ended by the owner QVERIFY(!queue.tryCancelCurrentStrokeAsync()); queue.endStroke(id); QVERIFY(queue.tryCancelCurrentStrokeAsync()); bool externalJobsPending = false; KisTestableUpdaterContext context(3); QVector jobs; queue.processQueue(context, externalJobsPending); // no? really? jobs = context.getJobs(); VERIFY_EMPTY(jobs[0]); VERIFY_EMPTY(jobs[1]); VERIFY_EMPTY(jobs[2]); } struct KisStrokesQueueTest::LodStrokesQueueTester { LodStrokesQueueTester(bool real = false) : fakeContext(2), realContext(2), context(!real ? fakeContext : realContext) { queue.setSuspendUpdatesStrokeStrategyFactory( []() { return KisSuspendResumePair( - new KisTestingStrokeStrategy("susp_u_", false, true, true), + new KisTestingStrokeStrategy(QLatin1String("susp_u_"), false, true, true), QList()); }); queue.setResumeUpdatesStrokeStrategyFactory( []() { return KisSuspendResumePair( - new KisTestingStrokeStrategy("resu_u_", false, true, true), + new KisTestingStrokeStrategy(QLatin1String("resu_u_"), false, true, true), QList()); }); queue.setLod0ToNStrokeStrategyFactory( [](bool forgettable) { Q_UNUSED(forgettable); return KisSuspendResumePair( - new KisTestingStrokeStrategy("sync_u_", false, true, true), + new KisTestingStrokeStrategy(QLatin1String("sync_u_"), false, true, true), QList()); }); } KisStrokesQueue queue; KisTestableUpdaterContext fakeContext; KisUpdaterContext realContext; KisUpdaterContext &context; QVector jobs; void processQueueNoAdd() { if (&context != &fakeContext) return; fakeContext.clear(); jobs = fakeContext.getJobs(); VERIFY_EMPTY(jobs[0]); VERIFY_EMPTY(jobs[1]); } void processQueueNoContextClear() { queue.processQueue(context, false); if (&context == &realContext) { context.waitForDone(); } } void processQueue() { processQueueNoAdd(); queue.processQueue(context, false); if (&context == &realContext) { context.waitForDone(); } } void checkNothing() { KIS_ASSERT(&context == &fakeContext); jobs = fakeContext.getJobs(); VERIFY_EMPTY(jobs[0]); VERIFY_EMPTY(jobs[1]); } void checkJobs(const QStringList &list) { KIS_ASSERT(&context == &fakeContext); jobs = fakeContext.getJobs(); for (int i = 0; i < 2; i++) { if (list.size() <= i) { VERIFY_EMPTY(jobs[i]); } else { QVERIFY(jobs[i]->isRunning()); COMPARE_NAME(jobs[i], list[i]); } } QCOMPARE(queue.needsExclusiveAccess(), false); } void checkOnlyJob(const QString &name) { KIS_ASSERT(&context == &fakeContext); jobs = fakeContext.getJobs(); COMPARE_NAME(jobs[0], name); VERIFY_EMPTY(jobs[1]); QCOMPARE(queue.needsExclusiveAccess(), false); } void checkOnlyExecutedJob(const QString &name) { realContext.waitForDone(); QVERIFY(!globalExecutedDabs.isEmpty()); QCOMPARE(globalExecutedDabs[0], name); QCOMPARE(globalExecutedDabs.size(), 1); globalExecutedDabs.clear(); } void checkExecutedJobs(const QStringList &list) { realContext.waitForDone(); QCOMPARE(globalExecutedDabs, list); globalExecutedDabs.clear(); } void checkNothingExecuted() { realContext.waitForDone(); QVERIFY(globalExecutedDabs.isEmpty()); } }; void KisStrokesQueueTest::testStrokesLevelOfDetail() { LodStrokesQueueTester t; KisStrokesQueue &queue = t.queue; // create a stroke with LOD0 + LOD2 queue.setDesiredLevelOfDetail(2); // process sync-lodn-planes stroke t.processQueue(); t.checkOnlyJob("sync_u_init"); - KisStrokeId id2 = queue.startStroke(new KisTestingStrokeStrategy("lod_", false, true)); + KisStrokeId id2 = queue.startStroke(new KisTestingStrokeStrategy(QLatin1String("lod_"), false, true)); queue.addJob(id2, new KisTestingStrokeJobData(KisStrokeJobData::CONCURRENT)); queue.endStroke(id2); // create a update with LOD == 0 (default one) // well, this walker is not initialized... but who cares? KisBaseRectsWalkerSP walker = new KisMergeWalker(QRect()); KisTestableUpdaterContext context(2); QVector jobs; context.addMergeJob(walker); queue.processQueue(context, false); jobs = context.getJobs(); COMPARE_WALKER(jobs[0], walker); VERIFY_EMPTY(jobs[1]); QCOMPARE(queue.needsExclusiveAccess(), false); context.clear(); jobs = context.getJobs(); VERIFY_EMPTY(jobs[0]); VERIFY_EMPTY(jobs[1]); context.clear(); queue.processQueue(context, false); jobs = context.getJobs(); COMPARE_NAME(jobs[0], "clone2_lod_dab"); VERIFY_EMPTY(jobs[1]); QCOMPARE(queue.needsExclusiveAccess(), false); // walker of a different LOD must not be allowed QCOMPARE(context.isJobAllowed(walker), false); context.clear(); context.addMergeJob(walker); queue.processQueue(context, false); jobs = context.getJobs(); COMPARE_WALKER(jobs[0], walker); COMPARE_NAME(jobs[1], "susp_u_init"); QCOMPARE(queue.needsExclusiveAccess(), false); context.clear(); queue.processQueue(context, false); jobs = context.getJobs(); COMPARE_NAME(jobs[0], "lod_dab"); VERIFY_EMPTY(jobs[1]); QCOMPARE(queue.needsExclusiveAccess(), false); context.clear(); queue.processQueue(context, false); jobs = context.getJobs(); COMPARE_NAME(jobs[0], "resu_u_init"); VERIFY_EMPTY(jobs[1]); QCOMPARE(queue.needsExclusiveAccess(), false); context.clear(); } #include #include struct TestUndoCommand : public KUndo2Command { TestUndoCommand(const QString &text) : KUndo2Command(kundo2_noi18n(text)) {} void undo() override { ENTER_FUNCTION(); undoCount++; } void redo() override { ENTER_FUNCTION(); redoCount++; } int undoCount = 0; int redoCount = 0; }; void KisStrokesQueueTest::testLodUndoBase() { LodStrokesQueueTester t; KisStrokesQueue &queue = t.queue; // create a stroke with LOD0 + LOD2 queue.setDesiredLevelOfDetail(2); // process sync-lodn-planes stroke t.processQueue(); t.checkOnlyJob("sync_u_init"); - KisStrokeId id1 = queue.startStroke(new KisTestingStrokeStrategy("str1_", false, true)); + KisStrokeId id1 = queue.startStroke(new KisTestingStrokeStrategy(QLatin1String("str1_"), false, true)); queue.addJob(id1, new KisTestingStrokeJobData(KisStrokeJobData::CONCURRENT)); queue.endStroke(id1); - KisStrokeId id2 = queue.startStroke(new KisTestingStrokeStrategy("str2_", false, true)); + KisStrokeId id2 = queue.startStroke(new KisTestingStrokeStrategy(QLatin1String("str2_"), false, true)); queue.addJob(id2, new KisTestingStrokeJobData(KisStrokeJobData::CONCURRENT)); queue.endStroke(id2); t.processQueue(); t.checkOnlyJob("clone2_str1_dab"); QSharedPointer undoStr1(new TestUndoCommand("str1_undo")); queue.lodNPostExecutionUndoAdapter()->addCommand(undoStr1); t.processQueue(); t.checkOnlyJob("clone2_str2_dab"); QSharedPointer undoStr2(new TestUndoCommand("str2_undo")); queue.lodNPostExecutionUndoAdapter()->addCommand(undoStr2); t.processQueue(); t.checkOnlyJob("susp_u_init"); t.processQueue(); t.checkOnlyJob("str1_dab"); t.processQueue(); t.checkOnlyJob("str2_dab"); t.processQueue(); t.checkOnlyJob("resu_u_init"); } void KisStrokesQueueTest::testLodUndoBase2() { LodStrokesQueueTester t(true); KisStrokesQueue &queue = t.queue; // create a stroke with LOD0 + LOD2 queue.setDesiredLevelOfDetail(2); - KisStrokeId id1 = queue.startStroke(new KisTestingStrokeStrategy("str1_", false, true, false, true)); + KisStrokeId id1 = queue.startStroke(new KisTestingStrokeStrategy(QLatin1String("str1_"), false, true, false, true)); queue.addJob(id1, new KisTestingStrokeJobData(KisStrokeJobData::CONCURRENT)); queue.endStroke(id1); - KisStrokeId id2 = queue.startStroke(new KisTestingStrokeStrategy("str2_", false, true, false, true)); + KisStrokeId id2 = queue.startStroke(new KisTestingStrokeStrategy(QLatin1String("str2_"), false, true, false, true)); queue.addJob(id2, new KisTestingStrokeJobData(KisStrokeJobData::CONCURRENT)); queue.endStroke(id2); t.processQueue(); t.checkOnlyExecutedJob("sync_u_init"); t.processQueue(); t.checkOnlyExecutedJob("clone2_str1_dab"); QSharedPointer undoStr1(new TestUndoCommand("str1_undo")); queue.lodNPostExecutionUndoAdapter()->addCommand(undoStr1); t.processQueue(); t.checkOnlyExecutedJob("clone2_str2_dab"); QSharedPointer undoStr2(new TestUndoCommand("str2_undo")); queue.lodNPostExecutionUndoAdapter()->addCommand(undoStr2); t.processQueue(); t.checkOnlyExecutedJob("susp_u_init"); queue.tryUndoLastStrokeAsync(); t.processQueue(); while (queue.currentStrokeName() == kundo2_noi18n("str2_undo")) { //queue.debugPrintStrokes(); t.processQueue(); } QCOMPARE(undoStr2->undoCount, 1); t.checkOnlyExecutedJob("str1_dab"); t.processQueue(); t.checkOnlyExecutedJob("str2_cancel"); t.processQueue(); t.checkOnlyExecutedJob("resu_u_init"); } void KisStrokesQueueTest::testMutatedJobs() { LodStrokesQueueTester t(true); KisStrokesQueue &queue = t.queue; - KisStrokeId id1 = queue.startStroke(new KisTestingStrokeStrategy("str1_", false, true, false, true)); + KisStrokeId id1 = queue.startStroke(new KisTestingStrokeStrategy(QLatin1String("str1_"), false, true, false, true)); queue.addJob(id1, new KisTestingStrokeJobData( KisStrokeJobData::CONCURRENT, KisStrokeJobData::NORMAL, true, "1")); queue.addJob(id1, new KisTestingStrokeJobData( KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::NORMAL, false, "2")); queue.endStroke(id1); t.processQueue(); t.checkOnlyExecutedJob("str1_dab_1"); t.processQueue(); QStringList refList; refList << "str1_dab_mutated" << "str1_dab_mutated"; t.checkExecutedJobs(refList); t.processQueue(); t.checkOnlyExecutedJob("str1_dab_mutated"); t.processQueue(); t.checkOnlyExecutedJob("str1_dab_2"); t.processQueue(); t.checkNothingExecuted(); } QString sequentialityToString(KisStrokeJobData::Sequentiality seq) { QString result = ""; switch (seq) { case KisStrokeJobData::SEQUENTIAL: result = "SEQUENTIAL"; break; case KisStrokeJobData::UNIQUELY_CONCURRENT: result = "UNIQUELY_CONCURRENT"; break; case KisStrokeJobData::BARRIER: result = "BARRIER"; break; case KisStrokeJobData::CONCURRENT: result = "CONCURRENT"; break; } return result; } void KisStrokesQueueTest::checkJobsOverlapping(LodStrokesQueueTester &t, KisStrokeId id, KisStrokeJobData::Sequentiality first, KisStrokeJobData::Sequentiality second, bool allowed) { t.queue.addJob(id, new KisTestingStrokeJobData(first, KisStrokeJobData::NORMAL, false, "first")); t.processQueue(); t.checkJobs({"str1_dab_first"}); t.queue.addJob(id, new KisTestingStrokeJobData(second, KisStrokeJobData::NORMAL, false, "second")); qDebug() << QString(" test %1 after %2 allowed: %3 ") .arg(sequentialityToString(second), 24) .arg(sequentialityToString(first), 24) .arg(allowed); if (allowed) { t.processQueueNoContextClear(); t.checkJobs({"str1_dab_first", "str1_dab_second"}); } else { t.processQueueNoContextClear(); t.checkJobs({"str1_dab_first"}); t.processQueue(); t.checkJobs({"str1_dab_second"}); } t.processQueueNoAdd(); t.checkNothing(); } void KisStrokesQueueTest::testUniquelyConcurrentJobs() { LodStrokesQueueTester t; KisStrokesQueue &queue = t.queue; - KisStrokeId id1 = queue.startStroke(new KisTestingStrokeStrategy("str1_", false, true)); + KisStrokeId id1 = queue.startStroke(new KisTestingStrokeStrategy(QLatin1String("str1_"), false, true)); queue.addJob(id1, new KisTestingStrokeJobData(KisStrokeJobData::CONCURRENT)); queue.addJob(id1, new KisTestingStrokeJobData(KisStrokeJobData::CONCURRENT)); { // manual test t.processQueue(); t.checkJobs({"str1_dab", "str1_dab"}); queue.addJob(id1, new KisTestingStrokeJobData(KisStrokeJobData::CONCURRENT)); t.processQueue(); t.checkJobs({"str1_dab"}); queue.addJob(id1, new KisTestingStrokeJobData(KisStrokeJobData::UNIQUELY_CONCURRENT, KisStrokeJobData::NORMAL, false, "ucon")); t.processQueueNoContextClear(); t.checkJobs({"str1_dab", "str1_dab_ucon"}); t.processQueueNoAdd(); t.checkNothing(); } // Test various cases of overlapping checkJobsOverlapping(t, id1, KisStrokeJobData::UNIQUELY_CONCURRENT, KisStrokeJobData::CONCURRENT, true); checkJobsOverlapping(t, id1, KisStrokeJobData::UNIQUELY_CONCURRENT, KisStrokeJobData::UNIQUELY_CONCURRENT, false); checkJobsOverlapping(t, id1, KisStrokeJobData::UNIQUELY_CONCURRENT, KisStrokeJobData::SEQUENTIAL, false); checkJobsOverlapping(t, id1, KisStrokeJobData::UNIQUELY_CONCURRENT, KisStrokeJobData::BARRIER, false); checkJobsOverlapping(t, id1, KisStrokeJobData::CONCURRENT, KisStrokeJobData::UNIQUELY_CONCURRENT , true); checkJobsOverlapping(t, id1, KisStrokeJobData::UNIQUELY_CONCURRENT, KisStrokeJobData::UNIQUELY_CONCURRENT, false); checkJobsOverlapping(t, id1, KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::UNIQUELY_CONCURRENT, false); checkJobsOverlapping(t, id1, KisStrokeJobData::BARRIER, KisStrokeJobData::UNIQUELY_CONCURRENT, false); queue.endStroke(id1); } QTEST_MAIN(KisStrokesQueueTest) diff --git a/libs/image/tests/kis_update_scheduler_test.cpp b/libs/image/tests/kis_update_scheduler_test.cpp index f4e6bebae9..bda1e77872 100644 --- a/libs/image/tests/kis_update_scheduler_test.cpp +++ b/libs/image/tests/kis_update_scheduler_test.cpp @@ -1,431 +1,431 @@ /* * Copyright (c) 2010 Dmitry Kazakov * * 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_update_scheduler_test.h" #include #include #include #include "kis_group_layer.h" #include "kis_paint_layer.h" #include "kis_adjustment_layer.h" #include "filter/kis_filter.h" #include "filter/kis_filter_configuration.h" #include "filter/kis_filter_registry.h" #include "kis_selection.h" #include "scheduler_utils.h" #include "kis_update_scheduler.h" #include "kis_updater_context.h" #include "kis_update_job_item.h" #include "kis_simple_update_queue.h" #include "../../sdk/tests/testutil.h" KisImageSP KisUpdateSchedulerTest::buildTestingImage() { QImage sourceImage1(QString(FILES_DATA_DIR) + QDir::separator() + "hakonepa.png"); QImage sourceImage2(QString(FILES_DATA_DIR) + QDir::separator() + "inverted_hakonepa.png"); QRect imageRect = QRect(QPoint(0,0), sourceImage1.size()); const KoColorSpace * cs = KoColorSpaceRegistry::instance()->rgb8(); KisImageSP image = new KisImage(0, imageRect.width(), imageRect.height(), cs, "merge test"); KisFilterSP filter = KisFilterRegistry::instance()->value("blur"); Q_ASSERT(filter); KisFilterConfigurationSP configuration = filter->defaultConfiguration(); Q_ASSERT(configuration); KisPaintLayerSP paintLayer1 = new KisPaintLayer(image, "paint1", OPACITY_OPAQUE_U8); KisPaintLayerSP paintLayer2 = new KisPaintLayer(image, "paint2", OPACITY_OPAQUE_U8 / 3); KisLayerSP blur1 = new KisAdjustmentLayer(image, "blur1", configuration, 0); paintLayer1->paintDevice()->convertFromQImage(sourceImage1, 0, 0, 0); paintLayer2->paintDevice()->convertFromQImage(sourceImage2, 0, 0, 0); image->lock(); image->addNode(paintLayer1); image->addNode(paintLayer2); image->addNode(blur1); image->unlock(); return image; } void KisUpdateSchedulerTest::testMerge() { KisImageSP image = buildTestingImage(); QRect imageRect = image->bounds(); KisNodeSP rootLayer = image->rootLayer(); KisNodeSP paintLayer1 = rootLayer->firstChild(); QCOMPARE(paintLayer1->name(), QString("paint1")); KisUpdateScheduler scheduler(image.data()); /** * Test synchronous Full Refresh */ scheduler.fullRefresh(rootLayer, image->bounds(), image->bounds()); QCOMPARE(rootLayer->exactBounds(), image->bounds()); QImage resultFRProjection = rootLayer->projection()->convertToQImage(0); resultFRProjection.save(QString(FILES_OUTPUT_DIR) + QDir::separator() + "scheduler_fr_merge_result.png"); /** * Test incremental updates */ rootLayer->projection()->clear(); const qint32 num = 4; qint32 width = imageRect.width() / num; qint32 lastWidth = imageRect.width() - width; QVector dirtyRects(num); for(qint32 i = 0; i < num-1; i++) { dirtyRects[i] = QRect(width*i, 0, width, imageRect.height()); } dirtyRects[num-1] = QRect(width*(num-1), 0, lastWidth, imageRect.height()); for(qint32 i = 0; i < num; i+=2) { scheduler.updateProjection(paintLayer1, dirtyRects[i], image->bounds()); } for(qint32 i = 1; i < num; i+=2) { scheduler.updateProjection(paintLayer1, dirtyRects[i], image->bounds()); } scheduler.waitForDone(); QCOMPARE(rootLayer->exactBounds(), image->bounds()); QImage resultDirtyProjection = rootLayer->projection()->convertToQImage(0); resultDirtyProjection.save(QString(FILES_OUTPUT_DIR) + QDir::separator() + "scheduler_dp_merge_result.png"); QPoint pt; QVERIFY(TestUtil::compareQImages(pt, resultFRProjection, resultDirtyProjection)); } void KisUpdateSchedulerTest::benchmarkOverlappedMerge() { KisImageSP image = buildTestingImage(); KisNodeSP rootLayer = image->rootLayer(); KisNodeSP paintLayer1 = rootLayer->firstChild(); QRect imageRect = image->bounds(); QCOMPARE(paintLayer1->name(), QString("paint1")); QCOMPARE(imageRect, QRect(0,0,640,441)); KisUpdateScheduler scheduler(image.data()); const qint32 xShift = 10; const qint32 yShift = 0; const qint32 numShifts = 64; QBENCHMARK{ QRect dirtyRect(0, 0, 200, imageRect.height()); for(int i = 0; i < numShifts; i++) { // dbgKrita << dirtyRect; scheduler.updateProjection(paintLayer1, dirtyRect, image->bounds()); dirtyRect.translate(xShift, yShift); } scheduler.waitForDone(); } } void KisUpdateSchedulerTest::testLocking() { KisImageSP image = buildTestingImage(); KisNodeSP rootLayer = image->rootLayer(); KisNodeSP paintLayer1 = rootLayer->firstChild(); QRect imageRect = image->bounds(); QCOMPARE(paintLayer1->name(), QString("paint1")); QCOMPARE(imageRect, QRect(0,0,640,441)); KisTestableUpdateScheduler scheduler(image.data(), 2); KisUpdaterContext *context = scheduler.updaterContext(); QVERIFY(context); QVector jobs; QRect dirtyRect1(0,0,50,100); QRect dirtyRect2(0,0,100,100); QRect dirtyRect3(50,0,50,100); QRect dirtyRect4(150,150,50,50); scheduler.updateProjection(paintLayer1, imageRect, imageRect); jobs = context->getJobs(); QCOMPARE(jobs[0]->isRunning(), true); QCOMPARE(jobs[1]->isRunning(), false); QVERIFY(checkWalker(jobs[0]->walker(), imageRect)); context->clear(); scheduler.lock(); scheduler.updateProjection(paintLayer1, dirtyRect1, imageRect); scheduler.updateProjection(paintLayer1, dirtyRect2, imageRect); scheduler.updateProjection(paintLayer1, dirtyRect3, imageRect); scheduler.updateProjection(paintLayer1, dirtyRect4, imageRect); jobs = context->getJobs(); QCOMPARE(jobs[0]->isRunning(), false); QCOMPARE(jobs[1]->isRunning(), false); scheduler.unlock(); jobs = context->getJobs(); QCOMPARE(jobs[0]->isRunning(), true); QCOMPARE(jobs[1]->isRunning(), true); QVERIFY(checkWalker(jobs[0]->walker(), dirtyRect2)); QVERIFY(checkWalker(jobs[1]->walker(), dirtyRect4)); } void KisUpdateSchedulerTest::testExclusiveStrokes() { KisImageSP image = buildTestingImage(); KisNodeSP rootLayer = image->rootLayer(); KisNodeSP paintLayer1 = rootLayer->firstChild(); QRect imageRect = image->bounds(); QCOMPARE(paintLayer1->name(), QString("paint1")); QCOMPARE(imageRect, QRect(0,0,640,441)); QRect dirtyRect1(0,0,50,100); KisTestableUpdateScheduler scheduler(image.data(), 2); KisUpdaterContext *context = scheduler.updaterContext(); QVector jobs; scheduler.updateProjection(paintLayer1, dirtyRect1, imageRect); jobs = context->getJobs(); QCOMPARE(jobs[0]->isRunning(), true); QCOMPARE(jobs[1]->isRunning(), false); QVERIFY(checkWalker(jobs[0]->walker(), dirtyRect1)); - KisStrokeId id = scheduler.startStroke(new KisTestingStrokeStrategy("excl_", true, false)); + KisStrokeId id = scheduler.startStroke(new KisTestingStrokeStrategy(QLatin1String("excl_"), true, false)); jobs = context->getJobs(); QCOMPARE(jobs[0]->isRunning(), true); QCOMPARE(jobs[1]->isRunning(), false); QVERIFY(checkWalker(jobs[0]->walker(), dirtyRect1)); context->clear(); scheduler.endStroke(id); jobs = context->getJobs(); QCOMPARE(jobs[0]->isRunning(), true); QCOMPARE(jobs[1]->isRunning(), false); COMPARE_NAME(jobs[0], "excl_init"); scheduler.updateProjection(paintLayer1, dirtyRect1, imageRect); jobs = context->getJobs(); QCOMPARE(jobs[0]->isRunning(), true); QCOMPARE(jobs[1]->isRunning(), false); COMPARE_NAME(jobs[0], "excl_init"); context->clear(); scheduler.processQueues(); jobs = context->getJobs(); QCOMPARE(jobs[0]->isRunning(), true); QCOMPARE(jobs[1]->isRunning(), false); COMPARE_NAME(jobs[0], "excl_finish"); context->clear(); scheduler.processQueues(); jobs = context->getJobs(); QCOMPARE(jobs[0]->isRunning(), true); QCOMPARE(jobs[1]->isRunning(), false); QVERIFY(checkWalker(jobs[0]->walker(), dirtyRect1)); } void KisUpdateSchedulerTest::testEmptyStroke() { KisImageSP image = buildTestingImage(); - KisStrokeId id = image->startStroke(new KisStrokeStrategy()); + KisStrokeId id = image->startStroke(new KisStrokeStrategy(QLatin1String())); image->addJob(id, 0); image->endStroke(id); image->waitForDone(); } #include "kis_lazy_wait_condition.h" void KisUpdateSchedulerTest::testLazyWaitCondition() { { dbgKrita << "Not initialized"; KisLazyWaitCondition condition; QVERIFY(!condition.wait(50)); } { dbgKrita << "Initialized, not awake"; KisLazyWaitCondition condition; condition.initWaiting(); QVERIFY(!condition.wait(50)); condition.endWaiting(); } { dbgKrita << "Initialized, awake"; KisLazyWaitCondition condition; condition.initWaiting(); condition.wakeAll(); QVERIFY(condition.wait(50)); condition.endWaiting(); } { dbgKrita << "Initialized, not awake, then awake"; KisLazyWaitCondition condition; condition.initWaiting(); QVERIFY(!condition.wait(50)); condition.wakeAll(); QVERIFY(condition.wait(50)); condition.endWaiting(); } { dbgKrita << "Doublewait"; KisLazyWaitCondition condition; condition.initWaiting(); condition.initWaiting(); QVERIFY(!condition.wait(50)); condition.wakeAll(); QVERIFY(condition.wait(50)); QVERIFY(condition.wait(50)); condition.endWaiting(); } } #define NUM_THREADS 10 #define NUM_CYCLES 500 #define NTH_CHECK 3 class UpdatesBlockTester : public QRunnable { public: UpdatesBlockTester(KisUpdateScheduler *scheduler, KisNodeSP node) : m_scheduler(scheduler), m_node(node) { } void run() override { for (int i = 0; i < NUM_CYCLES; i++) { if(i % NTH_CHECK == 0) { m_scheduler->blockUpdates(); QTest::qSleep(1); // a bit of salt for crashiness ;) Q_ASSERT(!m_scheduler->haveUpdatesRunning()); m_scheduler->unblockUpdates(); } else { QRect updateRect(0,0,100,100); updateRect.moveTopLeft(QPoint((i%10)*100, (i%10)*100)); m_scheduler->updateProjection(m_node, updateRect, QRect(0,0,1100,1100)); } } } private: KisUpdateScheduler *m_scheduler; KisNodeSP m_node; }; void KisUpdateSchedulerTest::testBlockUpdates() { KisImageSP image = buildTestingImage(); KisNodeSP rootLayer = image->rootLayer(); KisNodeSP paintLayer1 = rootLayer->firstChild(); KisUpdateScheduler scheduler(image.data()); QThreadPool threadPool; threadPool.setMaxThreadCount(NUM_THREADS); for(int i = 0; i< NUM_THREADS; i++) { UpdatesBlockTester *tester = new UpdatesBlockTester(&scheduler, paintLayer1); threadPool.start(tester); } threadPool.waitForDone(); } #include "kis_update_time_monitor.h" void KisUpdateSchedulerTest::testTimeMonitor() { QVector dirtyRects; KisUpdateTimeMonitor::instance()->startStrokeMeasure(); KisUpdateTimeMonitor::instance()->reportMouseMove(QPointF(100, 0)); KisUpdateTimeMonitor::instance()->reportJobStarted((void*) 10); QTest::qSleep(300); KisUpdateTimeMonitor::instance()->reportJobStarted((void*) 20); QTest::qSleep(100); dirtyRects << QRect(10,10,10,10); KisUpdateTimeMonitor::instance()->reportJobFinished((void*) 10, dirtyRects); QTest::qSleep(100); dirtyRects.clear(); dirtyRects << QRect(30,30,10,10); KisUpdateTimeMonitor::instance()->reportJobFinished((void*) 20, dirtyRects); QTest::qSleep(500); KisUpdateTimeMonitor::instance()->reportUpdateFinished(QRect(10,10,10,10)); QTest::qSleep(300); KisUpdateTimeMonitor::instance()->reportUpdateFinished(QRect(30,30,10,10)); KisUpdateTimeMonitor::instance()->reportMouseMove(QPointF(130, 0)); KisUpdateTimeMonitor::instance()->endStrokeMeasure(); } void KisUpdateSchedulerTest::testLodSync() { KisImageSP image = buildTestingImage(); KisNodeSP rootLayer = image->root(); KisNodeSP paintLayer1 = rootLayer->firstChild(); QCOMPARE(paintLayer1->name(), QString("paint1")); image->setLevelOfDetailBlocked(false); image->setDesiredLevelOfDetail(2); image->explicitRegenerateLevelOfDetail(); image->waitForDone(); } QTEST_MAIN(KisUpdateSchedulerTest) diff --git a/libs/image/tests/kis_updater_context_test.cpp b/libs/image/tests/kis_updater_context_test.cpp index 9d00d02650..3559b8128a 100644 --- a/libs/image/tests/kis_updater_context_test.cpp +++ b/libs/image/tests/kis_updater_context_test.cpp @@ -1,245 +1,249 @@ /* * Copyright (c) 2010 Dmitry Kazakov * * 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_updater_context_test.h" #include #include #include #include #include "kis_paint_layer.h" #include "kis_merge_walker.h" #include "kis_updater_context.h" #include "kis_image.h" #include "scheduler_utils.h" #include "lod_override.h" #include "config-limit-long-tests.h" void KisUpdaterContextTest::testJobInterference() { KisTestableUpdaterContext context(3); QRect imageRect(0,0,100,100); const KoColorSpace * cs = KoColorSpaceRegistry::instance()->rgb8(); KisImageSP image = new KisImage(0, imageRect.width(), imageRect.height(), cs, "merge test"); KisPaintLayerSP paintLayer = new KisPaintLayer(image, "test", OPACITY_OPAQUE_U8); image->lock(); image->addNode(paintLayer); image->unlock(); QRect dirtyRect1(0,0,50,100); KisBaseRectsWalkerSP walker1 = new KisMergeWalker(imageRect); walker1->collectRects(paintLayer, dirtyRect1); context.lock(); context.addMergeJob(walker1); context.unlock(); // overlapping job --- forbidden { QRect dirtyRect(30,0,100,100); KisBaseRectsWalkerSP walker = new KisMergeWalker(imageRect); walker->collectRects(paintLayer, dirtyRect); context.lock(); QVERIFY(!context.isJobAllowed(walker)); context.unlock(); } // not overlapping job --- allowed { QRect dirtyRect(60,0,100,100); KisBaseRectsWalkerSP walker = new KisMergeWalker(imageRect); walker->collectRects(paintLayer, dirtyRect); context.lock(); QVERIFY(context.isJobAllowed(walker)); context.unlock(); } // not overlapping job, conflicting LOD --- forbidden { TestUtil::LodOverride l(1, image); QCOMPARE(paintLayer->paintDevice()->defaultBounds()->currentLevelOfDetail(), 1); QRect dirtyRect(60,0,100,100); KisBaseRectsWalkerSP walker = new KisMergeWalker(imageRect); walker->collectRects(paintLayer, dirtyRect); context.lock(); QVERIFY(!context.isJobAllowed(walker)); context.unlock(); } } void KisUpdaterContextTest::testSnapshot() { KisTestableUpdaterContext context(3); QRect imageRect(0,0,100,100); KisBaseRectsWalkerSP walker1 = new KisMergeWalker(imageRect); qint32 numMergeJobs = -777; qint32 numStrokeJobs = -777; context.lock(); context.getJobsSnapshot(numMergeJobs, numStrokeJobs); QCOMPARE(numMergeJobs, 0); QCOMPARE(numStrokeJobs, 0); QCOMPARE(context.currentLevelOfDetail(), -1); context.addMergeJob(walker1); context.getJobsSnapshot(numMergeJobs, numStrokeJobs); QCOMPARE(numMergeJobs, 1); QCOMPARE(numStrokeJobs, 0); QCOMPARE(context.currentLevelOfDetail(), 0); KisStrokeJobData *data = new KisStrokeJobData(KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::NORMAL); QScopedPointer strategy( new KisNoopDabStrategy("test")); context.addStrokeJob(new KisStrokeJob(strategy.data(), data, 0, true)); context.getJobsSnapshot(numMergeJobs, numStrokeJobs); QCOMPARE(numMergeJobs, 1); QCOMPARE(numStrokeJobs, 1); QCOMPARE(context.currentLevelOfDetail(), 0); context.addSpontaneousJob(new KisNoopSpontaneousJob()); context.getJobsSnapshot(numMergeJobs, numStrokeJobs); QCOMPARE(numMergeJobs, 2); QCOMPARE(numStrokeJobs, 1); QCOMPARE(context.currentLevelOfDetail(), 0); context.unlock(); { context.lock(); context.clear(); context.getJobsSnapshot(numMergeJobs, numStrokeJobs); QCOMPARE(numMergeJobs, 0); QCOMPARE(numStrokeJobs, 0); QCOMPARE(context.currentLevelOfDetail(), -1); data = new KisStrokeJobData(KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::NORMAL); context.addStrokeJob(new KisStrokeJob(strategy.data(), data, 2, true)); context.getJobsSnapshot(numMergeJobs, numStrokeJobs); QCOMPARE(numMergeJobs, 0); QCOMPARE(numStrokeJobs, 1); QCOMPARE(context.currentLevelOfDetail(), 2); context.unlock(); } } #define NUM_THREADS 10 #ifdef LIMIT_LONG_TESTS # define NUM_JOBS 60 #else # define NUM_JOBS 6000 #endif #define EXCLUSIVE_NTH 3 #define NUM_CHECKS 10 #define CHECK_DELAY 3 // ms class ExclusivenessCheckerStrategy : public KisStrokeJobStrategy { public: ExclusivenessCheckerStrategy(QAtomicInt &counter, QAtomicInt &hadConcurrency) : m_counter(counter), m_hadConcurrency(hadConcurrency) { } void run(KisStrokeJobData *data) override { Q_UNUSED(data); m_counter.ref(); for(int i = 0; i < NUM_CHECKS; i++) { if(data->isExclusive()) { Q_ASSERT(m_counter == 1); } else if (m_counter > 1) { m_hadConcurrency.ref(); } QTest::qSleep(CHECK_DELAY); } m_counter.deref(); } + QString debugId() const override { + return "ExclusivenessCheckerStrategy"; + } + private: QAtomicInt &m_counter; QAtomicInt &m_hadConcurrency; }; void KisUpdaterContextTest::stressTestExclusiveJobs() { KisUpdaterContext context(NUM_THREADS); QAtomicInt counter; QAtomicInt hadConcurrency; for(int i = 0; i < NUM_JOBS; i++) { if(context.hasSpareThread()) { bool isExclusive = i % EXCLUSIVE_NTH == 0; KisStrokeJobData *data = new KisStrokeJobData(KisStrokeJobData::SEQUENTIAL, isExclusive ? KisStrokeJobData::EXCLUSIVE : KisStrokeJobData::NORMAL); KisStrokeJobStrategy *strategy = new ExclusivenessCheckerStrategy(counter, hadConcurrency); context.addStrokeJob(new KisStrokeJob(strategy, data, 0, true)); } else { QTest::qSleep(CHECK_DELAY); } } context.waitForDone(); QVERIFY(!counter); dbgKrita << "Concurrency observed:" << hadConcurrency << "/" << NUM_CHECKS * NUM_JOBS; } QTEST_MAIN(KisUpdaterContextTest) diff --git a/libs/image/tests/scheduler_utils.h b/libs/image/tests/scheduler_utils.h index 3c28f6e0de..c35e2d70cc 100644 --- a/libs/image/tests/scheduler_utils.h +++ b/libs/image/tests/scheduler_utils.h @@ -1,281 +1,293 @@ /* * Copyright (c) 2011 Dmitry Kazakov * * 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. */ #ifndef __SCHEDULER_UTILS_H #define __SCHEDULER_UTILS_H #include #include "kis_merge_walker.h" #include "kis_stroke_strategy.h" #include "kis_stroke_job.h" #include "kis_spontaneous_job.h" #include "kis_stroke.h" #include "kis_image.h" #define SCOMPARE(s1, s2) QCOMPARE(QString(s1), QString(s2)) #define COMPARE_WALKER(item, walker) \ QCOMPARE(item->walker(), walker) #define COMPARE_NAME(item, name) \ QCOMPARE(getJobName(item->strokeJob()), QString(name)) #define VERIFY_EMPTY(item) \ QVERIFY(!item->isRunning()) void executeStrokeJobs(KisStroke *stroke) { KisStrokeJob *job; while((job = stroke->popOneJob())) { job->run(); delete job; } } bool checkWalker(KisBaseRectsWalkerSP walker, const QRect &rect, int lod = 0) { if(walker->requestedRect() == rect && walker->levelOfDetail() == lod) { return true; } else { dbgKrita << "walker rect:" << walker->requestedRect(); dbgKrita << "expected rect:" << rect; dbgKrita << "walker lod:" << walker->levelOfDetail(); dbgKrita << "expected lod:" << lod; return false; } } class KisNoopSpontaneousJob : public KisSpontaneousJob { public: KisNoopSpontaneousJob(bool overridesEverything = false, int lod = 0) : m_overridesEverything(overridesEverything), m_lod(lod) { } void run() override { } bool overrides(const KisSpontaneousJob *otherJob) override { Q_UNUSED(otherJob); return m_overridesEverything; } int levelOfDetail() const override { return m_lod; } + QString debugName() const override { + return "KisNoopSpontaneousJob"; + } + private: bool m_overridesEverything; int m_lod; }; static QStringList globalExecutedDabs; class KisNoopDabStrategy : public KisStrokeJobStrategy { public: KisNoopDabStrategy(QString name) : m_name(name), m_isMarked(false) {} void run(KisStrokeJobData *data) override { Q_UNUSED(data); globalExecutedDabs << m_name; } virtual QString name(KisStrokeJobData *data) const { Q_UNUSED(data); return m_name; } void setMarked() { m_isMarked = true; } bool isMarked() const { return m_isMarked; } + QString debugId() const override { + return "KisNoopDabStrategy"; + } + private: QString m_name; bool m_isMarked; }; class KisTestingStrokeJobData : public KisStrokeJobData { public: KisTestingStrokeJobData(Sequentiality sequentiality = SEQUENTIAL, Exclusivity exclusivity = NORMAL, bool addMutatedJobs = false, const QString &customSuffix = QString()) : KisStrokeJobData(sequentiality, exclusivity), m_addMutatedJobs(addMutatedJobs), m_customSuffix(customSuffix) { } KisTestingStrokeJobData(const KisTestingStrokeJobData &rhs) : KisStrokeJobData(rhs), m_addMutatedJobs(rhs.m_addMutatedJobs) { } KisStrokeJobData* createLodClone(int levelOfDetail) override { Q_UNUSED(levelOfDetail); return new KisTestingStrokeJobData(*this); } bool m_addMutatedJobs = false; bool m_isMutated = false; QString m_customSuffix; }; class KisMutatableDabStrategy : public KisNoopDabStrategy { public: KisMutatableDabStrategy(const QString &name, KisStrokeStrategy *parentStrokeStrategy) : KisNoopDabStrategy(name), m_parentStrokeStrategy(parentStrokeStrategy) { } void run(KisStrokeJobData *data) override { KisTestingStrokeJobData *td = dynamic_cast(data); if (td && td->m_isMutated) { globalExecutedDabs << QString("%1_mutated").arg(name(data)); } else if (td && td->m_addMutatedJobs) { globalExecutedDabs << name(data); for (int i = 0; i < 3; i++) { KisTestingStrokeJobData *newData = new KisTestingStrokeJobData(td->sequentiality(), td->exclusivity(), false); newData->m_isMutated = true; m_parentStrokeStrategy->addMutatedJob(newData); } } else { globalExecutedDabs << name(data); } } virtual QString name(KisStrokeJobData *data) const override { const QString baseName = KisNoopDabStrategy::name(data); KisTestingStrokeJobData *td = dynamic_cast(data); return !td || td->m_customSuffix.isEmpty() ? baseName : QString("%1_%2").arg(baseName).arg(td->m_customSuffix); } + QString debugId() const override { + return "KisMutatableDabStrategy"; + } + private: KisStrokeStrategy *m_parentStrokeStrategy = 0; }; class KisTestingStrokeStrategy : public KisStrokeStrategy { public: - KisTestingStrokeStrategy(const QString &prefix = QString(), + KisTestingStrokeStrategy(const QLatin1String &prefix = QLatin1String(), bool exclusive = false, bool inhibitServiceJobs = false, bool forceAllowInitJob = false, bool forceAllowCancelJob = false) : KisStrokeStrategy(prefix, kundo2_noi18n(prefix)), m_prefix(prefix), m_inhibitServiceJobs(inhibitServiceJobs), m_forceAllowInitJob(forceAllowInitJob), m_forceAllowCancelJob(forceAllowCancelJob), m_cancelSeqNo(0) { setExclusive(exclusive); } KisTestingStrokeStrategy(const KisTestingStrokeStrategy &rhs, int levelOfDetail) : KisStrokeStrategy(rhs), m_prefix(rhs.m_prefix), m_inhibitServiceJobs(rhs.m_inhibitServiceJobs), m_forceAllowInitJob(rhs.m_forceAllowInitJob), m_cancelSeqNo(rhs.m_cancelSeqNo) { m_prefix = QString("clone%1_%2").arg(levelOfDetail).arg(m_prefix); } KisStrokeJobStrategy* createInitStrategy() override { return m_forceAllowInitJob || !m_inhibitServiceJobs ? new KisNoopDabStrategy(m_prefix + "init") : 0; } KisStrokeJobStrategy* createFinishStrategy() override { return !m_inhibitServiceJobs ? new KisNoopDabStrategy(m_prefix + "finish") : 0; } KisStrokeJobStrategy* createCancelStrategy() override { return m_forceAllowCancelJob || !m_inhibitServiceJobs ? new KisNoopDabStrategy(m_prefix + "cancel") : 0; } KisStrokeJobStrategy* createDabStrategy() override { return new KisMutatableDabStrategy(m_prefix + "dab", this); } KisStrokeStrategy* createLodClone(int levelOfDetail) override { return new KisTestingStrokeStrategy(*this, levelOfDetail); } class CancelData : public KisStrokeJobData { public: CancelData(int seqNo) : m_seqNo(seqNo) {} int seqNo() const { return m_seqNo; } private: int m_seqNo; }; KisStrokeJobData* createCancelData() override { return new CancelData(m_cancelSeqNo++); } private: QString m_prefix; bool m_inhibitServiceJobs; int m_forceAllowInitJob; bool m_forceAllowCancelJob; int m_cancelSeqNo; }; inline QString getJobName(KisStrokeJob *job) { KisNoopDabStrategy *pointer = dynamic_cast(job->testingGetDabStrategy()); KIS_ASSERT(pointer); return pointer->name(job->testingGetDabData()); } inline int cancelSeqNo(KisStrokeJob *job) { KisTestingStrokeStrategy::CancelData *pointer = dynamic_cast (job->testingGetDabData()); Q_ASSERT(pointer); return pointer->seqNo(); } #endif /* __SCHEDULER_UTILS_H */ diff --git a/libs/image/tiles3/kis_tile_hash_table2.h b/libs/image/tiles3/kis_tile_hash_table2.h index 906fda1959..37a4ec9020 100644 --- a/libs/image/tiles3/kis_tile_hash_table2.h +++ b/libs/image/tiles3/kis_tile_hash_table2.h @@ -1,482 +1,482 @@ /* * Copyright (c) 2018 Andrey Kamakin * * 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. */ #ifndef KIS_TILEHASHTABLE_2_H #define KIS_TILEHASHTABLE_2_H #include "kis_shared.h" #include "kis_shared_ptr.h" #include "3rdparty/lock_free_map/concurrent_map.h" #include "kis_tile.h" #include "kis_debug.h" #define SANITY_CHECK /** * This is a template for a hash table that stores tiles (or some other * objects resembling tiles). Actually, this object should only have * col()/row() methods and be able to answer notifyDetachedFromDataManager() requests to * be stored here. It is used in KisTiledDataManager and * KisMementoManager. * * How to use: * 1) each hash must be unique, otherwise tiles would rewrite each-other * 2) 0 key is reserved, so can't be used * 3) col and row must be less than 0x7FFF to guarantee uniqueness of hash for each pair */ template class KisTileHashTableIteratorTraits2; template class KisTileHashTableTraits2 { static constexpr bool isInherited = std::is_convertible::value; Q_STATIC_ASSERT_X(isInherited, "Template must inherit KisShared"); public: typedef T TileType; typedef KisSharedPtr TileTypeSP; typedef KisWeakSharedPtr TileTypeWSP; KisTileHashTableTraits2(KisMementoManager *mm); KisTileHashTableTraits2(const KisTileHashTableTraits2 &ht, KisMementoManager *mm); ~KisTileHashTableTraits2(); bool isEmpty() { return !m_numTiles.load(); } bool tileExists(qint32 col, qint32 row); /** * Returns a tile in position (col,row). If no tile exists, * returns null. * \param col column of the tile * \param row row of the tile */ TileTypeSP getExistingTile(qint32 col, qint32 row); /** * Returns a tile in position (col,row). If no tile exists, * creates a new one, attaches it to the list and returns. * \param col column of the tile * \param row row of the tile * \param newTile out-parameter, returns true if a new tile * was created */ TileTypeSP getTileLazy(qint32 col, qint32 row, bool& newTile); /** * Returns a tile in position (col,row). If no tile exists, * creates nothing, but returns shared default tile object * of the table. Be careful, this object has column and row * parameters set to (qint32_MIN, qint32_MIN). * \param col column of the tile * \param row row of the tile * \param existingTile returns true if the tile actually exists in the table * and it is not a lazily created default wrapper tile */ TileTypeSP getReadOnlyTileLazy(qint32 col, qint32 row, bool &existingTile); void addTile(TileTypeSP tile); bool deleteTile(TileTypeSP tile); bool deleteTile(qint32 col, qint32 row); void clear(); void setDefaultTileData(KisTileData *defaultTileData); KisTileData* defaultTileData(); qint32 numTiles() { return m_numTiles.load(); } void debugPrintInfo(); void debugMaxListLength(qint32 &min, qint32 &max); friend class KisTileHashTableIteratorTraits2; private: struct MemoryReclaimer { MemoryReclaimer(TileType *data) : d(data) {} void destroy() { TileTypeSP::deref(reinterpret_cast(this), d); delete this; } private: TileType *d; }; inline quint32 calculateHash(qint32 col, qint32 row) { #ifdef SANITY_CHECK - KIS_ASSERT_RECOVER_NOOP(row < 0x7FFF && col < 0x7FFF) + KIS_ASSERT_RECOVER_NOOP(row < 0x7FFF && col < 0x7FFF); #endif // SANITY_CHECK if (col == 0 && row == 0) { col = 0x7FFF; row = 0x7FFF; } return ((static_cast(row) << 16) | (static_cast(col) & 0xFFFF)); } inline void insert(quint32 idx, TileTypeSP item) { TileTypeSP::ref(&item, item.data()); TileType *tile = 0; { QReadLocker locker(&m_iteratorLock); m_map.getGC().lockRawPointerAccess(); tile = m_map.assign(idx, item.data()); } if (tile) { tile->notifyDeadWithoutDetaching(); m_map.getGC().enqueue(&MemoryReclaimer::destroy, new MemoryReclaimer(tile)); } else { m_numTiles.fetchAndAddRelaxed(1); } m_map.getGC().unlockRawPointerAccess(); m_map.getGC().update(); } inline bool erase(quint32 idx) { m_map.getGC().lockRawPointerAccess(); bool wasDeleted = false; TileType *tile = m_map.erase(idx); if (tile) { tile->notifyDetachedFromDataManager(); wasDeleted = true; m_numTiles.fetchAndSubRelaxed(1); m_map.getGC().enqueue(&MemoryReclaimer::destroy, new MemoryReclaimer(tile)); } m_map.getGC().unlockRawPointerAccess(); m_map.getGC().update(); return wasDeleted; } private: typedef ConcurrentMap LockFreeTileMap; typedef typename LockFreeTileMap::Mutator LockFreeTileMapMutator; mutable LockFreeTileMap m_map; /** * We still need something to guard changes in m_defaultTileData, * otherwise there will be concurrent read/writes, resulting in broken memory. */ QReadWriteLock m_defaultPixelDataLock; mutable QReadWriteLock m_iteratorLock; QAtomicInt m_numTiles; KisTileData *m_defaultTileData; KisMementoManager *m_mementoManager; }; template class KisTileHashTableIteratorTraits2 { public: typedef T TileType; typedef KisSharedPtr TileTypeSP; typedef typename ConcurrentMap::Iterator Iterator; KisTileHashTableIteratorTraits2(KisTileHashTableTraits2 *ht) : m_ht(ht) { m_ht->m_iteratorLock.lockForWrite(); m_iter.setMap(m_ht->m_map); } ~KisTileHashTableIteratorTraits2() { m_ht->m_iteratorLock.unlock(); } void next() { m_iter.next(); } TileTypeSP tile() const { return TileTypeSP(m_iter.getValue()); } bool isDone() const { return !m_iter.isValid(); } void deleteCurrent() { m_ht->erase(m_iter.getKey()); next(); } void moveCurrentToHashTable(KisTileHashTableTraits2 *newHashTable) { TileTypeSP tile = m_iter.getValue(); next(); quint32 idx = m_ht->calculateHash(tile->col(), tile->row()); m_ht->erase(idx); newHashTable->insert(idx, tile); } private: KisTileHashTableTraits2 *m_ht; Iterator m_iter; }; template KisTileHashTableTraits2::KisTileHashTableTraits2(KisMementoManager *mm) : m_numTiles(0), m_defaultTileData(0), m_mementoManager(mm) { } template KisTileHashTableTraits2::KisTileHashTableTraits2(const KisTileHashTableTraits2 &ht, KisMementoManager *mm) : KisTileHashTableTraits2(mm) { setDefaultTileData(ht.m_defaultTileData); QWriteLocker locker(&ht.m_iteratorLock); typename ConcurrentMap::Iterator iter(ht.m_map); while (iter.isValid()) { TileTypeSP tile = new TileType(*iter.getValue(), m_mementoManager); insert(iter.getKey(), tile); iter.next(); } } template KisTileHashTableTraits2::~KisTileHashTableTraits2() { clear(); setDefaultTileData(0); } template bool KisTileHashTableTraits2::tileExists(qint32 col, qint32 row) { return getExistingTile(col, row); } template typename KisTileHashTableTraits2::TileTypeSP KisTileHashTableTraits2::getExistingTile(qint32 col, qint32 row) { quint32 idx = calculateHash(col, row); m_map.getGC().lockRawPointerAccess(); TileTypeSP tile = m_map.get(idx); m_map.getGC().unlockRawPointerAccess(); m_map.getGC().update(); return tile; } template typename KisTileHashTableTraits2::TileTypeSP KisTileHashTableTraits2::getTileLazy(qint32 col, qint32 row, bool &newTile) { newTile = false; quint32 idx = calculateHash(col, row); // we are going to assign a raw-pointer tile from the table // to a shared pointer... m_map.getGC().lockRawPointerAccess(); TileTypeSP tile = m_map.get(idx); while (!tile) { // we shouldn't try to acquire **any** lock with // raw-pointer lock held m_map.getGC().unlockRawPointerAccess(); { QReadLocker locker(&m_defaultPixelDataLock); tile = new TileType(col, row, m_defaultTileData, 0); } TileTypeSP::ref(&tile, tile.data()); TileType *discardedTile = 0; // iterator lock should be taken **before** // the pointers are locked m_iteratorLock.lockForRead(); // and now lock raw-pointers again m_map.getGC().lockRawPointerAccess(); // mutator might have become invalidated when // we released raw pointers, so we need to reinitialize it LockFreeTileMapMutator mutator = m_map.insertOrFind(idx); if (!mutator.getValue()) { discardedTile = mutator.exchangeValue(tile.data()); } else { discardedTile = tile.data(); } m_iteratorLock.unlock(); if (discardedTile) { // we've got our tile back, it didn't manage to // get into the table. Now release the allocated // tile and push TO/GA switch. tile = 0; discardedTile->notifyDeadWithoutDetaching(); m_map.getGC().enqueue(&MemoryReclaimer::destroy, new MemoryReclaimer(discardedTile)); tile = m_map.get(idx); continue; } else { newTile = true; m_numTiles.fetchAndAddRelaxed(1); tile->notifyAttachedToDataManager(m_mementoManager); } } m_map.getGC().unlockRawPointerAccess(); m_map.getGC().update(); return tile; } template typename KisTileHashTableTraits2::TileTypeSP KisTileHashTableTraits2::getReadOnlyTileLazy(qint32 col, qint32 row, bool &existingTile) { quint32 idx = calculateHash(col, row); m_map.getGC().lockRawPointerAccess(); TileTypeSP tile = m_map.get(idx); m_map.getGC().unlockRawPointerAccess(); existingTile = tile; if (!existingTile) { QReadLocker locker(&m_defaultPixelDataLock); tile = new TileType(col, row, m_defaultTileData, 0); } m_map.getGC().update(); return tile; } template void KisTileHashTableTraits2::addTile(TileTypeSP tile) { quint32 idx = calculateHash(tile->col(), tile->row()); insert(idx, tile); } template bool KisTileHashTableTraits2::deleteTile(TileTypeSP tile) { return deleteTile(tile->col(), tile->row()); } template bool KisTileHashTableTraits2::deleteTile(qint32 col, qint32 row) { quint32 idx = calculateHash(col, row); return erase(idx); } template void KisTileHashTableTraits2::clear() { { QWriteLocker locker(&m_iteratorLock); typename ConcurrentMap::Iterator iter(m_map); TileType *tile = 0; while (iter.isValid()) { m_map.getGC().lockRawPointerAccess(); tile = m_map.erase(iter.getKey()); if (tile) { tile->notifyDetachedFromDataManager(); m_map.getGC().enqueue(&MemoryReclaimer::destroy, new MemoryReclaimer(tile)); } m_map.getGC().unlockRawPointerAccess(); iter.next(); } m_numTiles.store(0); } // garbage collection must **not** be run with locks held m_map.getGC().update(); } template inline void KisTileHashTableTraits2::setDefaultTileData(KisTileData *defaultTileData) { QWriteLocker locker(&m_defaultPixelDataLock); if (m_defaultTileData) { m_defaultTileData->release(); m_defaultTileData = 0; } if (defaultTileData) { defaultTileData->acquire(); m_defaultTileData = defaultTileData; } } template inline KisTileData* KisTileHashTableTraits2::defaultTileData() { QReadLocker locker(&m_defaultPixelDataLock); return m_defaultTileData; } template void KisTileHashTableTraits2::debugPrintInfo() { } template void KisTileHashTableTraits2::debugMaxListLength(qint32 &/*min*/, qint32 &/*max*/) { } typedef KisTileHashTableTraits2 KisTileHashTable; typedef KisTileHashTableIteratorTraits2 KisTileHashTableIterator; typedef KisTileHashTableIteratorTraits2 KisTileHashTableConstIterator; #endif // KIS_TILEHASHTABLE_2_H diff --git a/libs/ui/KisCloneDocumentStroke.cpp b/libs/ui/KisCloneDocumentStroke.cpp index 17071af1dc..5df3b3684f 100644 --- a/libs/ui/KisCloneDocumentStroke.cpp +++ b/libs/ui/KisCloneDocumentStroke.cpp @@ -1,60 +1,60 @@ /* * Copyright (c) 2018 Dmitry Kazakov * * 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 "KisCloneDocumentStroke.h" #include "KisDocument.h" #include "kis_layer_utils.h" #include struct KRITAIMAGE_NO_EXPORT KisCloneDocumentStroke::Private { Private(KisDocument *_document) : document(_document) { } KisDocument *document = 0; }; KisCloneDocumentStroke::KisCloneDocumentStroke(KisDocument *document) - : KisSimpleStrokeStrategy("clone-document-stroke", kundo2_i18n("Clone Document")), + : KisSimpleStrokeStrategy(QLatin1String("clone-document-stroke"), kundo2_i18n("Clone Document")), m_d(new Private(document)) { setClearsRedoOnStart(false); setRequestsOtherStrokesToEnd(false); enableJob(JOB_INIT, true, KisStrokeJobData::BARRIER, KisStrokeJobData::EXCLUSIVE); enableJob(JOB_FINISH, true, KisStrokeJobData::BARRIER, KisStrokeJobData::EXCLUSIVE); } KisCloneDocumentStroke::~KisCloneDocumentStroke() { } void KisCloneDocumentStroke::initStrokeCallback() { KisLayerUtils::forceAllDelayedNodesUpdate(m_d->document->image()->root()); } void KisCloneDocumentStroke::finishStrokeCallback() { KisDocument *doc = m_d->document->clone(); doc->moveToThread(qApp->thread()); emit sigDocumentCloned(doc); } diff --git a/libs/ui/KisDocument.cpp b/libs/ui/KisDocument.cpp index 8d58b71c90..6f9bbb71da 100644 --- a/libs/ui/KisDocument.cpp +++ b/libs/ui/KisDocument.cpp @@ -1,2259 +1,2266 @@ /* This file is part of the Krita project * * Copyright (C) 2014 Boudewijn Rempt * * 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 "KisMainWindow.h" // XXX: remove #include // XXX: remove #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // Krita Image #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kis_layer_utils.h" +#include "kis_selection_mask.h" // Local #include "KisViewManager.h" #include "kis_clipboard.h" #include "widgets/kis_custom_image_widget.h" #include "canvas/kis_canvas2.h" #include "flake/kis_shape_controller.h" #include "kis_statusbar.h" #include "widgets/kis_progress_widget.h" #include "kis_canvas_resource_provider.h" #include "KisResourceServerProvider.h" #include "kis_node_manager.h" #include "KisPart.h" #include "KisApplication.h" #include "KisDocument.h" #include "KisImportExportManager.h" #include "KisView.h" #include "kis_grid_config.h" #include "kis_guides_config.h" #include "kis_image_barrier_lock_adapter.h" #include "KisReferenceImagesLayer.h" #include #include "kis_config_notifier.h" #include "kis_async_action_feedback.h" #include "KisCloneDocumentStroke.h" #include #include #include "kis_simple_stroke_strategy.h" // Define the protocol used here for embedded documents' URL // This used to "store" but QUrl didn't like it, // so let's simply make it "tar" ! #define STORE_PROTOCOL "tar" // The internal path is a hack to make QUrl happy and for document children #define INTERNAL_PROTOCOL "intern" #define INTERNAL_PREFIX "intern:/" // Warning, keep it sync in koStore.cc #include using namespace std; namespace { constexpr int errorMessageTimeout = 5000; constexpr int successMessageTimeout = 1000; } /********************************************************** * * KisDocument * **********************************************************/ //static QString KisDocument::newObjectName() { static int s_docIFNumber = 0; QString name; name.setNum(s_docIFNumber++); name.prepend("document_"); return name; } class UndoStack : public KUndo2Stack { public: UndoStack(KisDocument *doc) : KUndo2Stack(doc), m_doc(doc) { } void setIndex(int idx) override { KisImageWSP image = this->image(); image->requestStrokeCancellation(); if(image->tryBarrierLock()) { KUndo2Stack::setIndex(idx); image->unlock(); } } void notifySetIndexChangedOneCommand() override { KisImageWSP image = this->image(); image->unlock(); /** * Some very weird commands may emit blocking signals to * the GUI (e.g. KisGuiContextCommand). Here is the best thing * we can do to avoid the deadlock */ while(!image->tryBarrierLock()) { QApplication::processEvents(); } } void undo() override { KisImageWSP image = this->image(); image->requestUndoDuringStroke(); if (image->tryUndoUnfinishedLod0Stroke() == UNDO_OK) { return; } if(image->tryBarrierLock()) { KUndo2Stack::undo(); image->unlock(); } } void redo() override { KisImageWSP image = this->image(); if(image->tryBarrierLock()) { KUndo2Stack::redo(); image->unlock(); } } private: KisImageWSP image() { KisImageWSP currentImage = m_doc->image(); Q_ASSERT(currentImage); return currentImage; } private: KisDocument *m_doc; }; class Q_DECL_HIDDEN KisDocument::Private { public: Private(KisDocument *_q) : q(_q) , docInfo(new KoDocumentInfo(_q)) // deleted by QObject , importExportManager(new KisImportExportManager(_q)) // deleted manually , autoSaveTimer(new QTimer(_q)) , undoStack(new UndoStack(_q)) // deleted by QObject , m_bAutoDetectedMime(false) , modified(false) , readwrite(true) , firstMod(QDateTime::currentDateTime()) , lastMod(firstMod) , nserver(new KisNameServer(1)) , imageIdleWatcher(2000 /*ms*/) , globalAssistantsColor(KisConfig(true).defaultAssistantsColor()) , savingLock(&savingMutex) , batchMode(false) { if (QLocale().measurementSystem() == QLocale::ImperialSystem) { unit = KoUnit::Inch; } else { unit = KoUnit::Centimeter; } } Private(const Private &rhs, KisDocument *_q) : q(_q) , docInfo(new KoDocumentInfo(*rhs.docInfo, _q)) , importExportManager(new KisImportExportManager(_q)) , autoSaveTimer(new QTimer(_q)) , undoStack(new UndoStack(_q)) , nserver(new KisNameServer(*rhs.nserver)) , preActivatedNode(0) // the node is from another hierarchy! , imageIdleWatcher(2000 /*ms*/) , savingLock(&savingMutex) { copyFromImpl(rhs, _q, CONSTRUCT); } ~Private() { // Don't delete m_d->shapeController because it's in a QObject hierarchy. delete nserver; } KisDocument *q = 0; KoDocumentInfo *docInfo = 0; KoUnit unit; KisImportExportManager *importExportManager = 0; // The filter-manager to use when loading/saving [for the options] QByteArray mimeType; // The actual mimetype of the document QByteArray outputMimeType; // The mimetype to use when saving QTimer *autoSaveTimer; QString lastErrorMessage; // see openFile() QString lastWarningMessage; int autoSaveDelay = 300; // in seconds, 0 to disable. bool modifiedAfterAutosave = false; bool isAutosaving = false; bool disregardAutosaveFailure = false; int autoSaveFailureCount = 0; KUndo2Stack *undoStack = 0; KisGuidesConfig guidesConfig; KisMirrorAxisConfig mirrorAxisConfig; bool m_bAutoDetectedMime = false; // whether the mimetype in the arguments was detected by the part itself QUrl m_url; // local url - the one displayed to the user. QString m_file; // Local file - the only one the part implementation should deal with. QMutex savingMutex; bool modified = false; bool readwrite = false; QDateTime firstMod; QDateTime lastMod; KisNameServer *nserver; KisImageSP image; KisImageSP savingImage; KisNodeWSP preActivatedNode; KisShapeController* shapeController = 0; KoShapeController* koShapeController = 0; KisIdleWatcher imageIdleWatcher; QScopedPointer imageIdleConnection; QList assistants; QColor globalAssistantsColor; QList paletteList; bool ownsPaletteList = false; KisGridConfig gridConfig; StdLockableWrapper savingLock; bool modifiedWhileSaving = false; QScopedPointer backgroundSaveDocument; QPointer savingUpdater; QFuture childSavingFuture; KritaUtils::ExportFileJob backgroundSaveJob; bool isRecovered = false; bool batchMode { false }; void syncDecorationsWrapperLayerState(); void setImageAndInitIdleWatcher(KisImageSP _image) { image = _image; imageIdleWatcher.setTrackedImage(image); if (image) { imageIdleConnection.reset( new KisSignalAutoConnection( &imageIdleWatcher, SIGNAL(startedIdleMode()), image.data(), SLOT(explicitRegenerateLevelOfDetail()))); } } void copyFrom(const Private &rhs, KisDocument *q); void copyFromImpl(const Private &rhs, KisDocument *q, KisDocument::CopyPolicy policy); /// clones the palette list oldList /// the ownership of the returned KoColorSet * belongs to the caller QList clonePaletteList(const QList &oldList); class StrippedSafeSavingLocker; }; void KisDocument::Private::syncDecorationsWrapperLayerState() { if (!this->image) return; KisImageSP image = this->image; KisDecorationsWrapperLayerSP decorationsLayer = KisLayerUtils::findNodeByType(image->root()); const bool needsDecorationsWrapper = gridConfig.showGrid() || (guidesConfig.showGuides() && guidesConfig.hasGuides()) || !assistants.isEmpty(); struct SyncDecorationsWrapperStroke : public KisSimpleStrokeStrategy { SyncDecorationsWrapperStroke(KisDocument *document, bool needsDecorationsWrapper) - : KisSimpleStrokeStrategy("sync-decorations-wrapper", kundo2_noi18n("start-isolated-mode")), + : KisSimpleStrokeStrategy(QLatin1String("sync-decorations-wrapper"), + kundo2_noi18n("start-isolated-mode")), m_document(document), m_needsDecorationsWrapper(needsDecorationsWrapper) { this->enableJob(JOB_INIT, true, KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::EXCLUSIVE); setClearsRedoOnStart(false); } void initStrokeCallback() { KisDecorationsWrapperLayerSP decorationsLayer = KisLayerUtils::findNodeByType(m_document->image()->root()); if (m_needsDecorationsWrapper && !decorationsLayer) { m_document->image()->addNode(new KisDecorationsWrapperLayer(m_document)); } else if (!m_needsDecorationsWrapper && decorationsLayer) { m_document->image()->removeNode(decorationsLayer); } } private: KisDocument *m_document = 0; bool m_needsDecorationsWrapper = false; }; KisStrokeId id = image->startStroke(new SyncDecorationsWrapperStroke(q, needsDecorationsWrapper)); image->endStroke(id); } void KisDocument::Private::copyFrom(const Private &rhs, KisDocument *q) { copyFromImpl(rhs, q, KisDocument::REPLACE); } void KisDocument::Private::copyFromImpl(const Private &rhs, KisDocument *q, KisDocument::CopyPolicy policy) { if (policy == REPLACE) { delete docInfo; } docInfo = (new KoDocumentInfo(*rhs.docInfo, q)); unit = rhs.unit; mimeType = rhs.mimeType; outputMimeType = rhs.outputMimeType; if (policy == REPLACE) { q->setGuidesConfig(rhs.guidesConfig); q->setMirrorAxisConfig(rhs.mirrorAxisConfig); q->setModified(rhs.modified); q->setAssistants(KisPaintingAssistant::cloneAssistantList(rhs.assistants)); q->setGridConfig(rhs.gridConfig); } else { // in CONSTRUCT mode, we cannot use the functions of KisDocument // because KisDocument does not yet have a pointer to us. guidesConfig = rhs.guidesConfig; mirrorAxisConfig = rhs.mirrorAxisConfig; modified = rhs.modified; assistants = KisPaintingAssistant::cloneAssistantList(rhs.assistants); gridConfig = rhs.gridConfig; } m_bAutoDetectedMime = rhs.m_bAutoDetectedMime; m_url = rhs.m_url; m_file = rhs.m_file; readwrite = rhs.readwrite; firstMod = rhs.firstMod; lastMod = rhs.lastMod; // XXX: the display properties will be shared between different snapshots globalAssistantsColor = rhs.globalAssistantsColor; if (policy == REPLACE) { QList newPaletteList = clonePaletteList(rhs.paletteList); q->setPaletteList(newPaletteList, /* emitSignal = */ true); // we still do not own palettes if we did not } else { paletteList = rhs.paletteList; } batchMode = rhs.batchMode; } QList KisDocument::Private::clonePaletteList(const QList &oldList) { QList newList; Q_FOREACH (KoColorSet *palette, oldList) { newList << new KoColorSet(*palette); } return newList; } class KisDocument::Private::StrippedSafeSavingLocker { public: StrippedSafeSavingLocker(QMutex *savingMutex, KisImageSP image) : m_locked(false) , m_image(image) , m_savingLock(savingMutex) , m_imageLock(image, true) { /** * Initial try to lock both objects. Locking the image guards * us from any image composition threads running in the * background, while savingMutex guards us from entering the * saving code twice by autosave and main threads. * * Since we are trying to lock multiple objects, so we should * do it in a safe manner. */ m_locked = std::try_lock(m_imageLock, m_savingLock) < 0; if (!m_locked) { m_image->requestStrokeEnd(); QApplication::processEvents(QEventLoop::ExcludeUserInputEvents); // one more try... m_locked = std::try_lock(m_imageLock, m_savingLock) < 0; } } ~StrippedSafeSavingLocker() { if (m_locked) { m_imageLock.unlock(); m_savingLock.unlock(); } } bool successfullyLocked() const { return m_locked; } private: bool m_locked; KisImageSP m_image; StdLockableWrapper m_savingLock; KisImageBarrierLockAdapter m_imageLock; }; KisDocument::KisDocument() : d(new Private(this)) { connect(KisConfigNotifier::instance(), SIGNAL(configChanged()), SLOT(slotConfigChanged())); connect(d->undoStack, SIGNAL(cleanChanged(bool)), this, SLOT(slotUndoStackCleanChanged(bool))); connect(d->autoSaveTimer, SIGNAL(timeout()), this, SLOT(slotAutoSave())); setObjectName(newObjectName()); // preload the krita resources KisResourceServerProvider::instance(); d->shapeController = new KisShapeController(this, d->nserver); d->koShapeController = new KoShapeController(0, d->shapeController); d->shapeController->resourceManager()->setGlobalShapeController(d->koShapeController); slotConfigChanged(); } KisDocument::KisDocument(const KisDocument &rhs) : QObject(), d(new Private(*rhs.d, this)) { copyFromDocumentImpl(rhs, CONSTRUCT); } KisDocument::~KisDocument() { // wait until all the pending operations are in progress waitForSavingToComplete(); /** * Push a timebomb, which will try to release the memory after * the document has been deleted */ KisPaintDevice::createMemoryReleaseObject()->deleteLater(); d->autoSaveTimer->disconnect(this); d->autoSaveTimer->stop(); delete d->importExportManager; // Despite being QObject they needs to be deleted before the image delete d->shapeController; delete d->koShapeController; if (d->image) { d->image->notifyAboutToBeDeleted(); /** * WARNING: We should wait for all the internal image jobs to * finish before entering KisImage's destructor. The problem is, * while execution of KisImage::~KisImage, all the weak shared * pointers pointing to the image enter an inconsistent * state(!). The shared counter is already zero and destruction * has started, but the weak reference doesn't know about it, * because KisShared::~KisShared hasn't been executed yet. So all * the threads running in background and having weak pointers will * enter the KisImage's destructor as well. */ d->image->requestStrokeCancellation(); d->image->waitForDone(); // clear undo commands that can still point to the image d->undoStack->clear(); d->image->waitForDone(); KisImageWSP sanityCheckPointer = d->image; Q_UNUSED(sanityCheckPointer); // The following line trigger the deletion of the image d->image.clear(); // check if the image has actually been deleted KIS_SAFE_ASSERT_RECOVER_NOOP(!sanityCheckPointer.isValid()); } if (d->ownsPaletteList) { qDeleteAll(d->paletteList); } delete d; } bool KisDocument::reload() { // XXX: reimplement! return false; } KisDocument *KisDocument::clone() { return new KisDocument(*this); } bool KisDocument::exportDocumentImpl(const KritaUtils::ExportFileJob &job, KisPropertiesConfigurationSP exportConfiguration) { QFileInfo filePathInfo(job.filePath); if (filePathInfo.exists() && !filePathInfo.isWritable()) { slotCompleteSavingDocument(job, ImportExportCodes::NoAccessToWrite, i18n("%1 cannot be written to. Please save under a different name.", job.filePath)); //return ImportExportCodes::NoAccessToWrite; return false; } KisConfig cfg(true); if (cfg.backupFile() && filePathInfo.exists()) { QString backupDir; switch(cfg.readEntry("backupfilelocation", 0)) { case 1: backupDir = QStandardPaths::writableLocation(QStandardPaths::HomeLocation); break; case 2: backupDir = QStandardPaths::writableLocation(QStandardPaths::TempLocation); break; default: // Do nothing: the empty string is user file location break; } int numOfBackupsKept = cfg.readEntry("numberofbackupfiles", 1); QString suffix = cfg.readEntry("backupfilesuffix", "~"); if (numOfBackupsKept == 1) { if (!KBackup::simpleBackupFile(job.filePath, backupDir, suffix)) { qWarning() << "Failed to create simple backup file!" << job.filePath << backupDir << suffix; KisUsageLogger::log(QString("Failed to create a simple backup for %1 in %2.").arg(job.filePath).arg(backupDir.isEmpty() ? "the same location as the file" : backupDir)); return false; } else { KisUsageLogger::log(QString("Create a simple backup for %1 in %2.").arg(job.filePath).arg(backupDir.isEmpty() ? "the same location as the file" : backupDir)); } } else if (numOfBackupsKept > 2) { if (!KBackup::numberedBackupFile(job.filePath, backupDir, suffix, numOfBackupsKept)) { qWarning() << "Failed to create numbered backup file!" << job.filePath << backupDir << suffix; KisUsageLogger::log(QString("Failed to create a numbered backup for %2.").arg(job.filePath).arg(backupDir.isEmpty() ? "the same location as the file" : backupDir)); return false; } else { KisUsageLogger::log(QString("Create a simple backup for %1 in %2.").arg(job.filePath).arg(backupDir.isEmpty() ? "the same location as the file" : backupDir)); } } } //KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(!job.mimeType.isEmpty(), false); if (job.mimeType.isEmpty()) { KisImportExportErrorCode error = ImportExportCodes::FileFormatIncorrect; slotCompleteSavingDocument(job, error, error.errorMessage()); return false; } const QString actionName = job.flags & KritaUtils::SaveIsExporting ? i18n("Exporting Document...") : i18n("Saving Document..."); bool started = initiateSavingInBackground(actionName, this, SLOT(slotCompleteSavingDocument(KritaUtils::ExportFileJob, KisImportExportErrorCode ,QString)), job, exportConfiguration); if (!started) { emit canceled(QString()); } return started; } bool KisDocument::exportDocument(const QUrl &url, const QByteArray &mimeType, bool showWarnings, KisPropertiesConfigurationSP exportConfiguration) { using namespace KritaUtils; SaveFlags flags = SaveIsExporting; if (showWarnings) { flags |= SaveShowWarnings; } KisUsageLogger::log(QString("Exporting Document: %1 as %2. %3 * %4 pixels, %5 layers, %6 frames, %7 framerate. Export configuration: %8") .arg(url.toLocalFile()) .arg(QString::fromLatin1(mimeType)) .arg(d->image->width()) .arg(d->image->height()) .arg(d->image->nlayers()) .arg(d->image->animationInterface()->totalLength()) .arg(d->image->animationInterface()->framerate()) .arg(exportConfiguration ? exportConfiguration->toXML() : "No configuration")); return exportDocumentImpl(KritaUtils::ExportFileJob(url.toLocalFile(), mimeType, flags), exportConfiguration); } bool KisDocument::saveAs(const QUrl &_url, const QByteArray &mimeType, bool showWarnings, KisPropertiesConfigurationSP exportConfiguration) { using namespace KritaUtils; KisUsageLogger::log(QString("Saving Document %9 as %1 (mime: %2). %3 * %4 pixels, %5 layers. %6 frames, %7 framerate. Export configuration: %8") .arg(_url.toLocalFile()) .arg(QString::fromLatin1(mimeType)) .arg(d->image->width()) .arg(d->image->height()) .arg(d->image->nlayers()) .arg(d->image->animationInterface()->totalLength()) .arg(d->image->animationInterface()->framerate()) .arg(exportConfiguration ? exportConfiguration->toXML() : "No configuration") .arg(url().toLocalFile())); return exportDocumentImpl(ExportFileJob(_url.toLocalFile(), mimeType, showWarnings ? SaveShowWarnings : SaveNone), exportConfiguration); } bool KisDocument::save(bool showWarnings, KisPropertiesConfigurationSP exportConfiguration) { return saveAs(url(), mimeType(), showWarnings, exportConfiguration); } QByteArray KisDocument::serializeToNativeByteArray() { QByteArray byteArray; QBuffer buffer(&byteArray); QScopedPointer filter(KisImportExportManager::filterForMimeType(nativeFormatMimeType(), KisImportExportManager::Export)); filter->setBatchMode(true); filter->setMimeType(nativeFormatMimeType()); Private::StrippedSafeSavingLocker locker(&d->savingMutex, d->image); if (!locker.successfullyLocked()) { return byteArray; } d->savingImage = d->image; if (!filter->convert(this, &buffer).isOk()) { qWarning() << "serializeToByteArray():: Could not export to our native format"; } return byteArray; } void KisDocument::slotCompleteSavingDocument(const KritaUtils::ExportFileJob &job, KisImportExportErrorCode status, const QString &errorMessage) { if (status.isCancelled()) return; const QString fileName = QFileInfo(job.filePath).fileName(); if (!status.isOk()) { emit statusBarMessage(i18nc("%1 --- failing file name, %2 --- error message", "Error during saving %1: %2", fileName, exportErrorToUserMessage(status, errorMessage)), errorMessageTimeout); if (!fileBatchMode()) { const QString filePath = job.filePath; QMessageBox::critical(0, i18nc("@title:window", "Krita"), i18n("Could not save %1\nReason: %2", filePath, exportErrorToUserMessage(status, errorMessage))); } } else { if (!(job.flags & KritaUtils::SaveIsExporting)) { const QString existingAutoSaveBaseName = localFilePath(); const bool wasRecovered = isRecovered(); setUrl(QUrl::fromLocalFile(job.filePath)); setLocalFilePath(job.filePath); setMimeType(job.mimeType); updateEditingTime(true); if (!d->modifiedWhileSaving) { /** * If undo stack is already clean/empty, it doesn't emit any * signals, so we might forget update document modified state * (which was set, e.g. while recovering an autosave file) */ if (d->undoStack->isClean()) { setModified(false); } else { d->undoStack->setClean(); } } setRecovered(false); removeAutoSaveFiles(existingAutoSaveBaseName, wasRecovered); } emit completed(); emit sigSavingFinished(); emit statusBarMessage(i18n("Finished saving %1", fileName), successMessageTimeout); } } QByteArray KisDocument::mimeType() const { return d->mimeType; } void KisDocument::setMimeType(const QByteArray & mimeType) { d->mimeType = mimeType; } bool KisDocument::fileBatchMode() const { return d->batchMode; } void KisDocument::setFileBatchMode(const bool batchMode) { d->batchMode = batchMode; } KisDocument* KisDocument::lockAndCloneForSaving() { // force update of all the asynchronous nodes before cloning QApplication::processEvents(QEventLoop::ExcludeUserInputEvents); KisLayerUtils::forceAllDelayedNodesUpdate(d->image->root()); KisMainWindow *window = KisPart::instance()->currentMainwindow(); if (window) { if (window->viewManager()) { if (!window->viewManager()->blockUntilOperationsFinished(d->image)) { return 0; } } } Private::StrippedSafeSavingLocker locker(&d->savingMutex, d->image); if (!locker.successfullyLocked()) { return 0; } return new KisDocument(*this); } KisDocument *KisDocument::lockAndCreateSnapshot() { KisDocument *doc = lockAndCloneForSaving(); if (doc) { // clone palette list doc->d->paletteList = doc->d->clonePaletteList(doc->d->paletteList); doc->d->ownsPaletteList = true; } return doc; } void KisDocument::copyFromDocument(const KisDocument &rhs) { copyFromDocumentImpl(rhs, REPLACE); } void KisDocument::copyFromDocumentImpl(const KisDocument &rhs, CopyPolicy policy) { if (policy == REPLACE) { d->copyFrom(*(rhs.d), this); d->undoStack->clear(); } else { // in CONSTRUCT mode, d should be already initialized connect(KisConfigNotifier::instance(), SIGNAL(configChanged()), SLOT(slotConfigChanged())); connect(d->undoStack, SIGNAL(cleanChanged(bool)), this, SLOT(slotUndoStackCleanChanged(bool))); connect(d->autoSaveTimer, SIGNAL(timeout()), this, SLOT(slotAutoSave())); d->shapeController = new KisShapeController(this, d->nserver); d->koShapeController = new KoShapeController(0, d->shapeController); d->shapeController->resourceManager()->setGlobalShapeController(d->koShapeController); } setObjectName(rhs.objectName()); slotConfigChanged(); if (rhs.d->image) { if (policy == REPLACE) { d->image->barrierLock(/* readOnly = */ false); rhs.d->image->barrierLock(/* readOnly = */ true); d->image->copyFromImage(*(rhs.d->image)); d->image->unlock(); rhs.d->image->unlock(); setCurrentImage(d->image, /* forceInitialUpdate = */ true); } else { // clone the image with keeping the GUIDs of the layers intact // NOTE: we expect the image to be locked! setCurrentImage(rhs.image()->clone(/* exactCopy = */ true), /* forceInitialUpdate = */ false); } } if (rhs.d->preActivatedNode) { QQueue linearizedNodes; KisLayerUtils::recursiveApplyNodes(rhs.d->image->root(), [&linearizedNodes](KisNodeSP node) { linearizedNodes.enqueue(node); }); KisLayerUtils::recursiveApplyNodes(d->image->root(), [&linearizedNodes, &rhs, this](KisNodeSP node) { KisNodeSP refNode = linearizedNodes.dequeue(); if (rhs.d->preActivatedNode.data() == refNode.data()) { d->preActivatedNode = node; } }); } // reinitialize references' signal connection KisReferenceImagesLayerSP referencesLayer = this->referenceImagesLayer(); setReferenceImagesLayer(referencesLayer, false); KisDecorationsWrapperLayerSP decorationsLayer = KisLayerUtils::findNodeByType(d->image->root()); if (decorationsLayer) { decorationsLayer->setDocument(this); } if (policy == REPLACE) { setModified(true); } } bool KisDocument::exportDocumentSync(const QUrl &url, const QByteArray &mimeType, KisPropertiesConfigurationSP exportConfiguration) { { /** * The caller guarantees that no one else uses the document (usually, * it is a temporary document created specifically for exporting), so * we don't need to copy or lock the document. Instead we should just * ensure the barrier lock is synced and then released. */ Private::StrippedSafeSavingLocker locker(&d->savingMutex, d->image); if (!locker.successfullyLocked()) { return false; } } d->savingImage = d->image; const QString fileName = url.toLocalFile(); KisImportExportErrorCode status = d->importExportManager-> exportDocument(fileName, fileName, mimeType, false, exportConfiguration); d->savingImage = 0; return status.isOk(); } bool KisDocument::initiateSavingInBackground(const QString actionName, const QObject *receiverObject, const char *receiverMethod, const KritaUtils::ExportFileJob &job, KisPropertiesConfigurationSP exportConfiguration) { return initiateSavingInBackground(actionName, receiverObject, receiverMethod, job, exportConfiguration, std::unique_ptr()); } bool KisDocument::initiateSavingInBackground(const QString actionName, const QObject *receiverObject, const char *receiverMethod, const KritaUtils::ExportFileJob &job, KisPropertiesConfigurationSP exportConfiguration, std::unique_ptr &&optionalClonedDocument) { KIS_ASSERT_RECOVER_RETURN_VALUE(job.isValid(), false); QScopedPointer clonedDocument; if (!optionalClonedDocument) { clonedDocument.reset(lockAndCloneForSaving()); } else { clonedDocument.reset(optionalClonedDocument.release()); } // we block saving until the current saving is finished! if (!clonedDocument || !d->savingMutex.tryLock()) { return false; } auto waitForImage = [] (KisImageSP image) { KisMainWindow *window = KisPart::instance()->currentMainwindow(); if (window) { if (window->viewManager()) { window->viewManager()->blockUntilOperationsFinishedForced(image); } } }; { KisNodeSP newRoot = clonedDocument->image()->root(); KIS_SAFE_ASSERT_RECOVER(!KisLayerUtils::hasDelayedNodeWithUpdates(newRoot)) { KisLayerUtils::forceAllDelayedNodesUpdate(newRoot); waitForImage(clonedDocument->image()); } } + if (clonedDocument->image()->hasOverlaySelectionMask()) { + clonedDocument->image()->setOverlaySelectionMask(0); + waitForImage(clonedDocument->image()); + } + KIS_SAFE_ASSERT_RECOVER(clonedDocument->image()->isIdle()) { waitForImage(clonedDocument->image()); } KIS_ASSERT_RECOVER_RETURN_VALUE(!d->backgroundSaveDocument, false); KIS_ASSERT_RECOVER_RETURN_VALUE(!d->backgroundSaveJob.isValid(), false); d->backgroundSaveDocument.reset(clonedDocument.take()); d->backgroundSaveJob = job; d->modifiedWhileSaving = false; if (d->backgroundSaveJob.flags & KritaUtils::SaveInAutosaveMode) { d->backgroundSaveDocument->d->isAutosaving = true; } connect(d->backgroundSaveDocument.data(), SIGNAL(sigBackgroundSavingFinished(KisImportExportErrorCode, QString)), this, SLOT(slotChildCompletedSavingInBackground(KisImportExportErrorCode, QString))); connect(this, SIGNAL(sigCompleteBackgroundSaving(KritaUtils::ExportFileJob, KisImportExportErrorCode, QString)), receiverObject, receiverMethod, Qt::UniqueConnection); bool started = d->backgroundSaveDocument->startExportInBackground(actionName, job.filePath, job.filePath, job.mimeType, job.flags & KritaUtils::SaveShowWarnings, exportConfiguration); if (!started) { // the state should have been deinitialized in slotChildCompletedSavingInBackground() KIS_SAFE_ASSERT_RECOVER (!d->backgroundSaveDocument && !d->backgroundSaveJob.isValid()) { d->backgroundSaveDocument.take()->deleteLater(); d->savingMutex.unlock(); d->backgroundSaveJob = KritaUtils::ExportFileJob(); } } return started; } void KisDocument::slotChildCompletedSavingInBackground(KisImportExportErrorCode status, const QString &errorMessage) { KIS_ASSERT_RECOVER_RETURN(isSaving()); KIS_ASSERT_RECOVER(d->backgroundSaveDocument) { d->savingMutex.unlock(); return; } if (d->backgroundSaveJob.flags & KritaUtils::SaveInAutosaveMode) { d->backgroundSaveDocument->d->isAutosaving = false; } d->backgroundSaveDocument.take()->deleteLater(); KIS_ASSERT_RECOVER(d->backgroundSaveJob.isValid()) { d->savingMutex.unlock(); return; } const KritaUtils::ExportFileJob job = d->backgroundSaveJob; d->backgroundSaveJob = KritaUtils::ExportFileJob(); // unlock at the very end d->savingMutex.unlock(); QFileInfo fi(job.filePath); KisUsageLogger::log(QString("Completed saving %1 (mime: %2). Result: %3. Size: %4. MD5 Hash: %5") .arg(job.filePath) .arg(QString::fromLatin1(job.mimeType)) .arg(!status.isOk() ? exportErrorToUserMessage(status, errorMessage) : "OK") .arg(fi.size()) .arg(QString::fromLatin1(KoMD5Generator().generateHash(job.filePath).toHex()))); emit sigCompleteBackgroundSaving(job, status, errorMessage); } void KisDocument::slotAutoSaveImpl(std::unique_ptr &&optionalClonedDocument) { if (!d->modified || !d->modifiedAfterAutosave) return; const QString autoSaveFileName = generateAutoSaveFileName(localFilePath()); emit statusBarMessage(i18n("Autosaving... %1", autoSaveFileName), successMessageTimeout); KisUsageLogger::log(QString("Autosaving: %1").arg(autoSaveFileName)); const bool hadClonedDocument = bool(optionalClonedDocument); bool started = false; if (d->image->isIdle() || hadClonedDocument) { started = initiateSavingInBackground(i18n("Autosaving..."), this, SLOT(slotCompleteAutoSaving(KritaUtils::ExportFileJob, KisImportExportErrorCode, QString)), KritaUtils::ExportFileJob(autoSaveFileName, nativeFormatMimeType(), KritaUtils::SaveIsExporting | KritaUtils::SaveInAutosaveMode), 0, std::move(optionalClonedDocument)); } else { emit statusBarMessage(i18n("Autosaving postponed: document is busy..."), errorMessageTimeout); } if (!started && !hadClonedDocument && d->autoSaveFailureCount >= 3) { KisCloneDocumentStroke *stroke = new KisCloneDocumentStroke(this); connect(stroke, SIGNAL(sigDocumentCloned(KisDocument*)), this, SLOT(slotInitiateAsyncAutosaving(KisDocument*)), Qt::BlockingQueuedConnection); KisStrokeId strokeId = d->image->startStroke(stroke); d->image->endStroke(strokeId); setInfiniteAutoSaveInterval(); } else if (!started) { setEmergencyAutoSaveInterval(); } else { d->modifiedAfterAutosave = false; } } void KisDocument::slotAutoSave() { slotAutoSaveImpl(std::unique_ptr()); } void KisDocument::slotInitiateAsyncAutosaving(KisDocument *clonedDocument) { slotAutoSaveImpl(std::unique_ptr(clonedDocument)); } void KisDocument::slotCompleteAutoSaving(const KritaUtils::ExportFileJob &job, KisImportExportErrorCode status, const QString &errorMessage) { Q_UNUSED(job); const QString fileName = QFileInfo(job.filePath).fileName(); if (!status.isOk()) { setEmergencyAutoSaveInterval(); emit statusBarMessage(i18nc("%1 --- failing file name, %2 --- error message", "Error during autosaving %1: %2", fileName, exportErrorToUserMessage(status, errorMessage)), errorMessageTimeout); } else { KisConfig cfg(true); d->autoSaveDelay = cfg.autoSaveInterval(); if (!d->modifiedWhileSaving) { d->autoSaveTimer->stop(); // until the next change d->autoSaveFailureCount = 0; } else { setNormalAutoSaveInterval(); } emit statusBarMessage(i18n("Finished autosaving %1", fileName), successMessageTimeout); } } bool KisDocument::startExportInBackground(const QString &actionName, const QString &location, const QString &realLocation, const QByteArray &mimeType, bool showWarnings, KisPropertiesConfigurationSP exportConfiguration) { d->savingImage = d->image; KisMainWindow *window = KisPart::instance()->currentMainwindow(); if (window) { if (window->viewManager()) { d->savingUpdater = window->viewManager()->createThreadedUpdater(actionName); d->importExportManager->setUpdater(d->savingUpdater); } } KisImportExportErrorCode initializationStatus(ImportExportCodes::OK); d->childSavingFuture = d->importExportManager->exportDocumentAsyc(location, realLocation, mimeType, initializationStatus, showWarnings, exportConfiguration); if (!initializationStatus.isOk()) { if (d->savingUpdater) { d->savingUpdater->cancel(); } d->savingImage.clear(); emit sigBackgroundSavingFinished(initializationStatus, initializationStatus.errorMessage()); return false; } typedef QFutureWatcher StatusWatcher; StatusWatcher *watcher = new StatusWatcher(); watcher->setFuture(d->childSavingFuture); connect(watcher, SIGNAL(finished()), SLOT(finishExportInBackground())); connect(watcher, SIGNAL(finished()), watcher, SLOT(deleteLater())); return true; } void KisDocument::finishExportInBackground() { KIS_SAFE_ASSERT_RECOVER(d->childSavingFuture.isFinished()) { emit sigBackgroundSavingFinished(ImportExportCodes::InternalError, ""); return; } KisImportExportErrorCode status = d->childSavingFuture.result(); const QString errorMessage = status.errorMessage(); d->savingImage.clear(); d->childSavingFuture = QFuture(); d->lastErrorMessage.clear(); if (d->savingUpdater) { d->savingUpdater->setProgress(100); } emit sigBackgroundSavingFinished(status, errorMessage); } void KisDocument::setReadWrite(bool readwrite) { d->readwrite = readwrite; setNormalAutoSaveInterval(); Q_FOREACH (KisMainWindow *mainWindow, KisPart::instance()->mainWindows()) { mainWindow->setReadWrite(readwrite); } } void KisDocument::setAutoSaveDelay(int delay) { if (isReadWrite() && delay > 0) { d->autoSaveTimer->start(delay * 1000); } else { d->autoSaveTimer->stop(); } } void KisDocument::setNormalAutoSaveInterval() { setAutoSaveDelay(d->autoSaveDelay); d->autoSaveFailureCount = 0; } void KisDocument::setEmergencyAutoSaveInterval() { const int emergencyAutoSaveInterval = 10; /* sec */ setAutoSaveDelay(emergencyAutoSaveInterval); d->autoSaveFailureCount++; } void KisDocument::setInfiniteAutoSaveInterval() { setAutoSaveDelay(-1); } KoDocumentInfo *KisDocument::documentInfo() const { return d->docInfo; } bool KisDocument::isModified() const { return d->modified; } QPixmap KisDocument::generatePreview(const QSize& size) { KisImageSP image = d->image; if (d->savingImage) image = d->savingImage; if (image) { QRect bounds = image->bounds(); QSize newSize = bounds.size(); newSize.scale(size, Qt::KeepAspectRatio); QPixmap px = QPixmap::fromImage(image->convertToQImage(newSize, 0)); if (px.size() == QSize(0,0)) { px = QPixmap(newSize); QPainter gc(&px); QBrush checkBrush = QBrush(KisCanvasWidgetBase::createCheckersImage(newSize.width() / 5)); gc.fillRect(px.rect(), checkBrush); gc.end(); } return px; } return QPixmap(size); } QString KisDocument::generateAutoSaveFileName(const QString & path) const { QString retval; // Using the extension allows to avoid relying on the mime magic when opening const QString extension (".kra"); QString prefix = KisConfig(true).readEntry("autosavefileshidden") ? QString(".") : QString(); QRegularExpression autosavePattern1("^\\..+-autosave.kra$"); QRegularExpression autosavePattern2("^.+-autosave.kra$"); QFileInfo fi(path); QString dir = fi.absolutePath(); QString filename = fi.fileName(); if (path.isEmpty() || autosavePattern1.match(filename).hasMatch() || autosavePattern2.match(filename).hasMatch() || !fi.isWritable()) { // Never saved? #ifdef Q_OS_WIN // On Windows, use the temp location (https://bugs.kde.org/show_bug.cgi?id=314921) retval = QString("%1%2%7%3-%4-%5-autosave%6").arg(QDir::tempPath()).arg(QDir::separator()).arg("krita").arg(qApp->applicationPid()).arg(objectName()).arg(extension).arg(prefix); #else // On Linux, use a temp file in $HOME then. Mark it with the pid so two instances don't overwrite each other's autosave file retval = QString("%1%2%7%3-%4-%5-autosave%6").arg(QDir::homePath()).arg(QDir::separator()).arg("krita").arg(qApp->applicationPid()).arg(objectName()).arg(extension).arg(prefix); #endif } else { retval = QString("%1%2%5%3-autosave%4").arg(dir).arg(QDir::separator()).arg(filename).arg(extension).arg(prefix); } //qDebug() << "generateAutoSaveFileName() for path" << path << ":" << retval; return retval; } bool KisDocument::importDocument(const QUrl &_url) { bool ret; dbgUI << "url=" << _url.url(); // open... ret = openUrl(_url); // reset url & m_file (kindly? set by KisParts::openUrl()) to simulate a // File --> Import if (ret) { dbgUI << "success, resetting url"; resetURL(); setTitleModified(); } return ret; } bool KisDocument::openUrl(const QUrl &_url, OpenFlags flags) { if (!_url.isLocalFile()) { return false; } dbgUI << "url=" << _url.url(); d->lastErrorMessage.clear(); // Reimplemented, to add a check for autosave files and to improve error reporting if (!_url.isValid()) { d->lastErrorMessage = i18n("Malformed URL\n%1", _url.url()); // ## used anywhere ? return false; } QUrl url(_url); bool autosaveOpened = false; if (url.isLocalFile() && !fileBatchMode()) { QString file = url.toLocalFile(); QString asf = generateAutoSaveFileName(file); if (QFile::exists(asf)) { KisApplication *kisApp = static_cast(qApp); kisApp->hideSplashScreen(); //qDebug() <<"asf=" << asf; // ## TODO compare timestamps ? int res = QMessageBox::warning(0, i18nc("@title:window", "Krita"), i18n("An autosaved file exists for this document.\nDo you want to open the autosaved file instead?"), QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel, QMessageBox::Yes); switch (res) { case QMessageBox::Yes : url.setPath(asf); autosaveOpened = true; break; case QMessageBox::No : KisUsageLogger::log(QString("Removing autosave file: %1").arg(asf)); QFile::remove(asf); break; default: // Cancel return false; } } } bool ret = openUrlInternal(url); if (autosaveOpened || flags & RecoveryFile) { setReadWrite(true); // enable save button setModified(true); setRecovered(true); } else { if (ret) { if (!(flags & DontAddToRecent)) { KisPart::instance()->addRecentURLToAllMainWindows(_url); } // Detect readonly local-files; remote files are assumed to be writable QFileInfo fi(url.toLocalFile()); setReadWrite(fi.isWritable()); } setRecovered(false); } return ret; } class DlgLoadMessages : public KoDialog { public: DlgLoadMessages(const QString &title, const QString &message, const QStringList &warnings) { setWindowTitle(title); setWindowIcon(KisIconUtils::loadIcon("warning")); QWidget *page = new QWidget(this); QVBoxLayout *layout = new QVBoxLayout(page); QHBoxLayout *hlayout = new QHBoxLayout(); QLabel *labelWarning= new QLabel(); labelWarning->setPixmap(KisIconUtils::loadIcon("warning").pixmap(32, 32)); hlayout->addWidget(labelWarning); hlayout->addWidget(new QLabel(message)); layout->addLayout(hlayout); QTextBrowser *browser = new QTextBrowser(); QString warning = "

"; if (warnings.size() == 1) { warning += " Reason:

"; } else { warning += " Reasons:

"; } warning += "

    "; Q_FOREACH(const QString &w, warnings) { warning += "\n
  • " + w + "
  • "; } warning += "
"; browser->setHtml(warning); browser->setMinimumHeight(200); browser->setMinimumWidth(400); layout->addWidget(browser); setMainWidget(page); setButtons(KoDialog::Ok); resize(minimumSize()); } }; bool KisDocument::openFile() { //dbgUI <<"for" << localFilePath(); if (!QFile::exists(localFilePath()) && !fileBatchMode()) { QMessageBox::critical(0, i18nc("@title:window", "Krita"), i18n("File %1 does not exist.", localFilePath())); return false; } QString filename = localFilePath(); QString typeName = mimeType(); if (typeName.isEmpty()) { typeName = KisMimeDatabase::mimeTypeForFile(filename); } //qDebug() << "mimetypes 4:" << typeName; // Allow to open backup files, don't keep the mimetype application/x-trash. if (typeName == "application/x-trash") { QString path = filename; while (path.length() > 0) { path.chop(1); typeName = KisMimeDatabase::mimeTypeForFile(path); //qDebug() << "\t" << path << typeName; if (!typeName.isEmpty()) { break; } } //qDebug() << "chopped" << filename << "to" << path << "Was trash, is" << typeName; } dbgUI << localFilePath() << "type:" << typeName; KisMainWindow *window = KisPart::instance()->currentMainwindow(); KoUpdaterPtr updater; if (window && window->viewManager()) { updater = window->viewManager()->createUnthreadedUpdater(i18n("Opening document")); d->importExportManager->setUpdater(updater); } KisImportExportErrorCode status = d->importExportManager->importDocument(localFilePath(), typeName); if (!status.isOk()) { if (window && window->viewManager()) { updater->cancel(); } QString msg = status.errorMessage(); if (!msg.isEmpty() && !fileBatchMode()) { DlgLoadMessages dlg(i18nc("@title:window", "Krita"), i18n("Could not open %2.\nReason: %1.", msg, prettyPathOrUrl()), errorMessage().split("\n") + warningMessage().split("\n")); dlg.exec(); } return false; } else if (!warningMessage().isEmpty() && !fileBatchMode()) { DlgLoadMessages dlg(i18nc("@title:window", "Krita"), i18n("There were problems opening %1.", prettyPathOrUrl()), warningMessage().split("\n")); dlg.exec(); setUrl(QUrl()); } setMimeTypeAfterLoading(typeName); d->syncDecorationsWrapperLayerState(); emit sigLoadingFinished(); undoStack()->clear(); return true; } void KisDocument::autoSaveOnPause() { if (!d->modified || !d->modifiedAfterAutosave) return; const QString autoSaveFileName = generateAutoSaveFileName(localFilePath()); QUrl url("file:/" + autoSaveFileName); bool started = exportDocumentSync(url, nativeFormatMimeType()); if (started) { d->modifiedAfterAutosave = false; dbgAndroid << "autoSaveOnPause successful"; } else { qWarning() << "Could not auto-save when paused"; } } // shared between openFile and koMainWindow's "create new empty document" code void KisDocument::setMimeTypeAfterLoading(const QString& mimeType) { d->mimeType = mimeType.toLatin1(); d->outputMimeType = d->mimeType; } bool KisDocument::loadNativeFormat(const QString & file_) { return openUrl(QUrl::fromLocalFile(file_)); } void KisDocument::setModified(bool mod) { if (mod) { updateEditingTime(false); } if (d->isAutosaving) // ignore setModified calls due to autosaving return; if ( !d->readwrite && d->modified ) { errKrita << "Can't set a read-only document to 'modified' !" << endl; return; } //dbgUI<<" url:" << url.path(); //dbgUI<<" mod="<docInfo->aboutInfo("editing-time").toInt() + d->firstMod.secsTo(d->lastMod))); d->firstMod = now; } else if (firstModDelta > 60 || forceStoreElapsed) { d->docInfo->setAboutInfo("editing-time", QString::number(d->docInfo->aboutInfo("editing-time").toInt() + firstModDelta)); d->firstMod = now; } d->lastMod = now; } QString KisDocument::prettyPathOrUrl() const { QString _url(url().toDisplayString()); #ifdef Q_OS_WIN if (url().isLocalFile()) { _url = QDir::toNativeSeparators(_url); } #endif return _url; } // Get caption from document info (title(), in about page) QString KisDocument::caption() const { QString c; const QString _url(url().fileName()); // if URL is empty...it is probably an unsaved file if (_url.isEmpty()) { c = " [" + i18n("Not Saved") + "] "; } else { c = _url; // Fall back to document URL } return c; } void KisDocument::setTitleModified() { emit titleModified(caption(), isModified()); } QDomDocument KisDocument::createDomDocument(const QString& tagName, const QString& version) const { return createDomDocument("krita", tagName, version); } //static QDomDocument KisDocument::createDomDocument(const QString& appName, const QString& tagName, const QString& version) { QDomImplementation impl; QString url = QString("http://www.calligra.org/DTD/%1-%2.dtd").arg(appName).arg(version); QDomDocumentType dtype = impl.createDocumentType(tagName, QString("-//KDE//DTD %1 %2//EN").arg(appName).arg(version), url); // The namespace URN doesn't need to include the version number. QString namespaceURN = QString("http://www.calligra.org/DTD/%1").arg(appName); QDomDocument doc = impl.createDocument(namespaceURN, tagName, dtype); doc.insertBefore(doc.createProcessingInstruction("xml", "version=\"1.0\" encoding=\"UTF-8\""), doc.documentElement()); return doc; } bool KisDocument::isNativeFormat(const QByteArray& mimetype) const { if (mimetype == nativeFormatMimeType()) return true; return extraNativeMimeTypes().contains(mimetype); } void KisDocument::setErrorMessage(const QString& errMsg) { d->lastErrorMessage = errMsg; } QString KisDocument::errorMessage() const { return d->lastErrorMessage; } void KisDocument::setWarningMessage(const QString& warningMsg) { d->lastWarningMessage = warningMsg; } QString KisDocument::warningMessage() const { return d->lastWarningMessage; } void KisDocument::removeAutoSaveFiles(const QString &autosaveBaseName, bool wasRecovered) { // Eliminate any auto-save file QString asf = generateAutoSaveFileName(autosaveBaseName); // the one in the current dir if (QFile::exists(asf)) { KisUsageLogger::log(QString("Removing autosave file: %1").arg(asf)); QFile::remove(asf); } asf = generateAutoSaveFileName(QString()); // and the one in $HOME if (QFile::exists(asf)) { KisUsageLogger::log(QString("Removing autosave file: %1").arg(asf)); QFile::remove(asf); } QList expressions; expressions << QRegularExpression("^\\..+-autosave.kra$") << QRegularExpression("^.+-autosave.kra$"); Q_FOREACH(const QRegularExpression &rex, expressions) { if (wasRecovered && !autosaveBaseName.isEmpty() && rex.match(QFileInfo(autosaveBaseName).fileName()).hasMatch() && QFile::exists(autosaveBaseName)) { KisUsageLogger::log(QString("Removing autosave file: %1").arg(autosaveBaseName)); QFile::remove(autosaveBaseName); } } } KoUnit KisDocument::unit() const { return d->unit; } void KisDocument::setUnit(const KoUnit &unit) { if (d->unit != unit) { d->unit = unit; emit unitChanged(unit); } } KUndo2Stack *KisDocument::undoStack() { return d->undoStack; } KisImportExportManager *KisDocument::importExportManager() const { return d->importExportManager; } void KisDocument::addCommand(KUndo2Command *command) { if (command) d->undoStack->push(command); } void KisDocument::beginMacro(const KUndo2MagicString & text) { d->undoStack->beginMacro(text); } void KisDocument::endMacro() { d->undoStack->endMacro(); } void KisDocument::slotUndoStackCleanChanged(bool value) { setModified(!value); } void KisDocument::slotConfigChanged() { KisConfig cfg(true); if (d->undoStack->undoLimit() != cfg.undoStackLimit()) { if (!d->undoStack->isClean()) { d->undoStack->clear(); } d->undoStack->setUndoLimit(cfg.undoStackLimit()); } d->autoSaveDelay = cfg.autoSaveInterval(); setNormalAutoSaveInterval(); } void KisDocument::slotImageRootChanged() { d->syncDecorationsWrapperLayerState(); } void KisDocument::clearUndoHistory() { d->undoStack->clear(); } KisGridConfig KisDocument::gridConfig() const { return d->gridConfig; } void KisDocument::setGridConfig(const KisGridConfig &config) { if (d->gridConfig != config) { d->gridConfig = config; d->syncDecorationsWrapperLayerState(); emit sigGridConfigChanged(config); } } QList &KisDocument::paletteList() { return d->paletteList; } void KisDocument::setPaletteList(const QList &paletteList, bool emitSignal) { if (d->paletteList != paletteList) { QList oldPaletteList = d->paletteList; d->paletteList = paletteList; if (emitSignal) { emit sigPaletteListChanged(oldPaletteList, paletteList); } } } const KisGuidesConfig& KisDocument::guidesConfig() const { return d->guidesConfig; } void KisDocument::setGuidesConfig(const KisGuidesConfig &data) { if (d->guidesConfig == data) return; d->guidesConfig = data; d->syncDecorationsWrapperLayerState(); emit sigGuidesConfigChanged(d->guidesConfig); } const KisMirrorAxisConfig& KisDocument::mirrorAxisConfig() const { return d->mirrorAxisConfig; } void KisDocument::setMirrorAxisConfig(const KisMirrorAxisConfig &config) { if (d->mirrorAxisConfig == config) { return; } d->mirrorAxisConfig = config; setModified(true); emit sigMirrorAxisConfigChanged(); } void KisDocument::resetURL() { setUrl(QUrl()); setLocalFilePath(QString()); } KoDocumentInfoDlg *KisDocument::createDocumentInfoDialog(QWidget *parent, KoDocumentInfo *docInfo) const { return new KoDocumentInfoDlg(parent, docInfo); } bool KisDocument::isReadWrite() const { return d->readwrite; } QUrl KisDocument::url() const { return d->m_url; } bool KisDocument::closeUrl(bool promptToSave) { if (promptToSave) { if ( isReadWrite() && isModified()) { Q_FOREACH (KisView *view, KisPart::instance()->views()) { if (view && view->document() == this) { if (!view->queryClose()) { return false; } } } } } // Not modified => ok and delete temp file. d->mimeType = QByteArray(); // It always succeeds for a read-only part, // but the return value exists for reimplementations // (e.g. pressing cancel for a modified read-write part) return true; } void KisDocument::setUrl(const QUrl &url) { d->m_url = url; } QString KisDocument::localFilePath() const { return d->m_file; } void KisDocument::setLocalFilePath( const QString &localFilePath ) { d->m_file = localFilePath; } bool KisDocument::openUrlInternal(const QUrl &url) { if ( !url.isValid() ) { return false; } if (d->m_bAutoDetectedMime) { d->mimeType = QByteArray(); d->m_bAutoDetectedMime = false; } QByteArray mimetype = d->mimeType; if ( !closeUrl() ) { return false; } d->mimeType = mimetype; setUrl(url); d->m_file.clear(); if (d->m_url.isLocalFile()) { d->m_file = d->m_url.toLocalFile(); bool ret; // set the mimetype only if it was not already set (for example, by the host application) if (d->mimeType.isEmpty()) { // get the mimetype of the file // using findByUrl() to avoid another string -> url conversion QString mime = KisMimeDatabase::mimeTypeForFile(d->m_url.toLocalFile()); d->mimeType = mime.toLocal8Bit(); d->m_bAutoDetectedMime = true; } setUrl(d->m_url); ret = openFile(); if (ret) { emit completed(); } else { emit canceled(QString()); } return ret; } return false; } bool KisDocument::newImage(const QString& name, qint32 width, qint32 height, const KoColorSpace* cs, const KoColor &bgColor, KisConfig::BackgroundStyle bgStyle, int numberOfLayers, const QString &description, const double imageResolution) { Q_ASSERT(cs); KisImageSP image; if (!cs) return false; QApplication::setOverrideCursor(Qt::BusyCursor); image = new KisImage(createUndoStore(), width, height, cs, name); Q_CHECK_PTR(image); connect(image, SIGNAL(sigImageModified()), this, SLOT(setImageModified()), Qt::UniqueConnection); image->setResolution(imageResolution, imageResolution); image->assignImageProfile(cs->profile()); image->waitForDone(); documentInfo()->setAboutInfo("title", name); documentInfo()->setAboutInfo("abstract", description); KisLayerSP layer; if (bgStyle == KisConfig::RASTER_LAYER || bgStyle == KisConfig::FILL_LAYER) { KoColor strippedAlpha = bgColor; strippedAlpha.setOpacity(OPACITY_OPAQUE_U8); if (bgStyle == KisConfig::RASTER_LAYER) { layer = new KisPaintLayer(image.data(), "Background", OPACITY_OPAQUE_U8, cs);; layer->paintDevice()->setDefaultPixel(strippedAlpha); } else if (bgStyle == KisConfig::FILL_LAYER) { KisFilterConfigurationSP filter_config = KisGeneratorRegistry::instance()->get("color")->defaultConfiguration(); filter_config->setProperty("color", strippedAlpha.toQColor()); layer = new KisGeneratorLayer(image.data(), "Background Fill", filter_config, image->globalSelection()); } layer->setOpacity(bgColor.opacityU8()); if (numberOfLayers > 1) { //Lock bg layer if others are present. layer->setUserLocked(true); } } else { // KisConfig::CANVAS_COLOR (needs an unlocked starting layer). image->setDefaultProjectionColor(bgColor); layer = new KisPaintLayer(image.data(), image->nextLayerName(), OPACITY_OPAQUE_U8, cs); } Q_CHECK_PTR(layer); image->addNode(layer.data(), image->rootLayer().data()); layer->setDirty(QRect(0, 0, width, height)); setCurrentImage(image); for(int i = 1; i < numberOfLayers; ++i) { KisPaintLayerSP layer = new KisPaintLayer(image, image->nextLayerName(), OPACITY_OPAQUE_U8, cs); image->addNode(layer, image->root(), i); layer->setDirty(QRect(0, 0, width, height)); } KisConfig cfg(false); cfg.defImageWidth(width); cfg.defImageHeight(height); cfg.defImageResolution(imageResolution); cfg.defColorModel(image->colorSpace()->colorModelId().id()); cfg.setDefaultColorDepth(image->colorSpace()->colorDepthId().id()); cfg.defColorProfile(image->colorSpace()->profile()->name()); KisUsageLogger::log(QString("Created image \"%1\", %2 * %3 pixels, %4 dpi. Color model: %6 %5 (%7). Layers: %8") .arg(name) .arg(width).arg(height) .arg(imageResolution * 72.0) .arg(image->colorSpace()->colorModelId().name()) .arg(image->colorSpace()->colorDepthId().name()) .arg(image->colorSpace()->profile()->name()) .arg(numberOfLayers)); QApplication::restoreOverrideCursor(); return true; } bool KisDocument::isSaving() const { const bool result = d->savingMutex.tryLock(); if (result) { d->savingMutex.unlock(); } return !result; } void KisDocument::waitForSavingToComplete() { if (isSaving()) { KisAsyncActionFeedback f(i18nc("progress dialog message when the user closes the document that is being saved", "Waiting for saving to complete..."), 0); f.waitForMutex(&d->savingMutex); } } KoShapeControllerBase *KisDocument::shapeController() const { return d->shapeController; } KoShapeLayer* KisDocument::shapeForNode(KisNodeSP layer) const { return d->shapeController->shapeForNode(layer); } QList KisDocument::assistants() const { return d->assistants; } void KisDocument::setAssistants(const QList &value) { if (d->assistants != value) { d->assistants = value; d->syncDecorationsWrapperLayerState(); emit sigAssistantsChanged(); } } KisReferenceImagesLayerSP KisDocument::referenceImagesLayer() const { if (!d->image) return KisReferenceImagesLayerSP(); KisReferenceImagesLayerSP referencesLayer = KisLayerUtils::findNodeByType(d->image->root()); return referencesLayer; } void KisDocument::setReferenceImagesLayer(KisSharedPtr layer, bool updateImage) { KisReferenceImagesLayerSP currentReferenceLayer = referenceImagesLayer(); if (currentReferenceLayer == layer) { return; } if (currentReferenceLayer) { currentReferenceLayer->disconnect(this); } if (updateImage) { if (currentReferenceLayer) { d->image->removeNode(currentReferenceLayer); } if (layer) { d->image->addNode(layer); } } currentReferenceLayer = layer; if (currentReferenceLayer) { connect(currentReferenceLayer, SIGNAL(sigUpdateCanvas(QRectF)), this, SIGNAL(sigReferenceImagesChanged())); } emit sigReferenceImagesLayerChanged(layer); } void KisDocument::setPreActivatedNode(KisNodeSP activatedNode) { d->preActivatedNode = activatedNode; } KisNodeSP KisDocument::preActivatedNode() const { return d->preActivatedNode; } KisImageWSP KisDocument::image() const { return d->image; } KisImageSP KisDocument::savingImage() const { return d->savingImage; } void KisDocument::setCurrentImage(KisImageSP image, bool forceInitialUpdate) { if (d->image) { // Disconnect existing sig/slot connections d->image->setUndoStore(new KisDumbUndoStore()); d->image->disconnect(this); d->shapeController->setImage(0); d->image = 0; } if (!image) return; d->setImageAndInitIdleWatcher(image); d->image->setUndoStore(new KisDocumentUndoStore(this)); d->shapeController->setImage(image); setModified(false); connect(d->image, SIGNAL(sigImageModified()), this, SLOT(setImageModified()), Qt::UniqueConnection); connect(d->image, SIGNAL(sigLayersChangedAsync()), this, SLOT(slotImageRootChanged())); if (forceInitialUpdate) { d->image->initialRefreshGraph(); } } void KisDocument::hackPreliminarySetImage(KisImageSP image) { KIS_SAFE_ASSERT_RECOVER_RETURN(!d->image); d->setImageAndInitIdleWatcher(image); d->shapeController->setImage(image); } void KisDocument::setImageModified() { // we only set as modified if undo stack is not at clean state setModified(!d->undoStack->isClean()); } KisUndoStore* KisDocument::createUndoStore() { return new KisDocumentUndoStore(this); } bool KisDocument::isAutosaving() const { return d->isAutosaving; } QString KisDocument::exportErrorToUserMessage(KisImportExportErrorCode status, const QString &errorMessage) { return errorMessage.isEmpty() ? status.errorMessage() : errorMessage; } void KisDocument::setAssistantsGlobalColor(QColor color) { d->globalAssistantsColor = color; } QColor KisDocument::assistantsGlobalColor() { return d->globalAssistantsColor; } QRectF KisDocument::documentBounds() const { QRectF bounds = d->image->bounds(); KisReferenceImagesLayerSP referenceImagesLayer = this->referenceImagesLayer(); if (referenceImagesLayer) { bounds |= referenceImagesLayer->boundingImageRect(); } return bounds; } diff --git a/libs/ui/KisImportExportManager.cpp b/libs/ui/KisImportExportManager.cpp index 4dccd9f57b..69c0fcdd8c 100644 --- a/libs/ui/KisImportExportManager.cpp +++ b/libs/ui/KisImportExportManager.cpp @@ -1,726 +1,726 @@ /* * Copyright (C) 2016 Boudewijn Rempt * * 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 "KisImportExportManager.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kis_config.h" #include "KisImportExportFilter.h" #include "KisDocument.h" #include #include #include "kis_painter.h" #include "kis_guides_config.h" #include "kis_grid_config.h" #include "kis_popup_button.h" #include #include "kis_async_action_feedback.h" #include "KisReferenceImagesLayer.h" // static cache for import and export mimetypes QStringList KisImportExportManager::m_importMimeTypes; QStringList KisImportExportManager::m_exportMimeTypes; class Q_DECL_HIDDEN KisImportExportManager::Private { public: KoUpdaterPtr updater; QString cachedExportFilterMimeType; QSharedPointer cachedExportFilter; }; struct KisImportExportManager::ConversionResult { ConversionResult() { } ConversionResult(const QFuture &futureStatus) : m_isAsync(true), m_futureStatus(futureStatus) { } ConversionResult(KisImportExportErrorCode status) : m_isAsync(false), m_status(status) { } bool isAsync() const { return m_isAsync; } QFuture futureStatus() const { // if the result is not async, then it means some failure happened, // just return a cancelled future KIS_SAFE_ASSERT_RECOVER_NOOP(m_isAsync || !m_status.isOk()); return m_futureStatus; } KisImportExportErrorCode status() const { return m_status; } void setStatus(KisImportExportErrorCode value) { m_status = value; } private: bool m_isAsync = false; QFuture m_futureStatus; KisImportExportErrorCode m_status = ImportExportCodes::InternalError; }; KisImportExportManager::KisImportExportManager(KisDocument* document) : m_document(document) , d(new Private) { } KisImportExportManager::~KisImportExportManager() { delete d; } KisImportExportErrorCode KisImportExportManager::importDocument(const QString& location, const QString& mimeType) { ConversionResult result = convert(Import, location, location, mimeType, false, 0, false); KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(!result.isAsync(), ImportExportCodes::InternalError); return result.status(); } KisImportExportErrorCode KisImportExportManager::exportDocument(const QString& location, const QString& realLocation, const QByteArray& mimeType, bool showWarnings, KisPropertiesConfigurationSP exportConfiguration) { ConversionResult result = convert(Export, location, realLocation, mimeType, showWarnings, exportConfiguration, false); KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(!result.isAsync(), ImportExportCodes::InternalError); return result.status(); } QFuture KisImportExportManager::exportDocumentAsyc(const QString &location, const QString &realLocation, const QByteArray &mimeType, KisImportExportErrorCode &status, bool showWarnings, KisPropertiesConfigurationSP exportConfiguration) { ConversionResult result = convert(Export, location, realLocation, mimeType, showWarnings, exportConfiguration, true); KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(result.isAsync() || !result.status().isOk(), QFuture()); status = result.status(); return result.futureStatus(); } // The static method to figure out to which parts of the // graph this mimetype has a connection to. QStringList KisImportExportManager::supportedMimeTypes(Direction direction) { // Find the right mimetype by the extension QSet mimeTypes; // mimeTypes << KisDocument::nativeFormatMimeType() << "application/x-krita-paintoppreset" << "image/openraster"; if (direction == KisImportExportManager::Import) { if (m_importMimeTypes.isEmpty()) { QListlist = KoJsonTrader::instance()->query("Krita/FileFilter", ""); Q_FOREACH(QPluginLoader *loader, list) { QJsonObject json = loader->metaData().value("MetaData").toObject(); Q_FOREACH(const QString &mimetype, json.value("X-KDE-Import").toString().split(",", QString::SkipEmptyParts)) { //qDebug() << "Adding import mimetype" << mimetype << KisMimeDatabase::descriptionForMimeType(mimetype) << "from plugin" << loader; mimeTypes << mimetype; } } qDeleteAll(list); m_importMimeTypes = mimeTypes.toList(); } return m_importMimeTypes; } else if (direction == KisImportExportManager::Export) { if (m_exportMimeTypes.isEmpty()) { QListlist = KoJsonTrader::instance()->query("Krita/FileFilter", ""); Q_FOREACH(QPluginLoader *loader, list) { QJsonObject json = loader->metaData().value("MetaData").toObject(); Q_FOREACH(const QString &mimetype, json.value("X-KDE-Export").toString().split(",", QString::SkipEmptyParts)) { //qDebug() << "Adding export mimetype" << mimetype << KisMimeDatabase::descriptionForMimeType(mimetype) << "from plugin" << loader; mimeTypes << mimetype; } } qDeleteAll(list); m_exportMimeTypes = mimeTypes.toList(); } return m_exportMimeTypes; } return QStringList(); } KisImportExportFilter *KisImportExportManager::filterForMimeType(const QString &mimetype, KisImportExportManager::Direction direction) { int weight = -1; KisImportExportFilter *filter = 0; QListlist = KoJsonTrader::instance()->query("Krita/FileFilter", ""); Q_FOREACH(QPluginLoader *loader, list) { QJsonObject json = loader->metaData().value("MetaData").toObject(); QString directionKey = direction == Export ? "X-KDE-Export" : "X-KDE-Import"; if (json.value(directionKey).toString().split(",", QString::SkipEmptyParts).contains(mimetype)) { KLibFactory *factory = qobject_cast(loader->instance()); if (!factory) { warnUI << loader->errorString(); continue; } QObject* obj = factory->create(0); if (!obj || !obj->inherits("KisImportExportFilter")) { delete obj; continue; } KisImportExportFilter *f = qobject_cast(obj); if (!f) { delete obj; continue; } int w = json.value("X-KDE-Weight").toInt(); if (w > weight) { delete filter; filter = f; f->setObjectName(loader->fileName()); weight = w; } } } qDeleteAll(list); if (filter) { filter->setMimeType(mimetype); } return filter; } bool KisImportExportManager::batchMode(void) const { return m_document->fileBatchMode(); } void KisImportExportManager::setUpdater(KoUpdaterPtr updater) { d->updater = updater; } QString KisImportExportManager::askForAudioFileName(const QString &defaultDir, QWidget *parent) { KoFileDialog dialog(parent, KoFileDialog::ImportFiles, "ImportAudio"); if (!defaultDir.isEmpty()) { dialog.setDefaultDir(defaultDir); } QStringList mimeTypes; mimeTypes << "audio/mpeg"; mimeTypes << "audio/ogg"; mimeTypes << "audio/vorbis"; mimeTypes << "audio/vnd.wave"; mimeTypes << "audio/flac"; dialog.setMimeTypeFilters(mimeTypes); dialog.setCaption(i18nc("@title:window", "Open Audio")); return dialog.filename(); } KisImportExportManager::ConversionResult KisImportExportManager::convert(KisImportExportManager::Direction direction, const QString &location, const QString& realLocation, const QString &mimeType, bool showWarnings, KisPropertiesConfigurationSP exportConfiguration, bool isAsync) { // export configuration is supported for export only KIS_SAFE_ASSERT_RECOVER_NOOP(direction == Export || !bool(exportConfiguration)); QString typeName = mimeType; if (typeName.isEmpty()) { typeName = KisMimeDatabase::mimeTypeForFile(location, direction == KisImportExportManager::Export ? false : true); } QSharedPointer filter; /** * Fetching a filter from the registry is a really expensive operation, * because it blocks all the threads. Cache the filter if possible. */ if (direction == KisImportExportManager::Export && d->cachedExportFilter && d->cachedExportFilterMimeType == typeName) { filter = d->cachedExportFilter; } else { filter = toQShared(filterForMimeType(typeName, direction)); if (direction == Export) { d->cachedExportFilter = filter; d->cachedExportFilterMimeType = typeName; } } if (!filter) { return KisImportExportErrorCode(ImportExportCodes::FileFormatIncorrect); } filter->setFilename(location); filter->setRealFilename(realLocation); filter->setBatchMode(batchMode()); filter->setMimeType(typeName); if (!d->updater.isNull()) { // WARNING: The updater is not guaranteed to be persistent! If you ever want // to add progress reporting to "Save also as .kra", make sure you create // a separate KoProgressUpdater for that! // WARNING2: the failsafe completion of the updater happens in the destructor // the filter. filter->setUpdater(d->updater); } QByteArray from, to; if (direction == Export) { from = m_document->nativeFormatMimeType(); to = typeName.toLatin1(); } else { from = typeName.toLatin1(); to = m_document->nativeFormatMimeType(); } KIS_ASSERT_RECOVER_RETURN_VALUE( direction == Import || direction == Export, KisImportExportErrorCode(ImportExportCodes::InternalError)); // "bad conversion graph" ConversionResult result = KisImportExportErrorCode(ImportExportCodes::OK); if (direction == Import) { KisUsageLogger::log(QString("Importing %1 to %2. Location: %3. Real location: %4. Batchmode: %5") .arg(QString::fromLatin1(from)) .arg(QString::fromLatin1(to)) .arg(location) .arg(realLocation) .arg(batchMode())); // async importing is not yet supported! KIS_SAFE_ASSERT_RECOVER_NOOP(!isAsync); // FIXME: Dmitry says "this progress reporting code never worked. Initial idea was to implement it his way, but I stopped and didn't finish it" if (0 && !batchMode()) { KisAsyncActionFeedback f(i18n("Opening document..."), 0); result = f.runAction(std::bind(&KisImportExportManager::doImport, this, location, filter)); } else { result = doImport(location, filter); } if (result.status().isOk()) { KisImageSP image = m_document->image(); KisUsageLogger::log(QString("Loaded image from %1. Size: %2 * %3 pixels, %4 dpi. Color model: %6 %5 (%7). Layers: %8") .arg(QString::fromLatin1(from)) .arg(image->width()) .arg(image->height()) .arg(image->xRes()) .arg(image->colorSpace()->colorModelId().name()) .arg(image->colorSpace()->colorDepthId().name()) .arg(image->colorSpace()->profile()->name()) .arg(image->nlayers())); } else { KisUsageLogger::log(QString("Failed to load image from %1").arg(QString::fromLatin1(from))); } } else /* if (direction == Export) */ { if (!exportConfiguration) { exportConfiguration = filter->lastSavedConfiguration(from, to); } if (exportConfiguration) { fillStaticExportConfigurationProperties(exportConfiguration); } bool alsoAsKra = false; bool askUser = askUserAboutExportConfiguration(filter, exportConfiguration, from, to, batchMode(), showWarnings, &alsoAsKra); if (!batchMode() && !askUser) { return KisImportExportErrorCode(ImportExportCodes::Cancelled); } KisUsageLogger::log(QString("Converting from %1 to %2. Location: %3. Real location: %4. Batchmode: %5. Configuration: %6") .arg(QString::fromLatin1(from)) .arg(QString::fromLatin1(to)) .arg(location) .arg(realLocation) .arg(batchMode()) .arg(exportConfiguration ? exportConfiguration->toXML() : "none")); if (isAsync) { result = QtConcurrent::run(std::bind(&KisImportExportManager::doExport, this, location, filter, exportConfiguration, alsoAsKra)); // we should explicitly report that the exporting has been initiated result.setStatus(ImportExportCodes::OK); } else if (!batchMode()) { KisAsyncActionFeedback f(i18n("Saving document..."), 0); result = f.runAction(std::bind(&KisImportExportManager::doExport, this, location, filter, exportConfiguration, alsoAsKra)); } else { result = doExport(location, filter, exportConfiguration, alsoAsKra); } - if (exportConfiguration && !batchMode() && showWarnings) { + if (exportConfiguration && !batchMode()) { KisConfig(false).setExportConfiguration(typeName, exportConfiguration); } } return result; } void KisImportExportManager::fillStaticExportConfigurationProperties(KisPropertiesConfigurationSP exportConfiguration, KisImageSP image) { KisPaintDeviceSP dev = image->projection(); const KoColorSpace* cs = dev->colorSpace(); const bool isThereAlpha = KisPainter::checkDeviceHasTransparency(image->projection()); exportConfiguration->setProperty(KisImportExportFilter::ImageContainsTransparencyTag, isThereAlpha); exportConfiguration->setProperty(KisImportExportFilter::ColorModelIDTag, cs->colorModelId().id()); exportConfiguration->setProperty(KisImportExportFilter::ColorDepthIDTag, cs->colorDepthId().id()); const bool sRGB = (cs->profile()->name().contains(QLatin1String("srgb"), Qt::CaseInsensitive) && !cs->profile()->name().contains(QLatin1String("g10"))); exportConfiguration->setProperty(KisImportExportFilter::sRGBTag, sRGB); } void KisImportExportManager::fillStaticExportConfigurationProperties(KisPropertiesConfigurationSP exportConfiguration) { return fillStaticExportConfigurationProperties(exportConfiguration, m_document->image()); } bool KisImportExportManager::askUserAboutExportConfiguration( QSharedPointer filter, KisPropertiesConfigurationSP exportConfiguration, const QByteArray &from, const QByteArray &to, const bool batchMode, const bool showWarnings, bool *alsoAsKra) { // prevents the animation renderer from running this code const QString mimeUserDescription = KisMimeDatabase::descriptionForMimeType(to); QStringList warnings; QStringList errors; { KisPreExportChecker checker; checker.check(m_document->image(), filter->exportChecks()); warnings = checker.warnings(); errors = checker.errors(); } KisConfigWidget *wdg = 0; if (QThread::currentThread() == qApp->thread()) { wdg = filter->createConfigurationWidget(0, from, to); KisMainWindow *kisMain = KisPart::instance()->currentMainwindow(); if (wdg && kisMain) { KisViewManager *manager = kisMain->viewManager(); wdg->setView(manager); } } // Extra checks that cannot be done by the checker, because the checker only has access to the image. if (!m_document->assistants().isEmpty() && to != m_document->nativeFormatMimeType()) { warnings.append(i18nc("image conversion warning", "The image contains assistants. The assistants will not be saved.")); } if (m_document->referenceImagesLayer() && m_document->referenceImagesLayer()->shapeCount() > 0 && to != m_document->nativeFormatMimeType()) { warnings.append(i18nc("image conversion warning", "The image contains reference images. The reference images will not be saved.")); } if (m_document->guidesConfig().hasGuides() && to != m_document->nativeFormatMimeType()) { warnings.append(i18nc("image conversion warning", "The image contains guides. The guides will not be saved.")); } if (!m_document->gridConfig().isDefault() && to != m_document->nativeFormatMimeType()) { warnings.append(i18nc("image conversion warning", "The image contains a custom grid configuration. The configuration will not be saved.")); } if (!batchMode && !errors.isEmpty()) { QString error = "

" + i18n("Error: cannot save this image as a %1.", mimeUserDescription) + " " + i18n("Reasons:") + "

" + "

    "; Q_FOREACH(const QString &w, errors) { error += "\n
  • " + w + "
  • "; } error += "
"; QMessageBox::critical(KisPart::instance()->currentMainwindow(), i18nc("@title:window", "Krita: Export Error"), error); return false; } if (!batchMode && (wdg || !warnings.isEmpty())) { KoDialog dlg; dlg.setButtons(KoDialog::Ok | KoDialog::Cancel); dlg.setWindowTitle(mimeUserDescription); QWidget *page = new QWidget(&dlg); QVBoxLayout *layout = new QVBoxLayout(page); if (showWarnings && !warnings.isEmpty()) { QHBoxLayout *hLayout = new QHBoxLayout(); QLabel *labelWarning = new QLabel(); labelWarning->setPixmap(KisIconUtils::loadIcon("warning").pixmap(32, 32)); hLayout->addWidget(labelWarning); KisPopupButton *bn = new KisPopupButton(0); bn->setText(i18nc("Keep the extra space at the end of the sentence, please", "Warning: saving as %1 will lose information from your image. ", mimeUserDescription)); hLayout->addWidget(bn); layout->addLayout(hLayout); QTextBrowser *browser = new QTextBrowser(); browser->setMinimumWidth(bn->width()); bn->setPopupWidget(browser); QString warning = "

" + i18n("You will lose information when saving this image as a %1.", mimeUserDescription); if (warnings.size() == 1) { warning += " " + i18n("Reason:") + "

"; } else { warning += " " + i18n("Reasons:") + "

"; } warning += "

    "; Q_FOREACH(const QString &w, warnings) { warning += "\n
  • " + w + "
  • "; } warning += "
"; browser->setHtml(warning); } if (wdg) { QGroupBox *box = new QGroupBox(i18n("Options")); QVBoxLayout *boxLayout = new QVBoxLayout(box); wdg->setConfiguration(exportConfiguration); boxLayout->addWidget(wdg); layout->addWidget(box); } QCheckBox *chkAlsoAsKra = 0; if (showWarnings && !warnings.isEmpty()) { chkAlsoAsKra = new QCheckBox(i18n("Also save your image as a Krita file.")); chkAlsoAsKra->setChecked(KisConfig(true).readEntry("AlsoSaveAsKra", false)); layout->addWidget(chkAlsoAsKra); } dlg.setMainWidget(page); dlg.resize(dlg.minimumSize()); if (showWarnings || wdg) { if (!dlg.exec()) { return false; } } *alsoAsKra = false; if (chkAlsoAsKra) { KisConfig(false).writeEntry("AlsoSaveAsKra", chkAlsoAsKra->isChecked()); *alsoAsKra = chkAlsoAsKra->isChecked(); } if (wdg) { *exportConfiguration = *wdg->configuration(); } } return true; } KisImportExportErrorCode KisImportExportManager::doImport(const QString &location, QSharedPointer filter) { QFile file(location); if (!file.exists()) { return ImportExportCodes::FileNotExist; } if (filter->supportsIO() && !file.open(QFile::ReadOnly)) { return KisImportExportErrorCode(KisImportExportErrorCannotRead(file.error())); } KisImportExportErrorCode status = filter->convert(m_document, &file, KisPropertiesConfigurationSP()); if (file.isOpen()) { file.close(); } return status; } KisImportExportErrorCode KisImportExportManager::doExport(const QString &location, QSharedPointer filter, KisPropertiesConfigurationSP exportConfiguration, bool alsoAsKra) { KisImportExportErrorCode status = doExportImpl(location, filter, exportConfiguration); if (alsoAsKra && status.isOk()) { QString kraLocation = location + ".kra"; QByteArray mime = m_document->nativeFormatMimeType(); QSharedPointer filter( filterForMimeType(QString::fromLatin1(mime), Export)); KIS_SAFE_ASSERT_RECOVER_NOOP(filter); if (filter) { filter->setFilename(kraLocation); KisPropertiesConfigurationSP kraExportConfiguration = filter->lastSavedConfiguration(mime, mime); status = doExportImpl(kraLocation, filter, kraExportConfiguration); } else { status = ImportExportCodes::FileFormatIncorrect; } } return status; } // Temporary workaround until QTBUG-57299 is fixed. // 02-10-2019 update: the bug is closed, but we've still seen this issue. // and without using QSaveFile the issue can still occur // when QFile::copy fails because Dropbox/Google/OneDrive // locks the target file. #ifndef Q_OS_WIN #define USE_QSAVEFILE #endif KisImportExportErrorCode KisImportExportManager::doExportImpl(const QString &location, QSharedPointer filter, KisPropertiesConfigurationSP exportConfiguration) { #ifdef USE_QSAVEFILE QSaveFile file(location); file.setDirectWriteFallback(true); if (filter->supportsIO() && !file.open(QFile::WriteOnly)) { #else QFileInfo fi(location); QTemporaryFile file(QDir::tempPath() + "/.XXXXXX.kra"); if (filter->supportsIO() && !file.open()) { #endif KisImportExportErrorCannotWrite result(file.error()); #ifdef USE_QSAVEFILE file.cancelWriting(); #endif return result; } KisImportExportErrorCode status = filter->convert(m_document, &file, exportConfiguration); if (filter->supportsIO()) { if (!status.isOk()) { #ifdef USE_QSAVEFILE file.cancelWriting(); #endif } else { #ifdef USE_QSAVEFILE if (!file.commit()) { qWarning() << "Could not commit QSaveFile"; status = KisImportExportErrorCannotWrite(file.error()); } #else file.flush(); file.close(); QFile target(location); if (target.exists()) { // There should already be a .kra~ backup target.remove(); } if (!file.copy(location)) { file.setAutoRemove(false); return KisImportExportErrorCannotWrite(file.error()); } #endif } } // Do some minimal verification QString verificationResult = filter->verify(location); if (!verificationResult.isEmpty()) { status = KisImportExportErrorCode(ImportExportCodes::ErrorWhileWriting); m_document->setErrorMessage(verificationResult); } return status; } #include diff --git a/libs/ui/flake/KisReferenceImagesLayer.cpp b/libs/ui/flake/KisReferenceImagesLayer.cpp index edf9fe039d..e029ce6149 100644 --- a/libs/ui/flake/KisReferenceImagesLayer.cpp +++ b/libs/ui/flake/KisReferenceImagesLayer.cpp @@ -1,234 +1,234 @@ /* * Copyright (C) 2017 Jouni Pentikäinen * * 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 #include #include #include #include #include "KisReferenceImagesLayer.h" #include "KisReferenceImage.h" #include "KisDocument.h" struct AddReferenceImagesCommand : KoShapeCreateCommand { AddReferenceImagesCommand(KisDocument *document, KisSharedPtr layer, const QList referenceImages) : KoShapeCreateCommand(layer->shapeController(), referenceImages, layer.data(), nullptr, kundo2_i18n("Add reference image")) , m_document(document) , m_layer(layer) {} void redo() override { auto layer = m_document->referenceImagesLayer(); - KIS_SAFE_ASSERT_RECOVER_NOOP(!layer || layer == m_layer) + KIS_SAFE_ASSERT_RECOVER_NOOP(!layer || layer == m_layer); if (!layer) { m_document->setReferenceImagesLayer(m_layer, true); } KoShapeCreateCommand::redo(); } void undo() override { KoShapeCreateCommand::undo(); if (m_layer->shapeCount() == 0) { m_document->setReferenceImagesLayer(nullptr, true); } } private: KisDocument *m_document; KisSharedPtr m_layer; }; struct RemoveReferenceImagesCommand : KoShapeDeleteCommand { RemoveReferenceImagesCommand(KisDocument *document, KisSharedPtr layer, QList referenceImages) : KoShapeDeleteCommand(layer->shapeController(), referenceImages) , m_document(document) , m_layer(layer) {} void redo() override { KoShapeDeleteCommand::redo(); if (m_layer->shapeCount() == 0) { m_document->setReferenceImagesLayer(nullptr, true); } } void undo() override { auto layer = m_document->referenceImagesLayer(); - KIS_SAFE_ASSERT_RECOVER_NOOP(!layer || layer == m_layer) + KIS_SAFE_ASSERT_RECOVER_NOOP(!layer || layer == m_layer); if (!layer) { m_document->setReferenceImagesLayer(m_layer, true); } KoShapeDeleteCommand::undo(); } private: KisDocument *m_document; KisSharedPtr m_layer; }; class ReferenceImagesCanvas : public KisShapeLayerCanvasBase { public: ReferenceImagesCanvas(KisReferenceImagesLayer *parent, KisImageWSP image) : KisShapeLayerCanvasBase(parent, image) , m_layer(parent) {} void updateCanvas(const QRectF &rect) override { if (!m_layer->image() || m_isDestroying) { return; } QRectF r = m_viewConverter->documentToView(rect); m_layer->signalUpdate(r); } void forceRepaint() override { m_layer->signalUpdate(m_layer->boundingImageRect()); } bool hasPendingUpdates() const override { return false; } void rerenderAfterBeingInvisible() override {} void resetCache() override {} void setImage(KisImageWSP image) override { m_viewConverter->setImage(image); } private: KisReferenceImagesLayer *m_layer; }; KisReferenceImagesLayer::KisReferenceImagesLayer(KoShapeControllerBase* shapeController, KisImageWSP image) : KisShapeLayer(shapeController, image, i18n("Reference images"), OPACITY_OPAQUE_U8, new ReferenceImagesCanvas(this, image)) {} KisReferenceImagesLayer::KisReferenceImagesLayer(const KisReferenceImagesLayer &rhs) : KisShapeLayer(rhs, rhs.shapeController(), new ReferenceImagesCanvas(this, rhs.image())) {} KUndo2Command * KisReferenceImagesLayer::addReferenceImages(KisDocument *document, const QList referenceImages) { KisSharedPtr layer = document->referenceImagesLayer(); if (!layer) { layer = new KisReferenceImagesLayer(document->shapeController(), document->image()); } return new AddReferenceImagesCommand(document, layer, referenceImages); } KUndo2Command * KisReferenceImagesLayer::removeReferenceImages(KisDocument *document, QList referenceImages) { return new RemoveReferenceImagesCommand(document, this, referenceImages); } QVector KisReferenceImagesLayer::referenceImages() const { QVector references; Q_FOREACH(auto shape, shapes()) { KisReferenceImage *referenceImage = dynamic_cast(shape); if (referenceImage) { references.append(referenceImage); } } return references; } void KisReferenceImagesLayer::paintReferences(QPainter &painter) { shapeManager()->paint(painter, *converter(), false); } bool KisReferenceImagesLayer::allowAsChild(KisNodeSP) const { return false; } bool KisReferenceImagesLayer::accept(KisNodeVisitor &visitor) { return visitor.visit(this); } void KisReferenceImagesLayer::accept(KisProcessingVisitor &visitor, KisUndoAdapter *undoAdapter) { visitor.visit(this, undoAdapter); } bool KisReferenceImagesLayer::isFakeNode() const { return true; } KUndo2Command *KisReferenceImagesLayer::setProfile(const KoColorProfile *profile) { // references should not be converted with the image Q_UNUSED(profile); return 0; } KUndo2Command *KisReferenceImagesLayer::convertTo(const KoColorSpace *dstColorSpace, KoColorConversionTransformation::Intent renderingIntent, KoColorConversionTransformation::ConversionFlags conversionFlags) { // references should not be converted with the image Q_UNUSED(dstColorSpace); Q_UNUSED(renderingIntent); Q_UNUSED(conversionFlags); return 0; } void KisReferenceImagesLayer::signalUpdate(const QRectF &rect) { emit sigUpdateCanvas(rect); } QRectF KisReferenceImagesLayer::boundingImageRect() const { return converter()->documentToView(boundingRect()); } QColor KisReferenceImagesLayer::getPixel(QPointF position) const { const QPointF docPoint = converter()->viewToDocument(position); KoShape *shape = shapeManager()->shapeAt(docPoint); if (shape) { auto *reference = dynamic_cast(shape); KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(reference, QColor()); return reference->getPixel(docPoint); } return QColor(); } diff --git a/libs/ui/flake/KisReferenceImagesLayer.h b/libs/ui/flake/KisReferenceImagesLayer.h index 8b9206001f..30764b6965 100644 --- a/libs/ui/flake/KisReferenceImagesLayer.h +++ b/libs/ui/flake/KisReferenceImagesLayer.h @@ -1,80 +1,80 @@ /* * Copyright (C) 2017 Jouni Pentikäinen * * 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 KRITA_KISREFERENCEIMAGESLAYER_H #define KRITA_KISREFERENCEIMAGESLAYER_H #include "kis_shape_layer.h" #include class KisDocument; class KRITAUI_EXPORT KisReferenceImagesLayer : public KisShapeLayer { Q_OBJECT public: KisReferenceImagesLayer(KoShapeControllerBase* shapeController, KisImageWSP image); KisReferenceImagesLayer(const KisReferenceImagesLayer &rhs); static KUndo2Command * addReferenceImages(KisDocument *document, QList referenceImages); KUndo2Command * removeReferenceImages(KisDocument *document, QList referenceImages); QVector referenceImages() const; QRectF boundingImageRect() const; QColor getPixel(QPointF position) const; void paintReferences(QPainter &painter); bool allowAsChild(KisNodeSP) const override; bool accept(KisNodeVisitor&) override; void accept(KisProcessingVisitor &visitor, KisUndoAdapter *undoAdapter) override; KisNodeSP clone() const override { return new KisReferenceImagesLayer(*this); } bool isFakeNode() const override; KUndo2Command* setProfile(const KoColorProfile *profile) override; KUndo2Command* convertTo(const KoColorSpace * dstColorSpace, KoColorConversionTransformation::Intent renderingIntent = KoColorConversionTransformation::internalRenderingIntent(), - KoColorConversionTransformation::ConversionFlags conversionFlags = KoColorConversionTransformation::internalConversionFlags()); + KoColorConversionTransformation::ConversionFlags conversionFlags = KoColorConversionTransformation::internalConversionFlags()) override; Q_SIGNALS: /** * The content of the layer has changed, and the canvas decoration * needs to update. */ void sigUpdateCanvas(const QRectF &rect); private: void signalUpdate(const QRectF &rect); friend struct AddReferenceImagesCommand; friend struct RemoveReferenceImagesCommand; friend class ReferenceImagesCanvas; }; typedef KisSharedPtr KisReferenceImagesLayerSP; #endif //KRITA_KISREFERENCEIMAGESLAYER_H diff --git a/libs/ui/flake/kis_shape_layer.cc b/libs/ui/flake/kis_shape_layer.cc index f5191fe744..4757138e8d 100644 --- a/libs/ui/flake/kis_shape_layer.cc +++ b/libs/ui/flake/kis_shape_layer.cc @@ -1,754 +1,754 @@ /* * Copyright (c) 2006-2008 Boudewijn Rempt * Copyright (c) 2007 Thomas Zander * Copyright (c) 2009 Cyrille Berger * Copyright (c) 2011 Jan Hambrecht * * 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.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "SvgWriter.h" #include "SvgParser.h" #include #include #include "kis_default_bounds.h" #include #include "kis_shape_layer_canvas.h" #include "kis_image_view_converter.h" #include #include "kis_node_visitor.h" #include "kis_processing_visitor.h" #include "kis_effect_mask.h" #include "commands/KoShapeReorderCommand.h" #include "kis_do_something_command.h" #include class ShapeLayerContainerModel : public SimpleShapeContainerModel { public: ShapeLayerContainerModel(KisShapeLayer *parent) : q(parent) {} void add(KoShape *child) override { SimpleShapeContainerModel::add(child); /** * The shape is always added with the absolute transformation set appropriately. * Here we should just squeeze it into the layer's transformation. */ KIS_SAFE_ASSERT_RECOVER_NOOP(inheritsTransform(child)); if (inheritsTransform(child)) { QTransform parentTransform = q->absoluteTransformation(0); child->applyAbsoluteTransformation(parentTransform.inverted()); } } void remove(KoShape *child) override { KIS_SAFE_ASSERT_RECOVER_NOOP(inheritsTransform(child)); if (inheritsTransform(child)) { QTransform parentTransform = q->absoluteTransformation(0); child->applyAbsoluteTransformation(parentTransform); } SimpleShapeContainerModel::remove(child); } void shapeHasBeenAddedToHierarchy(KoShape *shape, KoShapeContainer *addedToSubtree) override { q->shapeManager()->addShape(shape); SimpleShapeContainerModel::shapeHasBeenAddedToHierarchy(shape, addedToSubtree); } void shapeToBeRemovedFromHierarchy(KoShape *shape, KoShapeContainer *removedFromSubtree) override { q->shapeManager()->remove(shape); SimpleShapeContainerModel::shapeToBeRemovedFromHierarchy(shape, removedFromSubtree); } private: KisShapeLayer *q; }; struct KisShapeLayer::Private { public: Private() : canvas(0) , controller(0) , x(0) , y(0) {} KisPaintDeviceSP paintDevice; KisShapeLayerCanvasBase * canvas; KoShapeControllerBase* controller; int x; int y; }; KisShapeLayer::KisShapeLayer(KoShapeControllerBase* controller, KisImageWSP image, const QString &name, quint8 opacity) : KisExternalLayer(image, name, opacity), KoShapeLayer(new ShapeLayerContainerModel(this)), m_d(new Private()) { initShapeLayer(controller); } KisShapeLayer::KisShapeLayer(const KisShapeLayer& rhs) : KisShapeLayer(rhs, rhs.m_d->controller) { } KisShapeLayer::KisShapeLayer(const KisShapeLayer& _rhs, KoShapeControllerBase* controller, KisShapeLayerCanvasBase *canvas) : KisExternalLayer(_rhs) , KoShapeLayer(new ShapeLayerContainerModel(this)) //no _rhs here otherwise both layer have the same KoShapeContainerModel , m_d(new Private()) { // copy the projection to avoid extra round of updates! initShapeLayer(controller, _rhs.m_d->paintDevice, canvas); /** * The transformaitons of the added shapes are automatically merged into the transformation * of the layer, so we should apply this extra transform separately */ const QTransform thisInvertedTransform = this->absoluteTransformation(0).inverted(); m_d->canvas->shapeManager()->setUpdatesBlocked(true); Q_FOREACH (KoShape *shape, _rhs.shapes()) { KoShape *clonedShape = shape->cloneShape(); KIS_SAFE_ASSERT_RECOVER(clonedShape) { continue; } clonedShape->setTransformation(shape->absoluteTransformation(0) * thisInvertedTransform); addShape(clonedShape); } m_d->canvas->shapeManager()->setUpdatesBlocked(false); } KisShapeLayer::KisShapeLayer(const KisShapeLayer& _rhs, const KisShapeLayer &_addShapes) : KisExternalLayer(_rhs) , KoShapeLayer(new ShapeLayerContainerModel(this)) //no _merge here otherwise both layer have the same KoShapeContainerModel , m_d(new Private()) { // Make sure our new layer is visible otherwise the shapes cannot be painted. setVisible(true); initShapeLayer(_rhs.m_d->controller); /** * With current implementation this matrix will always be an identity, because * we do not copy the transformation from any of the source layers. But we should * handle this anyway, to not be caught by this in the future. */ const QTransform thisInvertedTransform = this->absoluteTransformation(0).inverted(); QList shapesAbove; QList shapesBelow; // copy in _rhs's shapes Q_FOREACH (KoShape *shape, _rhs.shapes()) { KoShape *clonedShape = shape->cloneShape(); KIS_SAFE_ASSERT_RECOVER(clonedShape) { continue; } clonedShape->setTransformation(shape->absoluteTransformation(0) * thisInvertedTransform); shapesBelow.append(clonedShape); } // copy in _addShapes's shapes Q_FOREACH (KoShape *shape, _addShapes.shapes()) { KoShape *clonedShape = shape->cloneShape(); KIS_SAFE_ASSERT_RECOVER(clonedShape) { continue; } clonedShape->setTransformation(shape->absoluteTransformation(0) * thisInvertedTransform); shapesAbove.append(clonedShape); } QList shapes = KoShapeReorderCommand::mergeDownShapes(shapesBelow, shapesAbove); KoShapeReorderCommand cmd(shapes); cmd.redo(); Q_FOREACH (KoShape *shape, shapesBelow + shapesAbove) { addShape(shape); } } KisShapeLayer::KisShapeLayer(KoShapeControllerBase* controller, KisImageWSP image, const QString &name, quint8 opacity, KisShapeLayerCanvasBase *canvas) : KisExternalLayer(image, name, opacity) , KoShapeLayer(new ShapeLayerContainerModel(this)) , m_d(new Private()) { initShapeLayer(controller, nullptr, canvas); } KisShapeLayer::~KisShapeLayer() { /** * Small hack alert: we should avoid updates on shape deletion */ m_d->canvas->prepareForDestroying(); Q_FOREACH (KoShape *shape, shapes()) { shape->setParent(0); delete shape; } delete m_d->canvas; delete m_d; } void KisShapeLayer::initShapeLayer(KoShapeControllerBase* controller, KisPaintDeviceSP copyFromProjection, KisShapeLayerCanvasBase *canvas) { setSupportsLodMoves(false); setShapeId(KIS_SHAPE_LAYER_ID); KIS_ASSERT_RECOVER_NOOP(this->image()); if (!copyFromProjection) { m_d->paintDevice = new KisPaintDevice(image()->colorSpace()); m_d->paintDevice->setDefaultBounds(new KisDefaultBounds(this->image())); m_d->paintDevice->setParentNode(this); } else { m_d->paintDevice = new KisPaintDevice(*copyFromProjection); } if (!canvas) { auto *slCanvas = new KisShapeLayerCanvas(this, image()); slCanvas->setProjection(m_d->paintDevice); canvas = slCanvas; } m_d->canvas = canvas; m_d->canvas->moveToThread(this->thread()); m_d->controller = controller; m_d->canvas->shapeManager()->selection()->disconnect(this); connect(m_d->canvas->selectedShapesProxy(), SIGNAL(selectionChanged()), this, SIGNAL(selectionChanged())); connect(m_d->canvas->selectedShapesProxy(), SIGNAL(currentLayerChanged(const KoShapeLayer*)), this, SIGNAL(currentLayerChanged(const KoShapeLayer*))); connect(this, SIGNAL(sigMoveShapes(QPointF)), SLOT(slotMoveShapes(QPointF))); } bool KisShapeLayer::allowAsChild(KisNodeSP node) const { return node->inherits("KisMask"); } void KisShapeLayer::setImage(KisImageWSP _image) { KisLayer::setImage(_image); m_d->canvas->setImage(_image); m_d->paintDevice->convertTo(_image->colorSpace()); m_d->paintDevice->setDefaultBounds(new KisDefaultBounds(_image)); } KisLayerSP KisShapeLayer::createMergedLayerTemplate(KisLayerSP prevLayer) { KisShapeLayer *prevShape = dynamic_cast(prevLayer.data()); if (prevShape) return new KisShapeLayer(*prevShape, *this); else return KisExternalLayer::createMergedLayerTemplate(prevLayer); } void KisShapeLayer::fillMergedLayerTemplate(KisLayerSP dstLayer, KisLayerSP prevLayer) { if (!dynamic_cast(dstLayer.data())) { KisLayer::fillMergedLayerTemplate(dstLayer, prevLayer); } } void KisShapeLayer::setParent(KoShapeContainer *parent) { Q_UNUSED(parent) - KIS_ASSERT_RECOVER_RETURN(0) + KIS_ASSERT_RECOVER_RETURN(0); } QIcon KisShapeLayer::icon() const { return KisIconUtils::loadIcon("vectorLayer"); } KisPaintDeviceSP KisShapeLayer::original() const { return m_d->paintDevice; } KisPaintDeviceSP KisShapeLayer::paintDevice() const { return 0; } qint32 KisShapeLayer::x() const { return m_d->x; } qint32 KisShapeLayer::y() const { return m_d->y; } void KisShapeLayer::setX(qint32 x) { qint32 delta = x - this->x(); QPointF diff = QPointF(m_d->canvas->viewConverter()->viewToDocumentX(delta), 0); emit sigMoveShapes(diff); // Save new value to satisfy LSP m_d->x = x; } void KisShapeLayer::setY(qint32 y) { qint32 delta = y - this->y(); QPointF diff = QPointF(0, m_d->canvas->viewConverter()->viewToDocumentY(delta)); emit sigMoveShapes(diff); // Save new value to satisfy LSP m_d->y = y; } namespace { void filterTransformableShapes(QList &shapes) { auto it = shapes.begin(); while (it != shapes.end()) { if (shapes.size() == 1) break; if ((*it)->inheritsTransformFromAny(shapes)) { it = shapes.erase(it); } else { ++it; } } } } QList KisShapeLayer::shapesToBeTransformed() { QList shapes = shapeManager()->shapes(); // We expect that **all** the shapes inherit the transform from its parent // SANITY_CHECK: we expect all the shapes inside the // shape layer to inherit transform! Q_FOREACH (KoShape *shape, shapes) { if (shape->parent()) { KIS_SAFE_ASSERT_RECOVER(shape->parent()->inheritsTransform(shape)) { break; } } } shapes << this; filterTransformableShapes(shapes); return shapes; } void KisShapeLayer::slotMoveShapes(const QPointF &diff) { QList shapes = shapesToBeTransformed(); if (shapes.isEmpty()) return; KoShapeMoveCommand cmd(shapes, diff); cmd.redo(); } bool KisShapeLayer::accept(KisNodeVisitor& visitor) { return visitor.visit(this); } void KisShapeLayer::accept(KisProcessingVisitor &visitor, KisUndoAdapter *undoAdapter) { return visitor.visit(this, undoAdapter); } KoShapeManager* KisShapeLayer::shapeManager() const { return m_d->canvas->shapeManager(); } KoViewConverter* KisShapeLayer::converter() const { return m_d->canvas->viewConverter(); } bool KisShapeLayer::visible(bool recursive) const { return KisExternalLayer::visible(recursive); } void KisShapeLayer::setVisible(bool visible, bool isLoading) { const bool oldVisible = this->visible(false); KoShapeLayer::setVisible(visible); KisExternalLayer::setVisible(visible, isLoading); if (visible && !oldVisible && m_d->canvas->hasChangedWhileBeingInvisible()) { m_d->canvas->rerenderAfterBeingInvisible(); } } void KisShapeLayer::setUserLocked(bool value) { KoShapeLayer::setGeometryProtected(value); KisExternalLayer::setUserLocked(value); } bool KisShapeLayer::isShapeEditable(bool recursive) const { return KoShapeLayer::isShapeEditable(recursive) && isEditable(true); } // we do not override KoShape::setGeometryProtected() as we consider // the user not being able to access the layer shape from Krita UI! void KisShapeLayer::forceUpdateTimedNode() { m_d->canvas->forceRepaint(); } bool KisShapeLayer::hasPendingTimedUpdates() const { return m_d->canvas->hasPendingUpdates(); } void KisShapeLayer::forceUpdateHiddenAreaOnOriginal() { m_d->canvas->forceRepaintWithHiddenAreas(); } bool KisShapeLayer::saveShapesToStore(KoStore *store, QList shapes, const QSizeF &sizeInPt) { if (!store->open("content.svg")) { return false; } KoStoreDevice storeDev(store); storeDev.open(QIODevice::WriteOnly); std::sort(shapes.begin(), shapes.end(), KoShape::compareShapeZIndex); SvgWriter writer(shapes); writer.save(storeDev, sizeInPt); if (!store->close()) { return false; } return true; } QList KisShapeLayer::createShapesFromSvg(QIODevice *device, const QString &baseXmlDir, const QRectF &rectInPixels, qreal resolutionPPI, KoDocumentResourceManager *resourceManager, QSizeF *fragmentSize) { QString errorMsg; int errorLine = 0; int errorColumn; KoXmlDocument doc = SvgParser::createDocumentFromSvg(device, &errorMsg, &errorLine, &errorColumn); if (doc.isNull()) { errKrita << "Parsing error in " << "contents.svg" << "! Aborting!" << endl << " In line: " << errorLine << ", column: " << errorColumn << endl << " Error message: " << errorMsg << endl; errKrita << i18n("Parsing error in the main document at line %1, column %2\nError message: %3" , errorLine , errorColumn , errorMsg); } SvgParser parser(resourceManager); parser.setXmlBaseDir(baseXmlDir); parser.setResolution(rectInPixels /* px */, resolutionPPI /* ppi */); return parser.parseSvg(doc.documentElement(), fragmentSize); } bool KisShapeLayer::saveLayer(KoStore * store) const { // FIXME: we handle xRes() only! const QSizeF sizeInPx = image()->bounds().size(); const QSizeF sizeInPt(sizeInPx.width() / image()->xRes(), sizeInPx.height() / image()->yRes()); return saveShapesToStore(store, this->shapes(), sizeInPt); } bool KisShapeLayer::loadSvg(QIODevice *device, const QString &baseXmlDir) { QSizeF fragmentSize; // unused! KisImageSP image = this->image(); // FIXME: we handle xRes() only! KIS_SAFE_ASSERT_RECOVER_NOOP(qFuzzyCompare(image->xRes(), image->yRes())); const qreal resolutionPPI = 72.0 * image->xRes(); QList shapes = createShapesFromSvg(device, baseXmlDir, image->bounds(), resolutionPPI, m_d->controller->resourceManager(), &fragmentSize); Q_FOREACH (KoShape *shape, shapes) { addShape(shape); } return true; } bool KisShapeLayer::loadLayer(KoStore* store) { if (!store) { warnKrita << i18n("No store backend"); return false; } if (store->open("content.svg")) { KoStoreDevice storeDev(store); storeDev.open(QIODevice::ReadOnly); loadSvg(&storeDev, ""); store->close(); return true; } KoOdfReadStore odfStore(store); QString errorMessage; odfStore.loadAndParse(errorMessage); if (!errorMessage.isEmpty()) { warnKrita << errorMessage; return false; } KoXmlElement contents = odfStore.contentDoc().documentElement(); // dbgKrita <<"Start loading OASIS document..." << contents.text(); // dbgKrita <<"Start loading OASIS contents..." << contents.lastChild().localName(); // dbgKrita <<"Start loading OASIS contents..." << contents.lastChild().namespaceURI(); // dbgKrita <<"Start loading OASIS contents..." << contents.lastChild().isElement(); KoXmlElement body(KoXml::namedItemNS(contents, KoXmlNS::office, "body")); if (body.isNull()) { //setErrorMessage( i18n( "Invalid OASIS document. No office:body tag found." ) ); return false; } body = KoXml::namedItemNS(body, KoXmlNS::office, "drawing"); if (body.isNull()) { //setErrorMessage( i18n( "Invalid OASIS document. No office:drawing tag found." ) ); return false; } KoXmlElement page(KoXml::namedItemNS(body, KoXmlNS::draw, "page")); if (page.isNull()) { //setErrorMessage( i18n( "Invalid OASIS document. No draw:page tag found." ) ); return false; } KoXmlElement * master = 0; if (odfStore.styles().masterPages().contains("Standard")) master = odfStore.styles().masterPages().value("Standard"); else if (odfStore.styles().masterPages().contains("Default")) master = odfStore.styles().masterPages().value("Default"); else if (! odfStore.styles().masterPages().empty()) master = odfStore.styles().masterPages().begin().value(); if (master) { const KoXmlElement *style = odfStore.styles().findStyle( master->attributeNS(KoXmlNS::style, "page-layout-name", QString())); KoPageLayout pageLayout; pageLayout.loadOdf(*style); setSize(QSizeF(pageLayout.width, pageLayout.height)); } // We work fine without a master page KoOdfLoadingContext context(odfStore.styles(), odfStore.store()); context.setManifestFile(QString("tar:/") + odfStore.store()->currentPath() + "META-INF/manifest.xml"); KoShapeLoadingContext shapeContext(context, m_d->controller->resourceManager()); KoXmlElement layerElement; forEachElement(layerElement, context.stylesReader().layerSet()) { // FIXME: investigate what is this // KoShapeLayer * l = new KoShapeLayer(); if (!loadOdf(layerElement, shapeContext)) { dbgKrita << "Could not load vector layer!"; return false; } } KoXmlElement child; forEachElement(child, page) { KoShape * shape = KoShapeRegistry::instance()->createShapeFromOdf(child, shapeContext); if (shape) { addShape(shape); } } return true; } void KisShapeLayer::resetCache() { m_d->canvas->resetCache(); } KUndo2Command* KisShapeLayer::crop(const QRect & rect) { QPoint oldPos(x(), y()); QPoint newPos = oldPos - rect.topLeft(); return new KisNodeMoveCommand2(this, oldPos, newPos); } KUndo2Command* KisShapeLayer::transform(const QTransform &transform) { QList shapes = shapesToBeTransformed(); if (shapes.isEmpty()) return 0; KisImageViewConverter *converter = dynamic_cast(this->converter()); QTransform realTransform = converter->documentToView() * transform * converter->viewToDocument(); QList oldTransformations; QList newTransformations; QList newShadows; const qreal transformBaseScale = KoUnit::approxTransformScale(transform); Q_FOREACH (const KoShape* shape, shapes) { QTransform oldTransform = shape->transformation(); oldTransformations.append(oldTransform); QTransform globalTransform = shape->absoluteTransformation(0); QTransform localTransform = globalTransform * realTransform * globalTransform.inverted(); newTransformations.append(localTransform * oldTransform); KoShapeShadow *shadow = 0; if (shape->shadow()) { shadow = new KoShapeShadow(*shape->shadow()); shadow->setOffset(transformBaseScale * shadow->offset()); shadow->setBlur(transformBaseScale * shadow->blur()); } newShadows.append(shadow); } KUndo2Command *parentCommand = new KUndo2Command(); new KoShapeTransformCommand(shapes, oldTransformations, newTransformations, parentCommand); new KoShapeShadowCommand(shapes, newShadows, parentCommand); return parentCommand; } KUndo2Command *KisShapeLayer::setProfile(const KoColorProfile *profile) { using namespace KisDoSomethingCommandOps; KUndo2Command *cmd = new KUndo2Command(); new KisDoSomethingCommand(this, false, cmd); m_d->paintDevice->setProfile(profile, cmd); new KisDoSomethingCommand(this, true, cmd); return cmd; } KUndo2Command *KisShapeLayer::convertTo(const KoColorSpace *dstColorSpace, KoColorConversionTransformation::Intent renderingIntent, KoColorConversionTransformation::ConversionFlags conversionFlags) { using namespace KisDoSomethingCommandOps; KUndo2Command *cmd = new KUndo2Command(); new KisDoSomethingCommand(this, false, cmd); m_d->paintDevice->convertTo(dstColorSpace, renderingIntent, conversionFlags, cmd); new KisDoSomethingCommand(this, true, cmd); return cmd; } KoShapeControllerBase *KisShapeLayer::shapeController() const { return m_d->controller; } diff --git a/libs/ui/flake/kis_shape_layer.h b/libs/ui/flake/kis_shape_layer.h index aa6f2083a4..fa55e2d73d 100644 --- a/libs/ui/flake/kis_shape_layer.h +++ b/libs/ui/flake/kis_shape_layer.h @@ -1,207 +1,207 @@ /* * Copyright (c) 2006 Boudewijn Rempt * Copyright (c) 2007 Thomas Zander * * 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. */ #ifndef KIS_SHAPE_LAYER_H_ #define KIS_SHAPE_LAYER_H_ #include #include #include #include #include #include class QRect; class QIcon; class QRect; class QString; class KoShapeManager; class KoStore; class KoViewConverter; class KoShapeControllerBase; class KoDocumentResourceManager; class KisShapeLayerCanvasBase; const QString KIS_SHAPE_LAYER_ID = "KisShapeLayer"; /** A KisShapeLayer contains any number of non-krita flakes, such as path shapes, text shapes and anything else people come up with. The KisShapeLayer has a shapemanager and a canvas of its own. The canvas paints onto the projection, and the projection is what we render in Krita. This means that no matter how many views you have, you cannot have a different view on your shapes per view. XXX: what about removing shapes? */ class KRITAUI_EXPORT KisShapeLayer : public KisExternalLayer, public KoShapeLayer, public KisDelayedUpdateNodeInterface, public KisCroppedOriginalLayerInterface { Q_OBJECT public: KisShapeLayer(KoShapeControllerBase* shapeController, KisImageWSP image, const QString &name, quint8 opacity); KisShapeLayer(const KisShapeLayer& _rhs); KisShapeLayer(const KisShapeLayer& _rhs, KoShapeControllerBase* controller, KisShapeLayerCanvasBase *canvas = 0); /** * Merge constructor. * * Creates a new layer as a merge of two existing layers. * * This is used by createMergedLayer() */ KisShapeLayer(const KisShapeLayer& _merge, const KisShapeLayer &_addShapes); ~KisShapeLayer() override; protected: KisShapeLayer(KoShapeControllerBase* shapeController, KisImageWSP image, const QString &name, quint8 opacity, KisShapeLayerCanvasBase *canvas); private: void initShapeLayer(KoShapeControllerBase* controller, KisPaintDeviceSP copyFromProjection = 0, KisShapeLayerCanvasBase *canvas = 0); public: KisNodeSP clone() const override { return new KisShapeLayer(*this); } bool allowAsChild(KisNodeSP) const override; void setImage(KisImageWSP image) override; KisLayerSP createMergedLayerTemplate(KisLayerSP prevLayer) override; void fillMergedLayerTemplate(KisLayerSP dstLayer, KisLayerSP prevLayer) override; public: // KoShape overrides bool isSelectable() const { return false; } void setParent(KoShapeContainer *parent); // KisExternalLayer implementation QIcon icon() const override; void resetCache() override; KisPaintDeviceSP original() const override; KisPaintDeviceSP paintDevice() const override; qint32 x() const override; qint32 y() const override; void setX(qint32) override; void setY(qint32) override; bool accept(KisNodeVisitor&) override; void accept(KisProcessingVisitor &visitor, KisUndoAdapter *undoAdapter) override; KoShapeManager *shapeManager() const; static bool saveShapesToStore(KoStore *store, QList shapes, const QSizeF &sizeInPt); static QList createShapesFromSvg(QIODevice *device, const QString &baseXmlDir, const QRectF &rectInPixels, qreal resolutionPPI, KoDocumentResourceManager *resourceManager, QSizeF *fragmentSize); bool saveLayer(KoStore * store) const; bool loadLayer(KoStore* store); KUndo2Command* crop(const QRect & rect) override; KUndo2Command* transform(const QTransform &transform) override; KUndo2Command* setProfile(const KoColorProfile *profile) override; KUndo2Command* convertTo(const KoColorSpace * dstColorSpace, KoColorConversionTransformation::Intent renderingIntent = KoColorConversionTransformation::internalRenderingIntent(), - KoColorConversionTransformation::ConversionFlags conversionFlags = KoColorConversionTransformation::internalConversionFlags()); + KoColorConversionTransformation::ConversionFlags conversionFlags = KoColorConversionTransformation::internalConversionFlags()) override; bool visible(bool recursive = false) const override; void setVisible(bool visible, bool isLoading = false) override; void setUserLocked(bool value) override; bool isShapeEditable(bool recursive) const override; /** * Forces a repaint of a shape layer without waiting for an event loop * calling a delayed timer update. If you want to see the result of the shape * layer right here and right now, you should do: * * shapeLayer->setDirty(); * shapeLayer->image()->waitForDone(); * shapeLayer->forceUpdateTimedNode(); * shapeLayer->image()->waitForDone(); * */ void forceUpdateTimedNode() override; /** * \return true if there are any pending updates in the delayed queue */ bool hasPendingTimedUpdates() const override; void forceUpdateHiddenAreaOnOriginal() override; protected: using KoShape::isVisible; bool loadSvg(QIODevice *device, const QString &baseXmlDir); friend class ShapeLayerContainerModel; KoViewConverter* converter() const; KoShapeControllerBase *shapeController() const; Q_SIGNALS: /** * These signals are forwarded from the local shape manager * This is done because we switch KoShapeManager and therefore * KoSelection in KisCanvas2, so we need to connect local managers * to the UI as well. * * \see comment in the constructor of KisCanvas2 */ void selectionChanged(); void currentLayerChanged(const KoShapeLayer *layer); Q_SIGNALS: /** * A signal + slot to synchronize UI and image * threads. Image thread emits the signal, UI * thread performs the action */ void sigMoveShapes(const QPointF &diff); private Q_SLOTS: void slotMoveShapes(const QPointF &diff); private: QList shapesToBeTransformed(); private: struct Private; Private * const m_d; }; #endif diff --git a/libs/ui/flake/kis_shape_layer_canvas.cpp b/libs/ui/flake/kis_shape_layer_canvas.cpp index d7879c26d5..3676366f82 100644 --- a/libs/ui/flake/kis_shape_layer_canvas.cpp +++ b/libs/ui/flake/kis_shape_layer_canvas.cpp @@ -1,419 +1,426 @@ /* * 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" //#define DEBUG_REPAINT 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_canvasUpdateCompressor(100, KisSignalCompressor::FIRST_INACTIVE) , m_asyncUpdateSignalCompressor(100, KisSignalCompressor::FIRST_INACTIVE) { /** * 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())); connect(this, SIGNAL(forwardRepaint()), &m_canvasUpdateCompressor, SLOT(start())); connect(&m_canvasUpdateCompressor, SIGNAL(timeout()), this, SLOT(slotStartDirectSyncRepaint())); 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(); } updateUpdateCompressorDelay(); } #ifdef DEBUG_REPAINT # include #endif 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; } } /** * HACK ALERT! * * The shapes may be accessed from both, GUI and worker threads! And we have no real * guard against this until the vector tools will be ported to the strokes framework. * * Here we just avoid the most obvious conflict of threads: * * 1) If the layer is modified by a non-gui (worker) thread, use a spontaneous jobs * to rerender the canvas. The job will be executed (almost) exclusively and it is * the responsibility of the worker thread to add a barrier to wait until this job is * completed, and not try to access the shapes concurrently. * * 2) If the layer is modified by a gui thread, it means that we are being accessed by * a legacy vector tool. It this case just emit a queued signal to make sure the updates * are compressed a little bit (TODO: add a compressor?) */ if (qApp->thread() == QThread::currentThread()) { emit forwardRepaint(); m_hasDirectSyncRepaintInitiated = true; } else { m_asyncUpdateSignalCompressor.start(); m_hasUpdateInCompressor = true; } } void KisShapeLayerCanvas::updateCanvas(const QRectF& rc) { updateCanvas(QVector({rc})); } void KisShapeLayerCanvas::slotStartAsyncRepaint() { m_hasUpdateInCompressor = false; m_image->addSpontaneousJob(new KisRepaintShapeLayerLayerJob(m_parentLayer, this)); } void KisShapeLayerCanvas::slotStartDirectSyncRepaint() { m_hasDirectSyncRepaintInitiated = false; repaint(); } void KisShapeLayerCanvas::slotImageSizeChanged() { QRegion dirtyCacheRegion; dirtyCacheRegion += m_image->bounds(); dirtyCacheRegion += m_cachedImageRect; dirtyCacheRegion -= m_image->bounds() & m_cachedImageRect; QVector dirtyRects; Q_FOREACH (const QRect &rc, dirtyCacheRegion.rects()) { dirtyRects.append(m_viewConverter->viewToDocument(rc)); } updateCanvas(dirtyRects); m_cachedImageRect = m_image->bounds(); updateUpdateCompressorDelay(); } void KisShapeLayerCanvas::repaint() { QRect repaintRect; bool forceUpdateHiddenAreasOnly = false; { QMutexLocker locker(&m_dirtyRegionMutex); repaintRect = m_dirtyRegion.boundingRect(); forceUpdateHiddenAreasOnly = m_forceUpdateHiddenAreasOnly; 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() repaintRect = repaintRect.intersected(m_parentLayer->image()->bounds()); } else { const QRectF shapesBounds = KoShape::boundingRect(m_shapeManager->shapes()); repaintRect = kisGrowRect(m_viewConverter->documentToView(shapesBounds).toAlignedRect(), 2); } const QRect r = repaintRect; 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()]; for (qint32 x = r.x(); x < r.x() + r.width(); x += MASK_IMAGE_WIDTH) { for (qint32 y = r.y(); y < r.y() + r.height(); y += MASK_IMAGE_HEIGHT) { image.fill(0); tempPainter.translate(-x, -y); tempPainter.setClipRect(QRect(x,y,MASK_IMAGE_WIDTH,MASK_IMAGE_HEIGHT)); #ifdef DEBUG_REPAINT QColor color = QColor(random() % 255, random() % 255, random() % 255); maskPainter.fillRect(srcRect, color); #endif m_shapeManager->paint(tempPainter, *m_viewConverter, false); tempPainter.translate(x, y); KoColorSpaceRegistry::instance()->rgb8() ->convertPixelsTo(image.constBits(), dstData, m_projection->colorSpace(), MASK_IMAGE_WIDTH * MASK_IMAGE_HEIGHT, KoColorConversionTransformation::internalRenderingIntent(), KoColorConversionTransformation::internalConversionFlags()); m_projection->writeBytes(dstData, x, y, MASK_IMAGE_WIDTH, MASK_IMAGE_HEIGHT); } } 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(); slotStartAsyncRepaint(); } } bool KisShapeLayerCanvas::hasPendingUpdates() const { return m_hasUpdateInCompressor || m_hasDirectSyncRepaintInitiated; } 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(); slotStartAsyncRepaint(); } 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)) + KIS_SAFE_ASSERT_RECOVER_RETURN(m_parentLayer->visible(true)); m_hasChangedWhileBeingInvisible = false; resetCache(); } void KisShapeLayerCanvas::updateUpdateCompressorDelay() { if (m_cachedImageRect.width() * m_cachedImageRect.height() < 2480 * 3508) { // A4 300 DPI m_canvasUpdateCompressor.setDelay(25); } else if (m_cachedImageRect.width() * m_cachedImageRect.height() < 4961 * 7061) { // A4 600 DPI m_canvasUpdateCompressor.setDelay(100); } else { // Really big m_canvasUpdateCompressor.setDelay(500); } } diff --git a/libs/ui/flake/kis_shape_selection.cpp b/libs/ui/flake/kis_shape_selection.cpp index e5f12e3ff6..4af0df5092 100644 --- a/libs/ui/flake/kis_shape_selection.cpp +++ b/libs/ui/flake/kis_shape_selection.cpp @@ -1,408 +1,416 @@ /* * Copyright (c) 2010 Sven Langkamp * Copyright (c) 2011 Jan Hambrecht * * 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_selection.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kis_shape_selection_model.h" #include "kis_shape_selection_canvas.h" #include "kis_take_all_shapes_command.h" #include "kis_image_view_converter.h" #include "kis_shape_layer.h" #include KisShapeSelection::KisShapeSelection(KoShapeControllerBase *shapeControllerBase, KisImageWSP image, KisSelectionWSP selection) : KoShapeLayer(m_model = new KisShapeSelectionModel(image, selection, this)) , m_image(image) , m_shapeControllerBase(shapeControllerBase) { Q_ASSERT(m_image); setShapeId("KisShapeSelection"); setSelectable(false); m_converter = new KisImageViewConverter(image); m_canvas = new KisShapeSelectionCanvas(shapeControllerBase); m_canvas->shapeManager()->addShape(this); m_model->setObjectName("KisShapeSelectionModel"); m_model->moveToThread(image->thread()); m_canvas->setObjectName("KisShapeSelectionCanvas"); m_canvas->moveToThread(image->thread()); connect(this, SIGNAL(sigMoveShapes(QPointF)), SLOT(slotMoveShapes(QPointF))); } KisShapeSelection::~KisShapeSelection() { m_model->setShapeSelection(0); delete m_canvas; delete m_converter; } KisShapeSelection::KisShapeSelection(const KisShapeSelection& rhs, KisSelection* selection) : KoShapeLayer(m_model = new KisShapeSelectionModel(rhs.m_image, selection, this)) { m_image = rhs.m_image; m_shapeControllerBase = rhs.m_shapeControllerBase; m_converter = new KisImageViewConverter(m_image); m_canvas = new KisShapeSelectionCanvas(m_shapeControllerBase); - m_canvas->shapeManager()->addShape(this); + // TODO: refactor shape selection to pass signals + // via KoShapeManager, not via the model + m_canvas->shapeManager()->setUpdatesBlocked(true); + m_model->setUpdatesEnabled(false); + + m_canvas->shapeManager()->addShape(this); Q_FOREACH (KoShape *shape, rhs.shapes()) { KoShape *clonedShape = shape->cloneShape(); KIS_SAFE_ASSERT_RECOVER(clonedShape) { continue; } this->addShape(clonedShape); } + + m_canvas->shapeManager()->setUpdatesBlocked(false); + m_model->setUpdatesEnabled(true); } KisSelectionComponent* KisShapeSelection::clone(KisSelection* selection) { /** * TODO: make cloning of vector selections safe! Right now it crashes * on Windows because of manipulations with timers from non-gui thread. */ KIS_SAFE_ASSERT_RECOVER_NOOP(QThread::currentThread() == qApp->thread()); return new KisShapeSelection(*this, selection); } bool KisShapeSelection::saveSelection(KoStore * store) const { const QSizeF sizeInPx = m_image->bounds().size(); const QSizeF sizeInPt(sizeInPx.width() / m_image->xRes(), sizeInPx.height() / m_image->yRes()); return KisShapeLayer::saveShapesToStore(store, this->shapes(), sizeInPt); } bool KisShapeSelection::loadSelection(KoStore* store) { QSizeF fragmentSize; // unused! // FIXME: we handle xRes() only! KIS_SAFE_ASSERT_RECOVER_NOOP(qFuzzyCompare(m_image->xRes(), m_image->yRes())); const qreal resolutionPPI = 72.0 * m_image->xRes(); QList shapes; if (store->open("content.svg")) { KoStoreDevice storeDev(store); storeDev.open(QIODevice::ReadOnly); shapes = KisShapeLayer::createShapesFromSvg(&storeDev, "", m_image->bounds(), resolutionPPI, m_canvas->shapeController()->resourceManager(), &fragmentSize); store->close(); Q_FOREACH (KoShape *shape, shapes) { addShape(shape); } return true; } KoOdfReadStore odfStore(store); QString errorMessage; odfStore.loadAndParse(errorMessage); if (!errorMessage.isEmpty()) { dbgKrita << errorMessage; return false; } KoXmlElement contents = odfStore.contentDoc().documentElement(); // dbgKrita <<"Start loading OASIS document..." << contents.text(); // dbgKrita <<"Start loading OASIS contents..." << contents.lastChild().localName(); // dbgKrita <<"Start loading OASIS contents..." << contents.lastChild().namespaceURI(); // dbgKrita <<"Start loading OASIS contents..." << contents.lastChild().isElement(); KoXmlElement body(KoXml::namedItemNS(contents, KoXmlNS::office, "body")); if (body.isNull()) { dbgKrita << "No office:body found!"; //setErrorMessage( i18n( "Invalid OASIS document. No office:body tag found." ) ); return false; } body = KoXml::namedItemNS(body, KoXmlNS::office, "drawing"); if (body.isNull()) { dbgKrita << "No office:drawing found!"; //setErrorMessage( i18n( "Invalid OASIS document. No office:drawing tag found." ) ); return false; } KoXmlElement page(KoXml::namedItemNS(body, KoXmlNS::draw, "page")); if (page.isNull()) { dbgKrita << "No office:drawing found!"; //setErrorMessage( i18n( "Invalid OASIS document. No draw:page tag found." ) ); return false; } KoXmlElement * master = 0; if (odfStore.styles().masterPages().contains("Standard")) master = odfStore.styles().masterPages().value("Standard"); else if (odfStore.styles().masterPages().contains("Default")) master = odfStore.styles().masterPages().value("Default"); else if (! odfStore.styles().masterPages().empty()) master = odfStore.styles().masterPages().begin().value(); if (master) { const KoXmlElement *style = odfStore.styles().findStyle( master->attributeNS(KoXmlNS::style, "page-layout-name", QString())); KoPageLayout pageLayout; pageLayout.loadOdf(*style); setSize(QSizeF(pageLayout.width, pageLayout.height)); } else { dbgKrita << "No master page found!"; return false; } KoOdfLoadingContext context(odfStore.styles(), odfStore.store()); KoShapeLoadingContext shapeContext(context, 0); KoXmlElement layerElement; forEachElement(layerElement, context.stylesReader().layerSet()) { if (!loadOdf(layerElement, shapeContext)) { dbgKrita << "Could not load vector layer!"; return false; } } KoXmlElement child; forEachElement(child, page) { KoShape * shape = KoShapeRegistry::instance()->createShapeFromOdf(child, shapeContext); if (shape) { addShape(shape); } } return true; } void KisShapeSelection::setUpdatesEnabled(bool enabled) { m_model->setUpdatesEnabled(enabled); } bool KisShapeSelection::updatesEnabled() const { return m_model->updatesEnabled(); } KUndo2Command* KisShapeSelection::resetToEmpty() { return new KisTakeAllShapesCommand(this, true); } bool KisShapeSelection::isEmpty() const { return !m_model->count(); } QPainterPath KisShapeSelection::outlineCache() const { return m_outline; } bool KisShapeSelection::outlineCacheValid() const { return true; } void KisShapeSelection::recalculateOutlineCache() { QList shapesList = shapes(); QPainterPath outline; Q_FOREACH (KoShape * shape, shapesList) { QTransform shapeMatrix = shape->absoluteTransformation(0); outline = outline.united(shapeMatrix.map(shape->outline())); } QTransform resolutionMatrix; resolutionMatrix.scale(m_image->xRes(), m_image->yRes()); m_outline = resolutionMatrix.map(outline); } void KisShapeSelection::paintComponent(QPainter& painter, const KoViewConverter& converter, KoShapePaintingContext &) { Q_UNUSED(painter); Q_UNUSED(converter); } void KisShapeSelection::renderToProjection(KisPaintDeviceSP projection) { Q_ASSERT(projection); Q_ASSERT(m_image); QRectF boundingRect = outlineCache().boundingRect(); renderSelection(projection, boundingRect.toAlignedRect()); } void KisShapeSelection::renderToProjection(KisPaintDeviceSP projection, const QRect& r) { Q_ASSERT(projection); renderSelection(projection, r); } void KisShapeSelection::renderSelection(KisPaintDeviceSP projection, const QRect& requestedRect) { KIS_SAFE_ASSERT_RECOVER_RETURN(projection); KIS_SAFE_ASSERT_RECOVER_RETURN(m_image); const qint32 MASK_IMAGE_WIDTH = 256; const qint32 MASK_IMAGE_HEIGHT = 256; const QPainterPath selectionOutline = outlineCache(); if (*projection->defaultPixel().data() == OPACITY_TRANSPARENT_U8) { projection->clear(requestedRect); } else { KoColor transparentColor(Qt::transparent, projection->colorSpace()); projection->fill(requestedRect, transparentColor); } const QRect r = requestedRect & selectionOutline.boundingRect().toAlignedRect(); QImage polygonMaskImage(MASK_IMAGE_WIDTH, MASK_IMAGE_HEIGHT, QImage::Format_ARGB32); QPainter maskPainter(&polygonMaskImage); maskPainter.setRenderHint(QPainter::Antialiasing, true); // Break the mask up into chunks so we don't have to allocate a potentially very large QImage. for (qint32 x = r.x(); x < r.x() + r.width(); x += MASK_IMAGE_WIDTH) { for (qint32 y = r.y(); y < r.y() + r.height(); y += MASK_IMAGE_HEIGHT) { maskPainter.fillRect(polygonMaskImage.rect(), Qt::black); maskPainter.translate(-x, -y); maskPainter.fillPath(selectionOutline, Qt::white); maskPainter.translate(x, y); qint32 rectWidth = qMin(r.x() + r.width() - x, MASK_IMAGE_WIDTH); qint32 rectHeight = qMin(r.y() + r.height() - y, MASK_IMAGE_HEIGHT); KisSequentialIterator it(projection, QRect(x, y, rectWidth, rectHeight)); while (it.nextPixel()) { (*it.rawData()) = qRed(polygonMaskImage.pixel(it.x() - x, it.y() - y)); } } } } KoShapeManager* KisShapeSelection::shapeManager() const { return m_canvas->shapeManager(); } KisShapeSelectionFactory::KisShapeSelectionFactory() : KoShapeFactoryBase("KisShapeSelection", "selection shape container") { setHidden(true); } void KisShapeSelection::moveX(qint32 x) { const QPointF diff(x / m_image->xRes(), 0); emit sigMoveShapes(diff); } void KisShapeSelection::moveY(qint32 y) { const QPointF diff(0, y / m_image->yRes()); emit sigMoveShapes(diff); } void KisShapeSelection::slotMoveShapes(const QPointF &diff) { Q_FOREACH (KoShape* shape, shapeManager()->shapes()) { if (shape != this) { QPointF pos = shape->position(); shape->setPosition(pos + diff); } } } // TODO same code as in vector layer, refactor! KUndo2Command* KisShapeSelection::transform(const QTransform &transform) { QList shapes = m_canvas->shapeManager()->shapes(); if(shapes.isEmpty()) return 0; QTransform realTransform = m_converter->documentToView() * transform * m_converter->viewToDocument(); QList oldTransformations; QList newTransformations; // this code won't work if there are shapes, that inherit the transformation from the parent container. // the chart and tree shapes are examples for that, but they aren't used in krita and there are no other shapes like that. Q_FOREACH (const KoShape* shape, shapes) { QTransform oldTransform = shape->transformation(); oldTransformations.append(oldTransform); if (dynamic_cast(shape)) { newTransformations.append(oldTransform); } else { QTransform globalTransform = shape->absoluteTransformation(0); QTransform localTransform = globalTransform * realTransform * globalTransform.inverted(); newTransformations.append(localTransform*oldTransform); } } return new KoShapeTransformCommand(shapes, oldTransformations, newTransformations); } diff --git a/libs/ui/kis_png_converter.cpp b/libs/ui/kis_png_converter.cpp index a7711be368..a69cfe2817 100644 --- a/libs/ui/kis_png_converter.cpp +++ b/libs/ui/kis_png_converter.cpp @@ -1,1394 +1,1394 @@ /* * Copyright (c) 2005-2007 Cyrille Berger * * 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_png_converter.h" // A big thank to Glenn Randers-Pehrson for his wonderful // documentation of libpng available at // http://www.libpng.org/pub/png/libpng-1.2.5-manual.html #ifndef PNG_MAX_UINT // Removed in libpng 1.4 #define PNG_MAX_UINT PNG_UINT_31_MAX #endif #include // WORDS_BIGENDIAN #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "dialogs/kis_dlg_png_import.h" #include "kis_clipboard.h" #include #include "kis_undo_stores.h" #include namespace { int getColorTypeforColorSpace(const KoColorSpace * cs , bool alpha) { QString id = cs->id(); if (id == "GRAYA" || id == "GRAYAU16" || id == "GRAYA16") { return alpha ? PNG_COLOR_TYPE_GRAY_ALPHA : PNG_COLOR_TYPE_GRAY; } if (id == "RGBA" || id == "RGBA16") { return alpha ? PNG_COLOR_TYPE_RGB_ALPHA : PNG_COLOR_TYPE_RGB; } return -1; } bool colorSpaceIdSupported(const QString &id) { return id == "RGBA" || id == "RGBA16" || id == "GRAYA" || id == "GRAYAU16" || id == "GRAYA16"; } QPair getColorSpaceForColorType(int color_type, int color_nb_bits) { QPair r; if (color_type == PNG_COLOR_TYPE_PALETTE) { r.first = RGBAColorModelID.id(); r.second = Integer8BitsColorDepthID.id(); } else { if (color_type == PNG_COLOR_TYPE_GRAY || color_type == PNG_COLOR_TYPE_GRAY_ALPHA) { r.first = GrayAColorModelID.id(); } else if (color_type == PNG_COLOR_TYPE_RGB_ALPHA || color_type == PNG_COLOR_TYPE_RGB) { r.first = RGBAColorModelID.id(); } if (color_nb_bits == 16) { r.second = Integer16BitsColorDepthID.id(); } else if (color_nb_bits <= 8) { r.second = Integer8BitsColorDepthID.id(); } } return r; } void fillText(png_text* p_text, const char* key, QString& text) { p_text->compression = PNG_TEXT_COMPRESSION_zTXt; p_text->key = const_cast(key); char* textc = new char[text.length()+1]; strcpy(textc, text.toLatin1()); p_text->text = textc; p_text->text_length = text.length() + 1; } long formatStringList(char *string, const size_t length, const char *format, va_list operands) { int n = vsnprintf(string, length, format, operands); if (n < 0) string[length-1] = '\0'; return((long) n); } long formatString(char *string, const size_t length, const char *format, ...) { long n; va_list operands; va_start(operands, format); n = (long) formatStringList(string, length, format, operands); va_end(operands); return(n); } void writeRawProfile(png_struct *ping, png_info *ping_info, QString profile_type, QByteArray profile_data) { png_textp text; png_uint_32 allocated_length, description_length; const uchar hex[16] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; dbgFile << "Writing Raw profile: type=" << profile_type << ", length=" << profile_data.length() << endl; text = (png_textp) png_malloc(ping, (png_uint_32) sizeof(png_text)); description_length = profile_type.length(); allocated_length = (png_uint_32)(profile_data.length() * 2 + (profile_data.length() >> 5) + 20 + description_length); text[0].text = (png_charp) png_malloc(ping, allocated_length); QString key = QLatin1Literal("Raw profile type ") + profile_type.toLatin1(); QByteArray keyData = key.toLatin1(); text[0].key = keyData.data(); uchar* sp = (uchar*)profile_data.data(); png_charp dp = text[0].text; *dp++ = '\n'; memcpy(dp, profile_type.toLatin1().constData(), profile_type.length()); dp += description_length; *dp++ = '\n'; formatString(dp, allocated_length - strlen(text[0].text), "%8lu ", (unsigned long)profile_data.length()); dp += 8; for (long i = 0; i < (long) profile_data.length(); i++) { if (i % 36 == 0) *dp++ = '\n'; *(dp++) = (char) hex[((*sp >> 4) & 0x0f)]; *(dp++) = (char) hex[((*sp++) & 0x0f)]; } *dp++ = '\n'; *dp = '\0'; text[0].text_length = (png_size_t)(dp - text[0].text); text[0].compression = -1; if (text[0].text_length <= allocated_length) png_set_text(ping, ping_info, text, 1); png_free(ping, text[0].text); png_free(ping, text); } QByteArray png_read_raw_profile(png_textp text) { QByteArray profile; static const unsigned char unhex[103] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 11, 12, 13, 14, 15 }; png_charp sp = text[0].text + 1; /* look for newline */ while (*sp != '\n') sp++; /* look for length */ while (*sp == '\0' || *sp == ' ' || *sp == '\n') sp++; png_uint_32 length = (png_uint_32) atol(sp); while (*sp != ' ' && *sp != '\n') sp++; if (length == 0) { return profile; } profile.resize(length); /* copy profile, skipping white space and column 1 "=" signs */ unsigned char *dp = (unsigned char*)profile.data(); png_uint_32 nibbles = length * 2; for (png_uint_32 i = 0; i < nibbles; i++) { while (*sp < '0' || (*sp > '9' && *sp < 'a') || *sp > 'f') { if (*sp == '\0') { return QByteArray(); } sp++; } if (i % 2 == 0) *dp = (unsigned char)(16 * unhex[(int) *sp++]); else (*dp++) += unhex[(int) *sp++]; } return profile; } void decode_meta_data(png_textp text, KisMetaData::Store* store, QString type, int headerSize) { dbgFile << "Decoding " << type << " " << text[0].key; KisMetaData::IOBackend* exifIO = KisMetaData::IOBackendRegistry::instance()->value(type); Q_ASSERT(exifIO); QByteArray rawProfile = png_read_raw_profile(text); if (headerSize > 0) { rawProfile.remove(0, headerSize); } if (rawProfile.size() > 0) { QBuffer buffer; buffer.setData(rawProfile); exifIO->loadFrom(store, &buffer); } else { dbgFile << "Decoding failed"; } } } KisPNGConverter::KisPNGConverter(KisDocument *doc, bool batchMode) { // Q_ASSERT(doc); // Q_ASSERT(adapter); m_doc = doc; m_stop = false; m_max_row = 0; m_image = 0; m_batchMode = batchMode; } KisPNGConverter::~KisPNGConverter() { } class KisPNGReadStream { public: KisPNGReadStream(quint8* buf, quint32 depth) : m_posinc(8), m_depth(depth), m_buf(buf) { } int nextValue() { if (m_posinc == 0) { m_posinc = 8; m_buf++; } m_posinc -= m_depth; return (((*m_buf) >> (m_posinc)) & ((1 << m_depth) - 1)); } private: quint32 m_posinc, m_depth; quint8* m_buf; }; class KisPNGWriteStream { public: KisPNGWriteStream(quint8* buf, quint32 depth) : m_posinc(8), m_depth(depth), m_buf(buf) { *m_buf = 0; } void setNextValue(int v) { if (m_posinc == 0) { m_posinc = 8; m_buf++; *m_buf = 0; } m_posinc -= m_depth; *m_buf = (v << m_posinc) | *m_buf; } private: quint32 m_posinc, m_depth; quint8* m_buf; }; class KisPNGReaderAbstract { public: KisPNGReaderAbstract(png_structp _png_ptr, int _width, int _height) : png_ptr(_png_ptr), width(_width), height(_height) {} virtual ~KisPNGReaderAbstract() {} virtual png_bytep readLine() = 0; protected: png_structp png_ptr; int width, height; }; class KisPNGReaderLineByLine : public KisPNGReaderAbstract { public: KisPNGReaderLineByLine(png_structp _png_ptr, png_infop info_ptr, int _width, int _height) : KisPNGReaderAbstract(_png_ptr, _width, _height) { png_uint_32 rowbytes = png_get_rowbytes(png_ptr, info_ptr); row_pointer = new png_byte[rowbytes]; } ~KisPNGReaderLineByLine() override { delete[] row_pointer; } png_bytep readLine() override { png_read_row(png_ptr, row_pointer, 0); return row_pointer; } private: png_bytep row_pointer; }; class KisPNGReaderFullImage : public KisPNGReaderAbstract { public: KisPNGReaderFullImage(png_structp _png_ptr, png_infop info_ptr, int _width, int _height) : KisPNGReaderAbstract(_png_ptr, _width, _height), y(0) { row_pointers = new png_bytep[height]; png_uint_32 rowbytes = png_get_rowbytes(png_ptr, info_ptr); for (int i = 0; i < height; i++) { row_pointers[i] = new png_byte[rowbytes]; } png_read_image(png_ptr, row_pointers); } ~KisPNGReaderFullImage() override { for (int i = 0; i < height; i++) { delete[] row_pointers[i]; } delete[] row_pointers; } png_bytep readLine() override { return row_pointers[y++]; } private: png_bytepp row_pointers; int y; }; static void _read_fn(png_structp png_ptr, png_bytep data, png_size_t length) { QIODevice *in = (QIODevice *)png_get_io_ptr(png_ptr); while (length) { int nr = in->read((char*)data, length); if (nr <= 0) { png_error(png_ptr, "Read Error"); return; } length -= nr; } } static void _write_fn(png_structp png_ptr, png_bytep data, png_size_t length) { QIODevice* out = (QIODevice*)png_get_io_ptr(png_ptr); uint nr = out->write((char*)data, length); if (nr != length) { png_error(png_ptr, "Write Error"); return; } } static void _flush_fn(png_structp png_ptr) { Q_UNUSED(png_ptr); } KisImportExportErrorCode KisPNGConverter::buildImage(QIODevice* iod) { dbgFile << "Start decoding PNG File"; png_byte signature[8]; iod->peek((char*)signature, 8); #if PNG_LIBPNG_VER < 10400 if (!png_check_sig(signature, 8)) { #else if (png_sig_cmp(signature, 0, 8) != 0) { #endif iod->close(); return (ImportExportCodes::FileFormatIncorrect); } // Initialize the internal structures png_structp png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, 0, 0, 0); if (!png_ptr) { iod->close(); } png_infop info_ptr = png_create_info_struct(png_ptr); if (!info_ptr) { png_destroy_read_struct(&png_ptr, (png_infopp)0, (png_infopp)0); iod->close(); return (ImportExportCodes::Failure); } png_infop end_info = png_create_info_struct(png_ptr); if (!end_info) { png_destroy_read_struct(&png_ptr, &info_ptr, (png_infopp)0); iod->close(); return (ImportExportCodes::Failure); } // Catch errors if (setjmp(png_jmpbuf(png_ptr))) { png_destroy_read_struct(&png_ptr, &info_ptr, &end_info); iod->close(); return (ImportExportCodes::Failure); } // Initialize the special png_set_read_fn(png_ptr, iod, _read_fn); #if defined(PNG_SKIP_sRGB_CHECK_PROFILE) && defined(PNG_SET_OPTION_SUPPORTED) png_set_option(png_ptr, PNG_SKIP_sRGB_CHECK_PROFILE, PNG_OPTION_ON); #endif // read all PNG info up to image data png_read_info(png_ptr, info_ptr); if (png_get_color_type(png_ptr, info_ptr) == PNG_COLOR_TYPE_GRAY && png_get_bit_depth(png_ptr, info_ptr) < 8) { png_set_expand(png_ptr); } if (png_get_color_type(png_ptr, info_ptr) == PNG_COLOR_TYPE_PALETTE && png_get_bit_depth(png_ptr, info_ptr) < 8) { png_set_packing(png_ptr); } if (png_get_color_type(png_ptr, info_ptr) != PNG_COLOR_TYPE_PALETTE && (png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS))) { png_set_expand(png_ptr); } png_read_update_info(png_ptr, info_ptr); // Read information about the png png_uint_32 width, height; int color_nb_bits, color_type, interlace_type; png_get_IHDR(png_ptr, info_ptr, &width, &height, &color_nb_bits, &color_type, &interlace_type, 0, 0); dbgFile << "width = " << width << " height = " << height << " color_nb_bits = " << color_nb_bits << " color_type = " << color_type << " interlace_type = " << interlace_type << endl; // swap byteorder on little endian machines. #ifndef WORDS_BIGENDIAN if (color_nb_bits > 8) png_set_swap(png_ptr); #endif // Determine the colorspace QPair csName = getColorSpaceForColorType(color_type, color_nb_bits); if (csName.first.isEmpty()) { png_destroy_read_struct(&png_ptr, &info_ptr, &end_info); iod->close(); return ImportExportCodes::FormatColorSpaceUnsupported; } bool hasalpha = (color_type == PNG_COLOR_TYPE_RGB_ALPHA || color_type == PNG_COLOR_TYPE_GRAY_ALPHA); // Read image profile png_charp profile_name; #if PNG_LIBPNG_VER_MAJOR >= 1 && PNG_LIBPNG_VER_MINOR >= 5 png_bytep profile_data; #else png_charp profile_data; #endif int compression_type; png_uint_32 proflen; // Get the various optional chunks // https://www.w3.org/TR/PNG/#11cHRM #if defined(PNG_cHRM_SUPPORTED) double whitePointX, whitePointY; double redX, redY; double greenX, greenY; double blueX, blueY; png_get_cHRM(png_ptr,info_ptr, &whitePointX, &whitePointY, &redX, &redY, &greenX, &greenY, &blueX, &blueY); dbgFile << "cHRM:" << whitePointX << whitePointY << redX << redY << greenX << greenY << blueX << blueY; #endif // https://www.w3.org/TR/PNG/#11gAMA #if defined(PNG_GAMMA_SUPPORTED) double gamma; png_get_gAMA(png_ptr, info_ptr, &gamma); dbgFile << "gAMA" << gamma; #endif // https://www.w3.org/TR/PNG/#11sRGB #if defined(PNG_sRGB_SUPPORTED) int sRGBIntent; png_get_sRGB(png_ptr, info_ptr, &sRGBIntent); dbgFile << "sRGB" << sRGBIntent; #endif bool fromBlender = false; png_text* text_ptr; int num_comments; png_get_text(png_ptr, info_ptr, &text_ptr, &num_comments); for (int i = 0; i < num_comments; i++) { QString key = QString(text_ptr[i].key).toLower(); if (key == "file") { QString relatedFile = text_ptr[i].text; if (relatedFile.contains(".blend", Qt::CaseInsensitive)){ fromBlender=true; } } } bool loadedImageIsHDR = false; const KoColorProfile* profile = 0; if (png_get_iCCP(png_ptr, info_ptr, &profile_name, &compression_type, &profile_data, &proflen)) { QByteArray profile_rawdata; // XXX: Hardcoded for icc type -- is that correct for us? profile_rawdata.resize(proflen); memcpy(profile_rawdata.data(), profile_data, proflen); profile = KoColorSpaceRegistry::instance()->createColorProfile(csName.first, csName.second, profile_rawdata); Q_CHECK_PTR(profile); if (profile) { // dbgFile << "profile name: " << profile->productName() << " profile description: " << profile->productDescription() << " information sur le produit: " << profile->productInfo(); if (!profile->isSuitableForOutput()) { dbgFile << "the profile is not suitable for output and therefore cannot be used in krita, we need to convert the image to a standard profile"; // TODO: in ko2 popup a selection menu to inform the user } } loadedImageIsHDR = strcmp(profile_name, "ITUR_2100_PQ_FULL") == 0; } else { dbgFile << "no embedded profile, will use the default profile"; if (color_nb_bits == 16 && !fromBlender && !qAppName().toLower().contains("test") && !m_batchMode) { KisConfig cfg(true); quint32 behaviour = cfg.pasteBehaviour(); if (behaviour == PASTE_ASK) { KisDlgPngImport dlg(m_path, csName.first, csName.second); KisCursorOverrideHijacker hijacker; Q_UNUSED(hijacker); dlg.exec(); if (!dlg.profile().isEmpty()) { profile = KoColorSpaceRegistry::instance()->profileByName(dlg.profile()); } } } dbgFile << "no embedded profile, will use the default profile"; } const QString colorSpaceId = KoColorSpaceRegistry::instance()->colorSpaceId(csName.first, csName.second); // Check that the profile is used by the color space if (profile && !KoColorSpaceRegistry::instance()->profileIsCompatible(profile, colorSpaceId)) { warnFile << "The profile " << profile->name() << " is not compatible with the color space model " << csName.first << " " << csName.second; profile = 0; } // Retrieve a pointer to the colorspace KoColorConversionTransformation* transform = 0; const KoColorSpace* cs = 0; if (loadedImageIsHDR && csName.first == RGBAColorModelID.id() && csName.second == Integer16BitsColorDepthID.id()) { const KoColorSpace *p2020PQCS = KoColorSpaceRegistry::instance()->colorSpace( RGBAColorModelID.id(), Integer16BitsColorDepthID.id(), KoColorSpaceRegistry::instance()->p2020PQProfile()); cs = p2020PQCS; } else if (profile && profile->isSuitableForOutput()) { dbgFile << "image has embedded profile: " << profile->name() << "\n"; cs = KoColorSpaceRegistry::instance()->colorSpace(csName.first, csName.second, profile); } else { if (csName.first == RGBAColorModelID.id()) { cs = KoColorSpaceRegistry::instance()->colorSpace(csName.first, csName.second, "sRGB-elle-V2-srgbtrc.icc"); } else if (csName.first == GrayAColorModelID.id()) { cs = KoColorSpaceRegistry::instance()->colorSpace(csName.first, csName.second, "Gray-D50-elle-V2-srgbtrc.icc"); } else { cs = KoColorSpaceRegistry::instance()->colorSpace(csName.first, csName.second, 0); } //TODO: two fixes : one tell the user about the problem and ask for a solution, and two once the kocolorspace include KoColorTransformation, use that instead of hacking a lcms transformation // Create the cmsTransform if needed if (profile) { transform = KoColorSpaceRegistry::instance()->colorSpace(csName.first, csName.second, profile)->createColorConverter(cs, KoColorConversionTransformation::internalRenderingIntent(), KoColorConversionTransformation::internalConversionFlags()); } } if (cs == 0) { png_destroy_read_struct(&png_ptr, &info_ptr, &end_info); return ImportExportCodes::FormatColorSpaceUnsupported; } // Creating the KisImageSP if (m_image == 0) { KisUndoStore *store = m_doc ? m_doc->createUndoStore() : new KisSurrogateUndoStore(); m_image = new KisImage(store, width, height, cs, "built image"); } // Read resolution int unit_type; png_uint_32 x_resolution, y_resolution; png_get_pHYs(png_ptr, info_ptr, &x_resolution, &y_resolution, &unit_type); if (x_resolution > 0 && y_resolution > 0 && unit_type == PNG_RESOLUTION_METER) { m_image->setResolution((double) POINT_TO_CM(x_resolution) / 100.0, (double) POINT_TO_CM(y_resolution) / 100.0); // It is the "invert" macro because we convert from pointer-per-inchs to points } double coeff = quint8_MAX / (double)(pow((double)2, color_nb_bits) - 1); KisPaintLayerSP layer = new KisPaintLayer(m_image.data(), m_image -> nextLayerName(), UCHAR_MAX); // Read comments/texts... png_get_text(png_ptr, info_ptr, &text_ptr, &num_comments); if (m_doc) { KoDocumentInfo * info = m_doc->documentInfo(); dbgFile << "There are " << num_comments << " comments in the text"; for (int i = 0; i < num_comments; i++) { QString key = QString(text_ptr[i].key).toLower(); dbgFile << "key: " << text_ptr[i].key << ", containing: " << text_ptr[i].text << ": " << (key == "raw profile type exif " ? "isExif" : "something else"); if (key == "title") { info->setAboutInfo("title", text_ptr[i].text); } else if (key == "description") { info->setAboutInfo("comment", text_ptr[i].text); } else if (key == "author") { info->setAuthorInfo("creator", text_ptr[i].text); } else if (key.contains("raw profile type exif")) { decode_meta_data(text_ptr + i, layer->metaData(), "exif", 6); } else if (key.contains("raw profile type iptc")) { decode_meta_data(text_ptr + i, layer->metaData(), "iptc", 14); } else if (key.contains("raw profile type xmp")) { decode_meta_data(text_ptr + i, layer->metaData(), "xmp", 0); } else if (key == "version") { m_image->addAnnotation(new KisAnnotation("kpp_version", "version", QByteArray(text_ptr[i].text))); } else if (key == "preset") { m_image->addAnnotation(new KisAnnotation("kpp_preset", "preset", QByteArray(text_ptr[i].text))); } } } // Read image data QScopedPointer reader; try { if (interlace_type == PNG_INTERLACE_ADAM7) { reader.reset(new KisPNGReaderFullImage(png_ptr, info_ptr, width, height)); } else { reader.reset(new KisPNGReaderLineByLine(png_ptr, info_ptr, width, height)); } } catch (const std::bad_alloc& e) { // new png_byte[] may raise such an exception if the image // is invalid / to large. dbgFile << "bad alloc: " << e.what(); // Free only the already allocated png_byte instances. png_destroy_read_struct(&png_ptr, &info_ptr, &end_info); return (ImportExportCodes::Failure); } // Read the palette if the file is indexed png_colorp palette ; int num_palette; if (color_type == PNG_COLOR_TYPE_PALETTE) { png_get_PLTE(png_ptr, info_ptr, &palette, &num_palette); } // Read the transparency palette quint8 palette_alpha[256]; memset(palette_alpha, 255, 256); if (png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS)) { if (color_type == PNG_COLOR_TYPE_PALETTE) { png_bytep alpha_ptr; int num_alpha; png_get_tRNS(png_ptr, info_ptr, &alpha_ptr, &num_alpha, 0); for (int i = 0; i < num_alpha; ++i) { palette_alpha[i] = alpha_ptr[i]; } } } for (png_uint_32 y = 0; y < height; y++) { KisHLineIteratorSP it = layer -> paintDevice() -> createHLineIteratorNG(0, y, width); png_bytep row_pointer = reader->readLine(); switch (color_type) { case PNG_COLOR_TYPE_GRAY: case PNG_COLOR_TYPE_GRAY_ALPHA: if (color_nb_bits == 16) { quint16 *src = reinterpret_cast(row_pointer); do { quint16 *d = reinterpret_cast(it->rawData()); d[0] = *(src++); if (hasalpha) { d[1] = *(src++); } else { d[1] = quint16_MAX; } if (transform) transform->transformInPlace(reinterpret_cast(d), reinterpret_cast(d), 1); } while (it->nextPixel()); } else { KisPNGReadStream stream(row_pointer, color_nb_bits); do { quint8 *d = it->rawData(); d[0] = (quint8)(stream.nextValue() * coeff); if (hasalpha) { d[1] = (quint8)(stream.nextValue() * coeff); } else { d[1] = UCHAR_MAX; } if (transform) transform->transformInPlace(d, d, 1); } while (it->nextPixel()); } // FIXME:should be able to read 1 and 4 bits depth and scale them to 8 bits" break; case PNG_COLOR_TYPE_RGB: case PNG_COLOR_TYPE_RGB_ALPHA: if (color_nb_bits == 16) { quint16 *src = reinterpret_cast(row_pointer); do { quint16 *d = reinterpret_cast(it->rawData()); d[2] = *(src++); d[1] = *(src++); d[0] = *(src++); if (hasalpha) d[3] = *(src++); else d[3] = quint16_MAX; if (transform) transform->transformInPlace(reinterpret_cast(d), reinterpret_cast(d), 1); } while (it->nextPixel()); } else { KisPNGReadStream stream(row_pointer, color_nb_bits); do { quint8 *d = it->rawData(); d[2] = (quint8)(stream.nextValue() * coeff); d[1] = (quint8)(stream.nextValue() * coeff); d[0] = (quint8)(stream.nextValue() * coeff); if (hasalpha) d[3] = (quint8)(stream.nextValue() * coeff); else d[3] = UCHAR_MAX; if (transform) transform->transformInPlace(d, d, 1); } while (it->nextPixel()); } break; case PNG_COLOR_TYPE_PALETTE: { KisPNGReadStream stream(row_pointer, color_nb_bits); do { quint8 *d = it->rawData(); quint8 index = stream.nextValue(); quint8 alpha = palette_alpha[ index ]; if (alpha == 0) { memset(d, 0, 4); } else { png_color c = palette[ index ]; d[2] = c.red; d[1] = c.green; d[0] = c.blue; d[3] = alpha; } } while (it->nextPixel()); } break; default: return ImportExportCodes::FormatFeaturesUnsupported; } } m_image->addNode(layer.data(), m_image->rootLayer().data()); png_read_end(png_ptr, end_info); iod->close(); // Freeing memory png_destroy_read_struct(&png_ptr, &info_ptr, &end_info); return ImportExportCodes::OK; } KisImportExportErrorCode KisPNGConverter::buildImage(const QString &filename) { m_path = filename; QFile fp(filename); if (fp.exists()) { if (!fp.open(QIODevice::ReadOnly)) { dbgFile << "Failed to open PNG File"; return (ImportExportCodes::FileFormatIncorrect); } return buildImage(&fp); } return (ImportExportCodes::FileNotExist); } KisImageSP KisPNGConverter::image() { return m_image; } bool KisPNGConverter::saveDeviceToStore(const QString &filename, const QRect &imageRect, const qreal xRes, const qreal yRes, KisPaintDeviceSP dev, KoStore *store, KisMetaData::Store* metaData) { if (store->open(filename)) { KoStoreDevice io(store); if (!io.open(QIODevice::WriteOnly)) { dbgFile << "Could not open for writing:" << filename; return false; } KisPNGConverter pngconv(0); vKisAnnotationSP_it annotIt = 0; KisMetaData::Store* metaDataStore = 0; if (metaData) { metaDataStore = new KisMetaData::Store(*metaData); } KisPNGOptions options; options.compression = 0; options.interlace = false; options.tryToSaveAsIndexed = false; options.alpha = true; options.saveSRGBProfile = false; if (dev->colorSpace()->id() != "RGBA") { dev = new KisPaintDevice(*dev.data()); dev->convertTo(KoColorSpaceRegistry::instance()->rgb8()); } KisImportExportErrorCode success = pngconv.buildFile(&io, imageRect, xRes, yRes, dev, annotIt, annotIt, options, metaDataStore); if (!success.isOk()) { dbgFile << "Saving PNG failed:" << filename; delete metaDataStore; return false; } delete metaDataStore; io.close(); if (!store->close()) { return false; } } else { dbgFile << "Opening of data file failed :" << filename; return false; } return true; } KisImportExportErrorCode KisPNGConverter::buildFile(const QString &filename, const QRect &imageRect, const qreal xRes, const qreal yRes, KisPaintDeviceSP device, vKisAnnotationSP_it annotationsStart, vKisAnnotationSP_it annotationsEnd, KisPNGOptions options, KisMetaData::Store* metaData) { dbgFile << "Start writing PNG File " << filename; // Open a QIODevice for writing QFile fp (filename); if (!fp.open(QIODevice::WriteOnly)) { dbgFile << "Failed to open PNG File for writing"; return (KisImportExportErrorCannotWrite(fp.error())); } KisImportExportErrorCode result = buildFile(&fp, imageRect, xRes, yRes, device, annotationsStart, annotationsEnd, options, metaData); return result; } KisImportExportErrorCode KisPNGConverter::buildFile(QIODevice* iodevice, const QRect &imageRect, const qreal xRes, const qreal yRes, KisPaintDeviceSP device, vKisAnnotationSP_it annotationsStart, vKisAnnotationSP_it annotationsEnd, KisPNGOptions options, KisMetaData::Store* metaData) { KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(device, ImportExportCodes::InternalError); if (!options.alpha) { KisPaintDeviceSP tmp = new KisPaintDevice(device->colorSpace()); KoColor c(options.transparencyFillColor, device->colorSpace()); tmp->fill(imageRect, c); KisPainter gc(tmp); gc.bitBlt(imageRect.topLeft(), device, imageRect); gc.end(); device = tmp; } if (device->colorSpace()->colorDepthId() == Float16BitsColorDepthID || device->colorSpace()->colorDepthId() == Float32BitsColorDepthID || device->colorSpace()->colorDepthId() == Float64BitsColorDepthID || options.saveAsHDR) { const KoColorSpace *dstCS = KoColorSpaceRegistry::instance()->colorSpace( device->colorSpace()->colorModelId().id(), Integer16BitsColorDepthID.id(), device->colorSpace()->profile()); if (options.saveAsHDR) { dstCS = KoColorSpaceRegistry::instance()->colorSpace( RGBAColorModelID.id(), Integer16BitsColorDepthID.id(), KoColorSpaceRegistry::instance()->p2020PQProfile()); } KisPaintDeviceSP tmp = new KisPaintDevice(device->colorSpace()); tmp->makeCloneFromRough(device, imageRect); tmp->convertTo(dstCS); device = tmp; } KIS_SAFE_ASSERT_RECOVER(!options.saveAsHDR || !options.forceSRGB) { options.forceSRGB = false; } KIS_SAFE_ASSERT_RECOVER(!options.saveAsHDR || !options.tryToSaveAsIndexed) { options.tryToSaveAsIndexed = false; } QStringList colormodels = QStringList() << RGBAColorModelID.id() << GrayAColorModelID.id(); if (options.forceSRGB || !colormodels.contains(device->colorSpace()->colorModelId().id())) { const KoColorSpace* cs = KoColorSpaceRegistry::instance()->colorSpace(RGBAColorModelID.id(), device->colorSpace()->colorDepthId().id(), "sRGB built-in - (lcms internal)"); device = new KisPaintDevice(*device); device->convertTo(cs); } // Initialize structures png_structp png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, 0, 0, 0); if (!png_ptr) { return (ImportExportCodes::Failure); } #if defined(PNG_SKIP_sRGB_CHECK_PROFILE) && defined(PNG_SET_OPTION_SUPPORTED) png_set_option(png_ptr, PNG_SKIP_sRGB_CHECK_PROFILE, PNG_OPTION_ON); #endif #ifdef PNG_READ_CHECK_FOR_INVALID_INDEX_SUPPORTED png_set_check_for_invalid_index(png_ptr, 0); #endif png_infop info_ptr = png_create_info_struct(png_ptr); if (!info_ptr) { png_destroy_write_struct(&png_ptr, (png_infopp)0); return (ImportExportCodes::Failure); } // If an error occurs during writing, libpng will jump here if (setjmp(png_jmpbuf(png_ptr))) { png_destroy_write_struct(&png_ptr, &info_ptr); return (ImportExportCodes::Failure); } // Initialize the writing // png_init_io(png_ptr, fp); // Setup the progress function // XXX: Implement progress updating -- png_set_write_status_fn(png_ptr, progress);" // setProgressTotalSteps(100/*height*/); /* set the zlib compression level */ png_set_compression_level(png_ptr, options.compression); png_set_write_fn(png_ptr, (void*)iodevice, _write_fn, _flush_fn); /* set other zlib parameters */ png_set_compression_mem_level(png_ptr, 8); png_set_compression_strategy(png_ptr, Z_DEFAULT_STRATEGY); png_set_compression_window_bits(png_ptr, 15); png_set_compression_method(png_ptr, 8); png_set_compression_buffer_size(png_ptr, 8192); int color_nb_bits = 8 * device->pixelSize() / device->channelCount(); int color_type = getColorTypeforColorSpace(device->colorSpace(), options.alpha); Q_ASSERT(color_type > -1); // Try to compute a table of color if the colorspace is RGB8f QScopedArrayPointer palette; int num_palette = 0; if (!options.alpha && options.tryToSaveAsIndexed && KoID(device->colorSpace()->id()) == KoID("RGBA")) { // png doesn't handle indexed images and alpha, and only have indexed for RGB8 palette.reset(new png_color[255]); KisSequentialIterator it(device, imageRect); bool toomuchcolor = false; while (it.nextPixel()) { const quint8* c = it.oldRawData(); bool findit = false; for (int i = 0; i < num_palette; i++) { if (palette[i].red == c[2] && palette[i].green == c[1] && palette[i].blue == c[0]) { findit = true; break; } } if (!findit) { if (num_palette == 255) { toomuchcolor = true; break; } palette[num_palette].red = c[2]; palette[num_palette].green = c[1]; palette[num_palette].blue = c[0]; num_palette++; } } if (!toomuchcolor) { dbgFile << "Found a palette of " << num_palette << " colors"; color_type = PNG_COLOR_TYPE_PALETTE; if (num_palette <= 2) { color_nb_bits = 1; } else if (num_palette <= 4) { color_nb_bits = 2; } else if (num_palette <= 16) { color_nb_bits = 4; } else { color_nb_bits = 8; } } else { palette.reset(); } } int interlacetype = options.interlace ? PNG_INTERLACE_ADAM7 : PNG_INTERLACE_NONE; KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(color_type >= 0, ImportExportCodes::Failure); png_set_IHDR(png_ptr, info_ptr, imageRect.width(), imageRect.height(), color_nb_bits, color_type, interlacetype, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT); // set sRGB only if the profile is sRGB -- http://www.w3.org/TR/PNG/#11sRGB says sRGB and iCCP should not both be present - bool sRGB = device->colorSpace()->profile()->name().contains(QLatin1String("srgb"), Qt::CaseInsensitive); + const bool sRGB = *device->colorSpace()->profile() == *KoColorSpaceRegistry::instance()->p709SRGBProfile(); /* * This automatically writes the correct gamma and chroma chunks along with the sRGB chunk, but firefox's * color management is bugged, so once you give it any incentive to start color managing an sRGB image it * will turn, for example, a nice desaturated rusty red into bright poppy red. So this is disabled for now. */ /*if (!options.saveSRGBProfile && sRGB) { png_set_sRGB_gAMA_and_cHRM(png_ptr, info_ptr, PNG_sRGB_INTENT_PERCEPTUAL); }*/ /** TODO: Firefox still opens the image incorrectly if there is gAMA+cHRM tags * present. According to the standard it should use iCCP tag with higher priority, * but it doesn't: * * "When the iCCP chunk is present, PNG decoders that recognize it and are capable * of colour management [ICC] shall ignore the gAMA and cHRM chunks and use * the iCCP chunk instead and interpret it according to [ICC-1] and [ICC-1A]" */ #if 0 if (options.saveAsHDR) { // https://www.w3.org/TR/PNG/#11gAMA #if defined(PNG_GAMMA_SUPPORTED) // the values are set in accurdance of HDR-PNG standard: // https://www.w3.org/TR/png-hdr-pq/ png_set_gAMA_fixed(png_ptr, info_ptr, 15000); dbgFile << "gAMA" << "(Rec 2100)"; #endif #if defined PNG_cHRM_SUPPORTED png_set_cHRM_fixed(png_ptr, info_ptr, 31270, 32900, // white point 70800, 29200, // red 17000, 79700, // green 13100, 4600 // blue ); dbgFile << "cHRM" << "(Rec 2100)"; #endif } #endif // we should ensure we don't access non-existing palette object KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(palette || color_type != PNG_COLOR_TYPE_PALETTE, ImportExportCodes::Failure); // set the palette if (color_type == PNG_COLOR_TYPE_PALETTE) { png_set_PLTE(png_ptr, info_ptr, palette.data(), num_palette); } // Save annotation vKisAnnotationSP_it it = annotationsStart; while (it != annotationsEnd) { if (!(*it) || (*it)->type().isEmpty()) { dbgFile << "Warning: empty annotation"; it++; continue; } dbgFile << "Trying to store annotation of type " << (*it) -> type() << " of size " << (*it) -> annotation() . size(); if ((*it) -> type().startsWith(QString("krita_attribute:"))) { // // Attribute // XXX: it should be possible to save krita_attributes in the \"CHUNKs\"" dbgFile << "cannot save this annotation : " << (*it) -> type(); } else if ((*it)->type() == "kpp_version" || (*it)->type() == "kpp_preset" ) { dbgFile << "Saving preset information " << (*it)->description(); png_textp text = (png_textp) png_malloc(png_ptr, (png_uint_32) sizeof(png_text)); QByteArray keyData = (*it)->description().toLatin1(); text[0].key = keyData.data(); text[0].text = (char*)(*it)->annotation().data(); text[0].text_length = (*it)->annotation().size(); text[0].compression = -1; png_set_text(png_ptr, info_ptr, text, 1); png_free(png_ptr, text); } it++; } // Save the color profile const KoColorProfile* colorProfile = device->colorSpace()->profile(); QByteArray colorProfileData = colorProfile->rawData(); if (!sRGB || options.saveSRGBProfile) { #if PNG_LIBPNG_VER_MAJOR >= 1 && PNG_LIBPNG_VER_MINOR >= 5 const char *typeString = !options.saveAsHDR ? "icc" : "ITUR_2100_PQ_FULL"; png_set_iCCP(png_ptr, info_ptr, (png_const_charp)typeString, PNG_COMPRESSION_TYPE_BASE, (png_const_bytep)colorProfileData.constData(), colorProfileData . size()); #else // older version of libpng has a problem with constness on the parameters char typeStringICC[] = "icc"; char typeStringHDR[] = "ITUR_2100_PQ_FULL"; char *typeString = !options.saveAsHDR ? typeStringICC : typeStringHDR; png_set_iCCP(png_ptr, info_ptr, typeString, PNG_COMPRESSION_TYPE_BASE, colorProfileData.data(), colorProfileData . size()); #endif } // save comments from the document information // warning: according to the official png spec, the keys need to be capitalized! if (m_doc) { png_text texts[4]; int nbtexts = 0; KoDocumentInfo * info = m_doc->documentInfo(); QString title = info->aboutInfo("title"); if (!title.isEmpty() && options.storeMetaData) { fillText(texts + nbtexts, "Title", title); nbtexts++; } QString abstract = info->aboutInfo("subject"); if (abstract.isEmpty()) { abstract = info->aboutInfo("abstract"); } if (!abstract.isEmpty() && options.storeMetaData) { QString keywords = info->aboutInfo("keyword"); if (!keywords.isEmpty()) { abstract = abstract + " keywords: " + keywords; } fillText(texts + nbtexts, "Description", abstract); nbtexts++; } QString license = info->aboutInfo("license"); if (!license.isEmpty() && options.storeMetaData) { fillText(texts + nbtexts, "Copyright", license); nbtexts++; } QString author = info->authorInfo("creator"); if (!author.isEmpty() && options.storeAuthor) { if (!info->authorContactInfo().isEmpty()) { QString contact = info->authorContactInfo().at(0); if (!contact.isEmpty()) { author = author+"("+contact+")"; } } fillText(texts + nbtexts, "Author", author); nbtexts++; } png_set_text(png_ptr, info_ptr, texts, nbtexts); } // Save metadata following imagemagick way // Save exif if (metaData && !metaData->empty()) { if (options.exif) { dbgFile << "Trying to save exif information"; KisMetaData::IOBackend* exifIO = KisMetaData::IOBackendRegistry::instance()->value("exif"); Q_ASSERT(exifIO); QBuffer buffer; exifIO->saveTo(metaData, &buffer, KisMetaData::IOBackend::JpegHeader); writeRawProfile(png_ptr, info_ptr, "exif", buffer.data()); } // Save IPTC if (options.iptc) { dbgFile << "Trying to save exif information"; KisMetaData::IOBackend* iptcIO = KisMetaData::IOBackendRegistry::instance()->value("iptc"); Q_ASSERT(iptcIO); QBuffer buffer; iptcIO->saveTo(metaData, &buffer, KisMetaData::IOBackend::JpegHeader); dbgFile << "IPTC information size is" << buffer.data().size(); writeRawProfile(png_ptr, info_ptr, "iptc", buffer.data()); } // Save XMP if (options.xmp) { dbgFile << "Trying to save XMP information"; KisMetaData::IOBackend* xmpIO = KisMetaData::IOBackendRegistry::instance()->value("xmp"); Q_ASSERT(xmpIO); QBuffer buffer; xmpIO->saveTo(metaData, &buffer, KisMetaData::IOBackend::NoHeader); dbgFile << "XMP information size is" << buffer.data().size(); writeRawProfile(png_ptr, info_ptr, "xmp", buffer.data()); } } #if 0 // Unimplemented? // Save resolution int unit_type; png_uint_32 x_resolution, y_resolution; #endif png_set_pHYs(png_ptr, info_ptr, CM_TO_POINT(xRes) * 100.0, CM_TO_POINT(yRes) * 100.0, PNG_RESOLUTION_METER); // It is the "invert" macro because we convert from pointer-per-inchs to points // Save the information to the file png_write_info(png_ptr, info_ptr); png_write_flush(png_ptr); // swap byteorder on little endian machines. #ifndef WORDS_BIGENDIAN if (color_nb_bits > 8) png_set_swap(png_ptr); #endif // Write the PNG // png_write_png(png_ptr, info_ptr, PNG_TRANSFORM_IDENTITY, 0); struct RowPointersStruct { RowPointersStruct(const QSize &size, int pixelSize) : numRows(size.height()) { rows = new png_byte*[numRows]; for (int i = 0; i < numRows; i++) { rows[i] = new png_byte[size.width() * pixelSize]; } } ~RowPointersStruct() { for (int i = 0; i < numRows; i++) { delete[] rows[i]; } delete[] rows; } const int numRows = 0; png_byte** rows = 0; }; // Fill the data structure RowPointersStruct rowPointers(imageRect.size(), device->pixelSize()); int row = 0; for (int y = imageRect.y(); y < imageRect.y() + imageRect.height(); y++, row++) { KisHLineConstIteratorSP it = device->createHLineConstIteratorNG(imageRect.x(), y, imageRect.width()); switch (color_type) { case PNG_COLOR_TYPE_GRAY: case PNG_COLOR_TYPE_GRAY_ALPHA: if (color_nb_bits == 16) { quint16 *dst = reinterpret_cast(rowPointers.rows[row]); do { const quint16 *d = reinterpret_cast(it->oldRawData()); *(dst++) = d[0]; if (options.alpha) *(dst++) = d[1]; } while (it->nextPixel()); } else { quint8 *dst = rowPointers.rows[row]; do { const quint8 *d = it->oldRawData(); *(dst++) = d[0]; if (options.alpha) *(dst++) = d[1]; } while (it->nextPixel()); } break; case PNG_COLOR_TYPE_RGB: case PNG_COLOR_TYPE_RGB_ALPHA: if (color_nb_bits == 16) { quint16 *dst = reinterpret_cast(rowPointers.rows[row]); do { const quint16 *d = reinterpret_cast(it->oldRawData()); *(dst++) = d[2]; *(dst++) = d[1]; *(dst++) = d[0]; if (options.alpha) *(dst++) = d[3]; } while (it->nextPixel()); } else { quint8 *dst = rowPointers.rows[row]; do { const quint8 *d = it->oldRawData(); *(dst++) = d[2]; *(dst++) = d[1]; *(dst++) = d[0]; if (options.alpha) *(dst++) = d[3]; } while (it->nextPixel()); } break; case PNG_COLOR_TYPE_PALETTE: { quint8 *dst = rowPointers.rows[row]; KisPNGWriteStream writestream(dst, color_nb_bits); do { const quint8 *d = it->oldRawData(); int i; for (i = 0; i < num_palette; i++) { if (palette[i].red == d[2] && palette[i].green == d[1] && palette[i].blue == d[0]) { break; } } writestream.setNextValue(i); } while (it->nextPixel()); } break; default: return ImportExportCodes::FormatColorSpaceUnsupported; } } png_write_image(png_ptr, rowPointers.rows); // Writing is over png_write_end(png_ptr, info_ptr); // Free memory png_destroy_write_struct(&png_ptr, &info_ptr); return ImportExportCodes::OK; } void KisPNGConverter::cancel() { m_stop = true; } void KisPNGConverter::progress(png_structp png_ptr, png_uint_32 row_number, int pass) { if (png_ptr == 0 || row_number > PNG_MAX_UINT || pass > 7) return; // setProgress(row_number); } bool KisPNGConverter::isColorSpaceSupported(const KoColorSpace *cs) { return colorSpaceIdSupported(cs->id()); } diff --git a/libs/ui/tool/strokes/freehand_stroke.cpp b/libs/ui/tool/strokes/freehand_stroke.cpp index 25fd09c670..278ba0ac45 100644 --- a/libs/ui/tool/strokes/freehand_stroke.cpp +++ b/libs/ui/tool/strokes/freehand_stroke.cpp @@ -1,350 +1,350 @@ /* * Copyright (c) 2011 Dmitry Kazakov * * 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 "freehand_stroke.h" #include #include "kis_canvas_resource_provider.h" #include #include #include "kis_painter.h" #include "kis_paintop.h" #include "kis_update_time_monitor.h" #include #include #include "FreehandStrokeRunnableJobDataWithUpdate.h" #include #include "KisStrokeEfficiencyMeasurer.h" #include #include #include #include "brushengine/kis_paintop_utils.h" #include "KisAsyncronousStrokeUpdateHelper.h" struct FreehandStrokeStrategy::Private { Private(KisResourcesSnapshotSP _resources) : resources(_resources), needsAsynchronousUpdates(_resources->presetNeedsAsynchronousUpdates()) { if (needsAsynchronousUpdates) { timeSinceLastUpdate.start(); } } Private(const Private &rhs) : randomSource(rhs.randomSource), resources(rhs.resources), needsAsynchronousUpdates(rhs.needsAsynchronousUpdates) { if (needsAsynchronousUpdates) { timeSinceLastUpdate.start(); } } KisStrokeRandomSource randomSource; KisResourcesSnapshotSP resources; KisStrokeEfficiencyMeasurer efficiencyMeasurer; QElapsedTimer timeSinceLastUpdate; int currentUpdatePeriod = 40; const bool needsAsynchronousUpdates = false; std::mutex updateEntryMutex; }; FreehandStrokeStrategy::FreehandStrokeStrategy(KisResourcesSnapshotSP resources, KisFreehandStrokeInfo *strokeInfo, const KUndo2MagicString &name) - : KisPainterBasedStrokeStrategy("FREEHAND_STROKE", name, + : KisPainterBasedStrokeStrategy(QLatin1String("FREEHAND_STROKE"), name, resources, strokeInfo), m_d(new Private(resources)) { init(); } FreehandStrokeStrategy::FreehandStrokeStrategy(KisResourcesSnapshotSP resources, QVector strokeInfos, const KUndo2MagicString &name) - : KisPainterBasedStrokeStrategy("FREEHAND_STROKE", name, + : KisPainterBasedStrokeStrategy(QLatin1String("FREEHAND_STROKE"), name, resources, strokeInfos), m_d(new Private(resources)) { init(); } FreehandStrokeStrategy::FreehandStrokeStrategy(const FreehandStrokeStrategy &rhs, int levelOfDetail) : KisPainterBasedStrokeStrategy(rhs, levelOfDetail), m_d(new Private(*rhs.m_d)) { m_d->randomSource.setLevelOfDetail(levelOfDetail); } FreehandStrokeStrategy::~FreehandStrokeStrategy() { KisStrokeSpeedMonitor::instance()->notifyStrokeFinished(m_d->efficiencyMeasurer.averageCursorSpeed(), m_d->efficiencyMeasurer.averageRenderingSpeed(), m_d->efficiencyMeasurer.averageFps(), m_d->resources->currentPaintOpPreset()); KisUpdateTimeMonitor::instance()->endStrokeMeasure(); } void FreehandStrokeStrategy::init() { setSupportsWrapAroundMode(true); setSupportsMaskingBrush(true); setSupportsIndirectPainting(true); enableJob(KisSimpleStrokeStrategy::JOB_DOSTROKE); if (m_d->needsAsynchronousUpdates) { /** * In case the paintop uses asynchronous updates, we should set priority to it, * because FPS is controlled separately, not by the queue's merging algorithm. */ setBalancingRatioOverride(0.01); // set priority to updates } KisUpdateTimeMonitor::instance()->startStrokeMeasure(); m_d->efficiencyMeasurer.setEnabled(KisStrokeSpeedMonitor::instance()->haveStrokeSpeedMeasurement()); } void FreehandStrokeStrategy::initStrokeCallback() { KisPainterBasedStrokeStrategy::initStrokeCallback(); m_d->efficiencyMeasurer.notifyRenderingStarted(); } void FreehandStrokeStrategy::finishStrokeCallback() { m_d->efficiencyMeasurer.notifyRenderingFinished(); KisPainterBasedStrokeStrategy::finishStrokeCallback(); } void FreehandStrokeStrategy::doStrokeCallback(KisStrokeJobData *data) { if (KisAsyncronousStrokeUpdateHelper::UpdateData *d = dynamic_cast(data)) { // this job is lod-clonable in contrast to FreehandStrokeRunnableJobDataWithUpdate! tryDoUpdate(d->forceUpdate); } else if (Data *d = dynamic_cast(data)) { KisMaskedFreehandStrokePainter *maskedPainter = this->maskedPainter(d->strokeInfoId); KisUpdateTimeMonitor::instance()->reportPaintOpPreset(maskedPainter->preset()); KisRandomSourceSP rnd = m_d->randomSource.source(); KisPerStrokeRandomSourceSP strokeRnd = m_d->randomSource.perStrokeSource(); switch(d->type) { case Data::POINT: d->pi1.setRandomSource(rnd); d->pi1.setPerStrokeRandomSource(strokeRnd); maskedPainter->paintAt(d->pi1); m_d->efficiencyMeasurer.addSample(d->pi1.pos()); break; case Data::LINE: d->pi1.setRandomSource(rnd); d->pi2.setRandomSource(rnd); d->pi1.setPerStrokeRandomSource(strokeRnd); d->pi2.setPerStrokeRandomSource(strokeRnd); maskedPainter->paintLine(d->pi1, d->pi2); m_d->efficiencyMeasurer.addSample(d->pi2.pos()); break; case Data::CURVE: d->pi1.setRandomSource(rnd); d->pi2.setRandomSource(rnd); d->pi1.setPerStrokeRandomSource(strokeRnd); d->pi2.setPerStrokeRandomSource(strokeRnd); maskedPainter->paintBezierCurve(d->pi1, d->control1, d->control2, d->pi2); m_d->efficiencyMeasurer.addSample(d->pi2.pos()); break; case Data::POLYLINE: maskedPainter->paintPolyline(d->points, 0, d->points.size()); m_d->efficiencyMeasurer.addSamples(d->points); break; case Data::POLYGON: maskedPainter->paintPolygon(d->points); m_d->efficiencyMeasurer.addSamples(d->points); break; case Data::RECT: maskedPainter->paintRect(d->rect); m_d->efficiencyMeasurer.addSample(d->rect.topLeft()); m_d->efficiencyMeasurer.addSample(d->rect.topRight()); m_d->efficiencyMeasurer.addSample(d->rect.bottomRight()); m_d->efficiencyMeasurer.addSample(d->rect.bottomLeft()); break; case Data::ELLIPSE: maskedPainter->paintEllipse(d->rect); // TODO: add speed measures break; case Data::PAINTER_PATH: maskedPainter->paintPainterPath(d->path); // TODO: add speed measures break; case Data::QPAINTER_PATH: maskedPainter->drawPainterPath(d->path, d->pen); break; case Data::QPAINTER_PATH_FILL: maskedPainter->drawAndFillPainterPath(d->path, d->pen, d->customColor); break; }; tryDoUpdate(); } else { KisPainterBasedStrokeStrategy::doStrokeCallback(data); FreehandStrokeRunnableJobDataWithUpdate *dataWithUpdate = dynamic_cast(data); if (dataWithUpdate) { tryDoUpdate(); } } } void FreehandStrokeStrategy::tryDoUpdate(bool forceEnd) { // we should enter this function only once! std::unique_lock entryLock(m_d->updateEntryMutex, std::try_to_lock); if (!entryLock.owns_lock()) return; if (m_d->needsAsynchronousUpdates) { if (forceEnd || m_d->timeSinceLastUpdate.elapsed() > m_d->currentUpdatePeriod) { m_d->timeSinceLastUpdate.restart(); for (int i = 0; i < numMaskedPainters(); i++) { KisMaskedFreehandStrokePainter *maskedPainter = this->maskedPainter(i); // TODO: well, we should count all N simultaneous painters for FPS rate! QVector jobs; bool needsMoreUpdates = false; std::tie(m_d->currentUpdatePeriod, needsMoreUpdates) = maskedPainter->doAsyncronousUpdate(jobs); if (!jobs.isEmpty() || maskedPainter->hasDirtyRegion() || (forceEnd && needsMoreUpdates)) { jobs.append(new KisRunnableStrokeJobData( [this] () { this->issueSetDirtySignals(); }, KisStrokeJobData::SEQUENTIAL)); if (forceEnd && needsMoreUpdates) { jobs.append(new KisRunnableStrokeJobData( [this] () { this->tryDoUpdate(true); }, KisStrokeJobData::SEQUENTIAL)); } runnableJobsInterface()->addRunnableJobs(jobs); m_d->efficiencyMeasurer.notifyFrameRenderingStarted(); } } } } else { issueSetDirtySignals(); } } void FreehandStrokeStrategy::issueSetDirtySignals() { QVector dirtyRects; for (int i = 0; i < numMaskedPainters(); i++) { KisMaskedFreehandStrokePainter *maskedPainter = this->maskedPainter(i); dirtyRects.append(maskedPainter->takeDirtyRegion()); } if (needsMaskingUpdates()) { // optimize the rects so that they would never intersect with each other! // that is a mandatory step for the multithreaded execution of merging jobs // sanity check: updates from the brush should have already been normalized // to the wrapping rect const KisDefaultBoundsBaseSP defaultBounds = targetNode()->projection()->defaultBounds(); if (defaultBounds->wrapAroundMode()) { const QRect wrapRect = defaultBounds->bounds(); for (auto it = dirtyRects.begin(); it != dirtyRects.end(); ++it) { KIS_SAFE_ASSERT_RECOVER(wrapRect.contains(*it)) { ENTER_FUNCTION() << ppVar(*it) << ppVar(wrapRect); *it = *it & wrapRect; } } } const int maxPatchSizeForMaskingUpdates = 64; const QRect totalRect = std::accumulate(dirtyRects.constBegin(), dirtyRects.constEnd(), QRect(), std::bit_or()); dirtyRects = KisPaintOpUtils::splitAndFilterDabRect(totalRect, dirtyRects, maxPatchSizeForMaskingUpdates); QVector jobs = doMaskingBrushUpdates(dirtyRects); jobs.append(new KisRunnableStrokeJobData( [this, dirtyRects] () { this->targetNode()->setDirty(dirtyRects); }, KisStrokeJobData::SEQUENTIAL)); runnableJobsInterface()->addRunnableJobs(jobs); } else { targetNode()->setDirty(dirtyRects); } //KisUpdateTimeMonitor::instance()->reportJobFinished(data, dirtyRects); } KisStrokeStrategy* FreehandStrokeStrategy::createLodClone(int levelOfDetail) { if (!m_d->resources->presetAllowsLod()) return 0; FreehandStrokeStrategy *clone = new FreehandStrokeStrategy(*this, levelOfDetail); return clone; } void FreehandStrokeStrategy::notifyUserStartedStroke() { m_d->efficiencyMeasurer.notifyCursorMoveStarted(); } void FreehandStrokeStrategy::notifyUserEndedStroke() { m_d->efficiencyMeasurer.notifyCursorMoveFinished(); } diff --git a/libs/ui/tool/strokes/kis_color_picker_stroke_strategy.cpp b/libs/ui/tool/strokes/kis_color_picker_stroke_strategy.cpp index 97252ff918..290d40c30d 100644 --- a/libs/ui/tool/strokes/kis_color_picker_stroke_strategy.cpp +++ b/libs/ui/tool/strokes/kis_color_picker_stroke_strategy.cpp @@ -1,73 +1,74 @@ /* * Copyright (c) 2016 Dmitry Kazakov * * 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_color_picker_stroke_strategy.h" #include "kis_tool_utils.h" #include "kis_paint_device.h" struct KisColorPickerStrokeStrategy::Private { Private() : shouldSkipWork(false) {} bool shouldSkipWork; int radius = 1; int blend = 100; }; KisColorPickerStrokeStrategy::KisColorPickerStrokeStrategy(int lod) - : m_d(new Private) + : KisSimpleStrokeStrategy(QLatin1String("KisColorPickerStrokeStrategy")), + m_d(new Private) { setSupportsWrapAroundMode(true); enableJob(KisSimpleStrokeStrategy::JOB_DOSTROKE); KisToolUtils::ColorPickerConfig config; config.load(); m_d->radius = qMax(1, qRound(config.radius * KisLodTransform::lodToScale(lod))); m_d->blend = config.blend; } KisColorPickerStrokeStrategy::~KisColorPickerStrokeStrategy() { } void KisColorPickerStrokeStrategy::doStrokeCallback(KisStrokeJobData *data) { if (m_d->shouldSkipWork) return; Data *d = dynamic_cast(data); KIS_ASSERT_RECOVER_RETURN(d); KoColor color; KoColor previous = d->currentColor; if (KisToolUtils::pickColor(color, d->dev, d->pt, &previous, m_d->radius, m_d->blend) == true) { emit sigColorUpdated(color); } } KisStrokeStrategy* KisColorPickerStrokeStrategy::createLodClone(int levelOfDetail) { m_d->shouldSkipWork = true; KisColorPickerStrokeStrategy *lodStrategy = new KisColorPickerStrokeStrategy(levelOfDetail); connect(lodStrategy, &KisColorPickerStrokeStrategy::sigColorUpdated, this, &KisColorPickerStrokeStrategy::sigColorUpdated, Qt::DirectConnection); return lodStrategy; } diff --git a/libs/ui/tool/strokes/kis_filter_stroke_strategy.cpp b/libs/ui/tool/strokes/kis_filter_stroke_strategy.cpp index 5ba8d96933..c31a634088 100644 --- a/libs/ui/tool/strokes/kis_filter_stroke_strategy.cpp +++ b/libs/ui/tool/strokes/kis_filter_stroke_strategy.cpp @@ -1,208 +1,208 @@ /* * Copyright (c) 2013 Dmitry Kazakov * * 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_filter_stroke_strategy.h" #include #include #include #include struct KisFilterStrokeStrategy::Private { Private() : updatesFacade(0), cancelSilently(false), secondaryTransaction(0), levelOfDetail(0) { } Private(const Private &rhs) : filter(rhs.filter), filterConfig(rhs.filterConfig), node(rhs.node), updatesFacade(rhs.updatesFacade), cancelSilently(rhs.cancelSilently), filterDevice(), filterDeviceBounds(), secondaryTransaction(0), progressHelper(), levelOfDetail(0) { KIS_ASSERT_RECOVER_RETURN(!rhs.filterDevice); KIS_ASSERT_RECOVER_RETURN(rhs.filterDeviceBounds.isEmpty()); KIS_ASSERT_RECOVER_RETURN(!rhs.secondaryTransaction); KIS_ASSERT_RECOVER_RETURN(!rhs.progressHelper); KIS_ASSERT_RECOVER_RETURN(!rhs.levelOfDetail); } KisFilterSP filter; KisFilterConfigurationSP filterConfig; KisNodeSP node; KisUpdatesFacade *updatesFacade; bool cancelSilently; KisPaintDeviceSP filterDevice; QRect filterDeviceBounds; KisTransaction *secondaryTransaction; QScopedPointer progressHelper; int levelOfDetail; }; KisFilterStrokeStrategy::KisFilterStrokeStrategy(KisFilterSP filter, KisFilterConfigurationSP filterConfig, KisResourcesSnapshotSP resources) - : KisPainterBasedStrokeStrategy("FILTER_STROKE", + : KisPainterBasedStrokeStrategy(QLatin1String("FILTER_STROKE"), kundo2_i18n("Filter \"%1\"", filter->name()), resources, QVector(),false), m_d(new Private()) { m_d->filter = filter; m_d->filterConfig = filterConfig; m_d->node = resources->currentNode(); m_d->updatesFacade = resources->image().data(); m_d->cancelSilently = false; m_d->secondaryTransaction = 0; m_d->levelOfDetail = 0; setSupportsWrapAroundMode(true); enableJob(KisSimpleStrokeStrategy::JOB_DOSTROKE); } KisFilterStrokeStrategy::KisFilterStrokeStrategy(const KisFilterStrokeStrategy &rhs, int levelOfDetail) : KisPainterBasedStrokeStrategy(rhs, levelOfDetail), m_d(new Private(*rhs.m_d)) { // only non-started transaction are allowed KIS_ASSERT_RECOVER_NOOP(!m_d->secondaryTransaction); m_d->levelOfDetail = levelOfDetail; } KisFilterStrokeStrategy::~KisFilterStrokeStrategy() { delete m_d; } void KisFilterStrokeStrategy::initStrokeCallback() { KisPainterBasedStrokeStrategy::initStrokeCallback(); KisPaintDeviceSP dev = targetDevice(); m_d->filterDeviceBounds = dev->extent(); if (activeSelection() || (dev->colorSpace() != dev->compositionSourceColorSpace() && *dev->colorSpace() != *dev->compositionSourceColorSpace())) { m_d->filterDevice = dev->createCompositionSourceDevice(dev); m_d->secondaryTransaction = new KisTransaction(m_d->filterDevice); if (activeSelection()) { m_d->filterDeviceBounds &= activeSelection()->selectedRect(); } } else { m_d->filterDevice = dev; } m_d->progressHelper.reset(new KisProcessingVisitor::ProgressHelper(m_d->node)); } void KisFilterStrokeStrategy::doStrokeCallback(KisStrokeJobData *data) { Data *d = dynamic_cast(data); CancelSilentlyMarker *cancelJob = dynamic_cast(data); if (d) { const QRect rc = d->processRect; if (!m_d->filterDeviceBounds.intersects( m_d->filter->neededRect(rc, m_d->filterConfig.data(), m_d->levelOfDetail))) { return; } m_d->filter->processImpl(m_d->filterDevice, rc, m_d->filterConfig.data(), m_d->progressHelper->updater()); if (m_d->secondaryTransaction) { KisPainter::copyAreaOptimized(rc.topLeft(), m_d->filterDevice, targetDevice(), rc, activeSelection()); // Free memory m_d->filterDevice->clear(rc); } m_d->node->setDirty(rc); } else if (cancelJob) { m_d->cancelSilently = true; } else { qFatal("KisFilterStrokeStrategy: job type is not known"); } } void KisFilterStrokeStrategy::cancelStrokeCallback() { delete m_d->secondaryTransaction; m_d->filterDevice = 0; KisProjectionUpdatesFilterSP prevUpdatesFilter; if (m_d->cancelSilently) { /** * TODO: Projection updates filter is not recursive, please * redesign this part */ prevUpdatesFilter = m_d->updatesFacade->projectionUpdatesFilter(); if (prevUpdatesFilter) { m_d->updatesFacade->setProjectionUpdatesFilter(KisProjectionUpdatesFilterSP()); } m_d->updatesFacade->disableDirtyRequests(); } KisPainterBasedStrokeStrategy::cancelStrokeCallback(); if (m_d->cancelSilently) { m_d->updatesFacade->enableDirtyRequests(); if (prevUpdatesFilter) { m_d->updatesFacade->setProjectionUpdatesFilter(prevUpdatesFilter); prevUpdatesFilter.clear(); } } } void KisFilterStrokeStrategy::finishStrokeCallback() { delete m_d->secondaryTransaction; m_d->filterDevice = 0; KisPainterBasedStrokeStrategy::finishStrokeCallback(); } KisStrokeStrategy* KisFilterStrokeStrategy::createLodClone(int levelOfDetail) { if (!m_d->filter->supportsLevelOfDetail(m_d->filterConfig.data(), levelOfDetail)) return 0; KisFilterStrokeStrategy *clone = new KisFilterStrokeStrategy(*this, levelOfDetail); return clone; } diff --git a/libs/ui/tool/strokes/kis_painter_based_stroke_strategy.cpp b/libs/ui/tool/strokes/kis_painter_based_stroke_strategy.cpp index c496f3bc58..ce0b6ef189 100644 --- a/libs/ui/tool/strokes/kis_painter_based_stroke_strategy.cpp +++ b/libs/ui/tool/strokes/kis_painter_based_stroke_strategy.cpp @@ -1,401 +1,401 @@ /* * Copyright (c) 2011 Dmitry Kazakov * * 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_painter_based_stroke_strategy.h" #include #include #include #include "kis_painter.h" #include "kis_paint_device.h" #include "kis_paint_layer.h" #include "kis_transaction.h" #include "kis_image.h" #include #include "kis_undo_stores.h" #include "KisFreehandStrokeInfo.h" #include "KisMaskedFreehandStrokePainter.h" #include "KisMaskingBrushRenderer.h" #include "KisRunnableStrokeJobData.h" #include "kis_paintop_preset.h" #include "kis_paintop_settings.h" -KisPainterBasedStrokeStrategy::KisPainterBasedStrokeStrategy(const QString &id, +KisPainterBasedStrokeStrategy::KisPainterBasedStrokeStrategy(const QLatin1String &id, const KUndo2MagicString &name, KisResourcesSnapshotSP resources, QVector strokeInfos,bool useMergeID) : KisRunnableBasedStrokeStrategy(id, name), m_resources(resources), m_strokeInfos(strokeInfos), m_transaction(0), m_useMergeID(useMergeID), m_supportsMaskingBrush(false), m_supportsIndirectPainting(false) { init(); } -KisPainterBasedStrokeStrategy::KisPainterBasedStrokeStrategy(const QString &id, +KisPainterBasedStrokeStrategy::KisPainterBasedStrokeStrategy(const QLatin1String &id, const KUndo2MagicString &name, KisResourcesSnapshotSP resources, KisFreehandStrokeInfo *strokeInfo,bool useMergeID) : KisRunnableBasedStrokeStrategy(id, name), m_resources(resources), m_strokeInfos(QVector() << strokeInfo), m_transaction(0), m_useMergeID(useMergeID), m_supportsMaskingBrush(false), m_supportsIndirectPainting(false) { init(); } KisPainterBasedStrokeStrategy::~KisPainterBasedStrokeStrategy() { } void KisPainterBasedStrokeStrategy::init() { enableJob(KisSimpleStrokeStrategy::JOB_INIT); enableJob(KisSimpleStrokeStrategy::JOB_FINISH); enableJob(KisSimpleStrokeStrategy::JOB_CANCEL, true, KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::EXCLUSIVE); enableJob(KisSimpleStrokeStrategy::JOB_SUSPEND); enableJob(KisSimpleStrokeStrategy::JOB_RESUME); } KisPainterBasedStrokeStrategy::KisPainterBasedStrokeStrategy(const KisPainterBasedStrokeStrategy &rhs, int levelOfDetail) : KisRunnableBasedStrokeStrategy(rhs), m_resources(rhs.m_resources), m_transaction(rhs.m_transaction), m_useMergeID(rhs.m_useMergeID), m_supportsMaskingBrush(rhs.m_supportsMaskingBrush), m_supportsIndirectPainting(rhs.m_supportsIndirectPainting) { Q_FOREACH (KisFreehandStrokeInfo *info, rhs.m_strokeInfos) { m_strokeInfos.append(new KisFreehandStrokeInfo(info, levelOfDetail)); } KIS_ASSERT_RECOVER_NOOP( rhs.m_maskStrokeInfos.isEmpty() && !rhs.m_transaction && !rhs.m_targetDevice && !rhs.m_activeSelection && "After the stroke has been started, no copying must happen"); } KisPaintDeviceSP KisPainterBasedStrokeStrategy::targetDevice() const { return m_targetDevice; } KisSelectionSP KisPainterBasedStrokeStrategy::activeSelection() const { return m_activeSelection; } KisMaskedFreehandStrokePainter *KisPainterBasedStrokeStrategy::maskedPainter(int strokeInfoId) { return m_maskedPainters[strokeInfoId]; } int KisPainterBasedStrokeStrategy::numMaskedPainters() const { return m_maskedPainters.size(); } bool KisPainterBasedStrokeStrategy::needsMaskingUpdates() const { return m_maskingBrushRenderer; } QVector KisPainterBasedStrokeStrategy::doMaskingBrushUpdates(const QVector &rects) { QVector jobs; KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(m_maskingBrushRenderer, jobs); Q_FOREACH (const QRect &rc, rects) { jobs.append(new KisRunnableStrokeJobData( [this, rc] () { this->m_maskingBrushRenderer->updateProjection(rc); }, KisStrokeJobData::CONCURRENT)); } return jobs; } void KisPainterBasedStrokeStrategy::setSupportsMaskingBrush(bool value) { m_supportsMaskingBrush = value; } bool KisPainterBasedStrokeStrategy::supportsMaskingBrush() const { return m_supportsMaskingBrush; } void KisPainterBasedStrokeStrategy::setSupportsIndirectPainting(bool value) { m_supportsIndirectPainting = value; } bool KisPainterBasedStrokeStrategy::supportsIndirectPainting() const { return m_supportsIndirectPainting; } void KisPainterBasedStrokeStrategy::initPainters(KisPaintDeviceSP targetDevice, KisPaintDeviceSP maskingDevice, KisSelectionSP selection, bool hasIndirectPainting, const QString &indirectPaintingCompositeOp) { Q_FOREACH (KisFreehandStrokeInfo *info, m_strokeInfos) { KisPainter *painter = info->painter; painter->begin(targetDevice, !hasIndirectPainting ? selection : 0); painter->setRunnableStrokeJobsInterface(runnableJobsInterface()); m_resources->setupPainter(painter); if(hasIndirectPainting) { painter->setCompositeOp(targetDevice->colorSpace()->compositeOp(indirectPaintingCompositeOp)); painter->setOpacity(OPACITY_OPAQUE_U8); painter->setChannelFlags(QBitArray()); } } if (maskingDevice) { for (int i = 0; i < m_strokeInfos.size(); i++) { KisFreehandStrokeInfo *maskingInfo = new KisFreehandStrokeInfo(*m_strokeInfos[i]->dragDistance); KisPainter *painter = maskingInfo->painter; painter->begin(maskingDevice, 0); m_resources->setupMaskingBrushPainter(painter); KIS_SAFE_ASSERT_RECOVER_NOOP(hasIndirectPainting); m_maskStrokeInfos.append(maskingInfo); } } for (int i = 0; i < m_strokeInfos.size(); i++) { m_maskedPainters.append( new KisMaskedFreehandStrokePainter(m_strokeInfos[i], !m_maskStrokeInfos.isEmpty() ? m_maskStrokeInfos[i] : 0)); } } void KisPainterBasedStrokeStrategy::deletePainters() { Q_FOREACH (KisFreehandStrokeInfo *info, m_strokeInfos) { delete info; } m_strokeInfos.clear(); Q_FOREACH (KisFreehandStrokeInfo *info, m_maskStrokeInfos) { delete info; } m_maskStrokeInfos.clear(); Q_FOREACH (KisMaskedFreehandStrokePainter *info, m_maskedPainters) { delete info; } m_maskedPainters.clear(); } void KisPainterBasedStrokeStrategy::initStrokeCallback() { KisNodeSP node = m_resources->currentNode(); KisPaintDeviceSP paintDevice = node->paintDevice(); KisPaintDeviceSP targetDevice = paintDevice; bool hasIndirectPainting = supportsIndirectPainting() && m_resources->needsIndirectPainting(); const QString indirectCompositeOp = m_resources->indirectPaintingCompositeOp(); KisSelectionSP selection = m_resources->activeSelection(); if (hasIndirectPainting) { KisIndirectPaintingSupport *indirect = dynamic_cast(node.data()); if (indirect) { targetDevice = paintDevice->createCompositionSourceDevice(); targetDevice->setParentNode(node); indirect->setCurrentColor(m_resources->currentFgColor()); indirect->setTemporaryTarget(targetDevice); indirect->setTemporaryCompositeOp(m_resources->compositeOpId()); indirect->setTemporaryOpacity(m_resources->opacity()); indirect->setTemporarySelection(selection); QBitArray channelLockFlags = m_resources->channelLockFlags(); indirect->setTemporaryChannelFlags(channelLockFlags); } else { hasIndirectPainting = false; } } if (m_useMergeID) { m_transaction = new KisTransaction(name(), targetDevice, 0, timedID(this->id())); } else { m_transaction = new KisTransaction(name(), targetDevice); } // WARNING: masked brush cannot work without indirect painting mode! KIS_SAFE_ASSERT_RECOVER_NOOP(!(supportsMaskingBrush() && m_resources->needsMaskingBrushRendering()) || hasIndirectPainting); if (hasIndirectPainting && supportsMaskingBrush() && m_resources->needsMaskingBrushRendering()) { const QString compositeOpId = m_resources->currentPaintOpPreset()->settings()->maskingBrushCompositeOp(); m_maskingBrushRenderer.reset(new KisMaskingBrushRenderer(targetDevice, compositeOpId)); initPainters(m_maskingBrushRenderer->strokeDevice(), m_maskingBrushRenderer->maskDevice(), selection, hasIndirectPainting, indirectCompositeOp); } else { initPainters(targetDevice, 0, selection, hasIndirectPainting, indirectCompositeOp); } m_targetDevice = targetDevice; m_activeSelection = selection; // sanity check: selection should be applied only once if (selection && !m_strokeInfos.isEmpty()) { KisIndirectPaintingSupport *indirect = dynamic_cast(node.data()); KIS_ASSERT_RECOVER_RETURN(hasIndirectPainting || m_strokeInfos.first()->painter->selection()); KIS_ASSERT_RECOVER_RETURN(!hasIndirectPainting || !indirect->temporarySelection() || !m_strokeInfos.first()->painter->selection()); } } void KisPainterBasedStrokeStrategy::finishStrokeCallback() { KisNodeSP node = m_resources->currentNode(); KisIndirectPaintingSupport *indirect = dynamic_cast(node.data()); KisPostExecutionUndoAdapter *undoAdapter = m_resources->postExecutionUndoAdapter(); QScopedPointer dumbUndoAdapter; QScopedPointer dumbUndoStore; if (!undoAdapter) { dumbUndoStore.reset(new KisDumbUndoStore()); dumbUndoAdapter.reset(new KisPostExecutionUndoAdapter(dumbUndoStore.data(), 0)); undoAdapter = dumbUndoAdapter.data(); } if (indirect && indirect->hasTemporaryTarget()) { KUndo2MagicString transactionText = m_transaction->text(); m_transaction->end(); if(m_useMergeID){ indirect->mergeToLayer(node, undoAdapter, transactionText,timedID(this->id())); } else{ indirect->mergeToLayer(node, undoAdapter, transactionText); } } else { m_transaction->commit(undoAdapter); } delete m_transaction; deletePainters(); } void KisPainterBasedStrokeStrategy::cancelStrokeCallback() { KisNodeSP node = m_resources->currentNode(); KisIndirectPaintingSupport *indirect = dynamic_cast(node.data()); bool revert = true; if (indirect) { KisPaintDeviceSP t = indirect->temporaryTarget(); if (t) { delete m_transaction; deletePainters(); QRegion region = t->region(); indirect->setTemporaryTarget(0); node->setDirty(region); revert = false; } } if (revert) { m_transaction->revert(); delete m_transaction; deletePainters(); } } void KisPainterBasedStrokeStrategy::suspendStrokeCallback() { KisNodeSP node = m_resources->currentNode(); KisIndirectPaintingSupport *indirect = dynamic_cast(node.data()); if(indirect && indirect->hasTemporaryTarget()) { indirect->setTemporaryTarget(0); } } void KisPainterBasedStrokeStrategy::resumeStrokeCallback() { KisNodeSP node = m_resources->currentNode(); KisIndirectPaintingSupport *indirect = dynamic_cast(node.data()); if(indirect) { // todo: don't ask about paint device // todo:change to an assert if (node->paintDevice() != m_targetDevice) { indirect->setTemporaryTarget(m_targetDevice); indirect->setTemporaryCompositeOp(m_resources->compositeOpId()); indirect->setTemporaryOpacity(m_resources->opacity()); indirect->setTemporarySelection(m_activeSelection); } } } KisNodeSP KisPainterBasedStrokeStrategy::targetNode() const { return m_resources->currentNode(); } diff --git a/libs/ui/tool/strokes/kis_painter_based_stroke_strategy.h b/libs/ui/tool/strokes/kis_painter_based_stroke_strategy.h index b838207e22..908c29ade5 100644 --- a/libs/ui/tool/strokes/kis_painter_based_stroke_strategy.h +++ b/libs/ui/tool/strokes/kis_painter_based_stroke_strategy.h @@ -1,140 +1,140 @@ /* * Copyright (c) 2011 Dmitry Kazakov * * 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. */ #ifndef __KIS_PAINTER_BASED_STROKE_STRATEGY_H #define __KIS_PAINTER_BASED_STROKE_STRATEGY_H #include #include "KisRunnableBasedStrokeStrategy.h" #include "kis_resources_snapshot.h" #include "kis_selection.h" class KisPainter; class KisDistanceInformation; class KisTransaction; class KisFreehandStrokeInfo; class KisMaskedFreehandStrokePainter; class KisMaskingBrushRenderer; class KisRunnableStrokeJobData; class KRITAUI_EXPORT KisPainterBasedStrokeStrategy : public KisRunnableBasedStrokeStrategy { public: - KisPainterBasedStrokeStrategy(const QString &id, + KisPainterBasedStrokeStrategy(const QLatin1String &id, const KUndo2MagicString &name, KisResourcesSnapshotSP resources, QVector strokeInfos, bool useMergeID = false); - KisPainterBasedStrokeStrategy(const QString &id, + KisPainterBasedStrokeStrategy(const QLatin1String &id, const KUndo2MagicString &name, KisResourcesSnapshotSP resources, - KisFreehandStrokeInfo *strokeInfo,bool useMergeID = false); + KisFreehandStrokeInfo *strokeInfo, bool useMergeID = false); ~KisPainterBasedStrokeStrategy(); void initStrokeCallback() override; void finishStrokeCallback() override; void cancelStrokeCallback() override; void suspendStrokeCallback() override; void resumeStrokeCallback() override; protected: KisNodeSP targetNode() const; KisPaintDeviceSP targetDevice() const; KisSelectionSP activeSelection() const; KisMaskedFreehandStrokePainter* maskedPainter(int strokeInfoId); int numMaskedPainters() const; void setUndoEnabled(bool value); /** * Return true if the descendant should execute a few more jobs before issuing setDirty() * call on the layer. * * If the returned value is true, then the stroke actually paints **not** on the * layer's paint device, but on some intermediate device owned by * KisPainterBasedStrokeStrategy and one should merge it first before asking the * update. * * The value can be true only when the stroke is declared to support masked brush! * \see supportsMaskingBrush() */ bool needsMaskingUpdates() const; /** * Create a list of update jobs that should be run before issuing the setDirty() * call on the node * * \see needsMaskingUpdates() */ QVector doMaskingBrushUpdates(const QVector &rects); protected: /** * The descendants may declare if this stroke should support auto-creation * of the masked brush. Default value: false */ void setSupportsMaskingBrush(bool value); /** * Return if the stroke should auto-create a masked brush from the provided * paintop preset or not */ bool supportsMaskingBrush() const; void setSupportsIndirectPainting(bool value); bool supportsIndirectPainting() const; protected: KisPainterBasedStrokeStrategy(const KisPainterBasedStrokeStrategy &rhs, int levelOfDetail); private: void init(); void initPainters(KisPaintDeviceSP targetDevice, KisPaintDeviceSP maskingDevice, KisSelectionSP selection, bool hasIndirectPainting, const QString &indirectPaintingCompositeOp); void deletePainters(); inline int timedID(const QString &id){ return int(qHash(id)); } private: KisResourcesSnapshotSP m_resources; QVector m_strokeInfos; QVector m_maskStrokeInfos; QVector m_maskedPainters; KisTransaction *m_transaction; QScopedPointer m_maskingBrushRenderer; KisPaintDeviceSP m_targetDevice; KisSelectionSP m_activeSelection; bool m_useMergeID; bool m_supportsMaskingBrush; bool m_supportsIndirectPainting; }; #endif /* __KIS_PAINTER_BASED_STROKE_STRATEGY_H */ diff --git a/libs/ui/widgets/kis_preset_live_preview_view.cpp b/libs/ui/widgets/kis_preset_live_preview_view.cpp index b719743ebf..5a488e6ca4 100644 --- a/libs/ui/widgets/kis_preset_live_preview_view.cpp +++ b/libs/ui/widgets/kis_preset_live_preview_view.cpp @@ -1,383 +1,384 @@ /* * Copyright (c) 2017 Scott Petrovic * * 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 #include #include #include "kis_paintop_settings.h" #include #include #include "KisAsyncronousStrokeUpdateHelper.h" #include KisPresetLivePreviewView::KisPresetLivePreviewView(QWidget *parent) : QGraphicsView(parent), m_updateCompressor(100, KisSignalCompressor::FIRST_ACTIVE) { connect(&m_updateCompressor, SIGNAL(timeout()), SLOT(updateStroke())); } KisPresetLivePreviewView::~KisPresetLivePreviewView() { delete m_noPreviewText; delete m_brushPreviewScene; } void KisPresetLivePreviewView::setup() { // initializing to 0 helps check later if they actually have something in them m_noPreviewText = 0; m_sceneImageItem = 0; setHorizontalScrollBarPolicy ( Qt::ScrollBarAlwaysOff ); setVerticalScrollBarPolicy ( Qt::ScrollBarAlwaysOff ); // layer image needs to be big enough to get an entire stroke of data m_canvasSize.setWidth(this->width()); m_canvasSize.setHeight(this->height()); m_canvasCenterPoint.setX(m_canvasSize.width()*0.5); m_canvasCenterPoint.setY(m_canvasSize.height()*0.5); m_colorSpace = KoColorSpaceRegistry::instance()->rgb8(); m_image = new KisImage(0, m_canvasSize.width(), m_canvasSize.height(), m_colorSpace, "stroke sample image"); m_layer = new KisPaintLayer(m_image, "livePreviewStrokeSample", OPACITY_OPAQUE_U8, m_colorSpace); // set scene for the view m_brushPreviewScene = new QGraphicsScene(); setScene(m_brushPreviewScene); } void KisPresetLivePreviewView::setCurrentPreset(KisPaintOpPresetSP preset) { m_currentPreset = preset; } void KisPresetLivePreviewView::requestUpdateStroke() { m_updateCompressor.start(); } void KisPresetLivePreviewView::updateStroke() { // do not paint a stroke if we are any of these engines (they have some issue currently) if (m_currentPreset->paintOp().id() == "roundmarker" || m_currentPreset->paintOp().id() == "experimentbrush" || m_currentPreset->paintOp().id() == "duplicate") { paintBackground(); slotPreviewGenerationCompleted(); return; } if (!m_previewGenerationInProgress) { paintBackground(); setupAndPaintStroke(); } else { m_updateCompressor.start(); } } void KisPresetLivePreviewView::slotPreviewGenerationCompleted() { m_previewGenerationInProgress = false; QImage m_temp_image; m_temp_image = m_layer->paintDevice()->convertToQImage(0, m_image->bounds()); // only add the object once...then just update the pixmap so we can move the preview around if (!m_sceneImageItem) { m_sceneImageItem = m_brushPreviewScene->addPixmap(QPixmap::fromImage(m_temp_image)); } else { m_sceneImageItem->setPixmap(QPixmap::fromImage(m_temp_image)); } } void KisPresetLivePreviewView::paintBackground() { // clean up "no preview" text object if it exists. we will add it later if we need it if (m_noPreviewText) { this->scene()->removeItem(m_noPreviewText); m_noPreviewText = 0; } if (m_currentPreset->paintOp().id() == "colorsmudge" || m_currentPreset->paintOp().id() == "deformbrush" || m_currentPreset->paintOp().id() == "filter") { // easier to see deformations and smudging with alternating stripes in the background // paint the whole background with alternating stripes // filter engine may or may not show things depending on the filter...but it is better than nothing int grayStrips = 20; for (int i=0; i < grayStrips; i++ ) { float sectionPercent = 1.0 / (float)grayStrips; bool isAlternating = i % 2; KoColor fillColor(m_layer->paintDevice()->colorSpace()); if (isAlternating) { fillColor.fromQColor(QColor(80,80,80)); } else { fillColor.fromQColor(QColor(140,140,140)); } const QRect fillRect(m_layer->image()->width()*sectionPercent*i, 0, m_layer->image()->width()*(sectionPercent*i +sectionPercent), m_layer->image()->height()); m_layer->paintDevice()->fill(fillRect, fillColor); } m_paintColor = KoColor(Qt::white, m_colorSpace); } else if (m_currentPreset->paintOp().id() == "roundmarker" || m_currentPreset->paintOp().id() == "experimentbrush" || m_currentPreset->paintOp().id() == "duplicate" ) { // cases where we will not show a preview for now // roundbrush (quick) -- this isn't showing anything, disable showing preview // experimentbrush -- this creates artifacts that carry over to other previews and messes up their display // duplicate (clone) brush doesn't have a preview as it doesn't show anything) if(m_sceneImageItem) { this->scene()->removeItem(m_sceneImageItem); m_sceneImageItem = 0; } QFont font; font.setPixelSize(14); font.setBold(false); m_noPreviewText = this->scene()->addText(i18n("No Preview for this engine"),font); m_noPreviewText->setPos(50, this->height()/4); return; } else { // fill with gray first to clear out what existed from previous preview m_layer->paintDevice()->fill(m_image->bounds(), KoColor(palette().color(QPalette::Background) , m_colorSpace)); m_paintColor = KoColor(palette().color(QPalette::Text), m_colorSpace); } } class NotificationStroke : public QObject, public KisSimpleStrokeStrategy { Q_OBJECT public: NotificationStroke() + : KisSimpleStrokeStrategy(QLatin1String("NotificationStroke")) { setClearsRedoOnStart(false); this->enableJob(JOB_INIT, true, KisStrokeJobData::BARRIER); this->enableJob(JOB_CANCEL, true, KisStrokeJobData::BARRIER); } void initStrokeCallback() { emit timeout(); } void cancelStrokeCallback() { emit cancelled(); } Q_SIGNALS: void timeout(); void cancelled(); }; void KisPresetLivePreviewView::setupAndPaintStroke() { // limit the brush stroke size. larger brush strokes just don't look good and are CPU intensive // we are making a proxy preset and setting it to the painter...otherwise setting the brush size of the original preset // will fire off signals that make this run in an infinite loop qreal originalPresetSize = m_currentPreset->settings()->paintOpSize(); qreal previewSize = qBound(3.0, m_currentPreset->settings()->paintOpSize(), 25.0 ); // constrain live preview brush size //Except for the sketchbrush where it determine sthe history. if (m_currentPreset->paintOp().id() == "sketchbrush" || m_currentPreset->paintOp().id() == "spraybrush") { previewSize = qMax(3.0, m_currentPreset->settings()->paintOpSize()); } KisPaintOpPresetSP proxy_preset = m_currentPreset->clone(); KisPaintOpSettingsSP settings = proxy_preset->settings(); settings->setPaintOpSize(previewSize); int maxTextureSize = 200; int textureOffsetX = settings->getInt("Texture/Pattern/MaximumOffsetX")*2; int textureOffsetY = settings->getInt("Texture/Pattern/MaximumOffsetY")*2; double textureScale = settings->getDouble("Texture/Pattern/Scale"); if ( textureOffsetX*textureScale> maxTextureSize || textureOffsetY*textureScale > maxTextureSize) { int maxSize = qMax(textureOffsetX, textureOffsetY); double result = qreal(maxTextureSize) / maxSize; settings->setProperty("Texture/Pattern/Scale", result); } if (proxy_preset->paintOp().id() == "spraybrush") { QDomElement element; QDomDocument d; QString brushDefinition = settings->getString("brush_definition"); if (!brushDefinition.isEmpty()) { d.setContent(brushDefinition, false); element = d.firstChildElement("Brush"); KisBrushSP brush = KisBrush::fromXML(element); qreal width = brush->image().width(); qreal scale = brush->scale(); qreal diameterToBrushRatio = 1.0; qreal diameter = settings->getInt("Spray/diameter"); //hack, 1000 being the maximum possible brushsize. if (brush->filename().endsWith(".svg")) { diameterToBrushRatio = diameter/(1000.0*scale); scale = 25.0 / 1000.0; } else { if (width * scale > 25.0) { diameterToBrushRatio = diameter / (width * scale); scale = 25.0 / width; } } settings->setProperty("Spray/diameter", int(25.0 * diameterToBrushRatio)); brush->setScale(scale); d.clear(); element = d.createElement("Brush"); brush->toXML(d, element); d.appendChild(element); settings->setProperty("brush_definition", d.toString()); } } // Preset preview cannot display gradient color source: there is // no resource manager for KisResourcesSnapshot, therefore gradient is nullptr. // BUG: 385521 (Selecting "Gradient" in brush editor crashes krita) if (proxy_preset->paintOp().id() == "paintbrush") { QString colorSourceType = settings->getString("ColorSource/Type", "plain"); if (colorSourceType == "gradient") { settings->setProperty("ColorSource/Type", "plain"); } } proxy_preset->setSettings(settings); KisResourcesSnapshotSP resources = new KisResourcesSnapshot(m_image, m_layer); resources->setOpacity(settings->paintOpOpacity()); resources->setBrush(proxy_preset); resources->setFGColorOverride(m_paintColor); KisFreehandStrokeInfo *strokeInfo = new KisFreehandStrokeInfo(); KisStrokeStrategy *stroke = new FreehandStrokeStrategy(resources, strokeInfo, kundo2_noi18n("temp_stroke")); KisStrokeId strokeId = m_image->startStroke(stroke); // paint the stroke. The sketchbrush gets a different shape than the others to show how it works if (proxy_preset->paintOp().id() == "sketchbrush" || proxy_preset->paintOp().id() == "curvebrush" || proxy_preset->paintOp().id() == "particlebrush") { qreal startX = m_canvasCenterPoint.x() - (this->width()*0.4); qreal endX = m_canvasCenterPoint.x() + (this->width()*0.4); qreal middle = m_canvasCenterPoint.y(); KisPaintInformation pointOne; pointOne.setPressure(0.0); pointOne.setPos(QPointF(startX, middle)); KisPaintInformation pointTwo; pointTwo.setPressure(0.0); pointTwo.setPos(QPointF(startX, middle)); int repeats = 8; for (int i = 0; i < repeats; i++) { pointOne.setPos(pointTwo.pos()); pointOne.setPressure(pointTwo.pressure()); pointTwo.setPressure((1.0/repeats)*(i+1)); qreal xPos = ((1.0/repeats) * (i+1) * (endX-startX) )+startX; pointTwo.setPos(QPointF(xPos, middle)); qreal offset = (this->height()/(repeats*1.5))*(i+1); qreal handleY = middle + offset; if (i%2 == 0) { handleY = middle - offset; } m_image->addJob(strokeId, new FreehandStrokeStrategy::Data(0, pointOne, QPointF(pointOne.pos().x(), handleY), QPointF(pointTwo.pos().x(), handleY), pointTwo)); m_image->addJob(strokeId, new KisAsyncronousStrokeUpdateHelper::UpdateData(true)); } } else { // paint an S curve m_curvePointPI1.setPos(QPointF(m_canvasCenterPoint.x() - (this->width()*0.45), m_canvasCenterPoint.y() + (this->height()*0.2))); m_curvePointPI1.setPressure(0.0); m_curvePointPI2.setPos(QPointF(m_canvasCenterPoint.x() + (this->width()*0.4), m_canvasCenterPoint.y() - (this->height()*0.2) )); m_curvePointPI2.setPressure(1.0); m_image->addJob(strokeId, new FreehandStrokeStrategy::Data(0, m_curvePointPI1, QPointF(m_canvasCenterPoint.x(), m_canvasCenterPoint.y()-this->height()), QPointF(m_canvasCenterPoint.x(), m_canvasCenterPoint.y()+this->height()), m_curvePointPI2)); m_image->addJob(strokeId, new KisAsyncronousStrokeUpdateHelper::UpdateData(true)); } m_image->endStroke(strokeId); m_previewGenerationInProgress = true; NotificationStroke *notificationStroke = new NotificationStroke(); connect(notificationStroke, SIGNAL(timeout()), SLOT(slotPreviewGenerationCompleted())); KisStrokeId notificationId = m_image->startStroke(notificationStroke); m_image->endStroke(notificationId); // TODO: if we don't have any regressions because of it until 4.2.8, then // just remove this code. // even though the brush is cloned, the proxy_preset still has some connection to the original preset which will mess brush sizing // we need to return brush size to normal.The normal brush sends out a lot of extra signals, so keeping the proxy for now //proxy_preset->settings()->setPaintOpSize(originalPresetSize); } #include "kis_preset_live_preview_view.moc" diff --git a/plugins/dockers/overview/overviewwidget.cc b/plugins/dockers/overview/overviewwidget.cc index 1b53b0b1d3..5cf840c60d 100644 --- a/plugins/dockers/overview/overviewwidget.cc +++ b/plugins/dockers/overview/overviewwidget.cc @@ -1,384 +1,385 @@ /* * Copyright (c) 2009 Cyrille Berger * Copyright (c) 2014 Sven Langkamp * * This library is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; 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 Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser 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 "overviewwidget.h" #include #include #include #include #include #include #include #include #include #include #include #include #include "kis_idle_watcher.h" #include "krita_utils.h" #include "kis_painter.h" #include #include "kis_transform_worker.h" #include "kis_filter_strategy.h" #include #include const qreal oversample = 2.; const int thumbnailTileDim = 128; struct OverviewThumbnailStrokeStrategy::Private { class ProcessData : public KisStrokeJobData { public: ProcessData(KisPaintDeviceSP _dev, KisPaintDeviceSP _thumbDev, const QSize& _thumbnailSize, const QRect &_rect) : KisStrokeJobData(CONCURRENT), dev(_dev), thumbDev(_thumbDev), thumbnailSize(_thumbnailSize), tileRect(_rect) {} KisPaintDeviceSP dev; KisPaintDeviceSP thumbDev; QSize thumbnailSize; QRect tileRect; }; class FinishProcessing : public KisStrokeJobData { public: FinishProcessing(KisPaintDeviceSP _thumbDev, const QSize& _thumbnailSize) : KisStrokeJobData(SEQUENTIAL), thumbDev(_thumbDev), thumbnailSize(_thumbnailSize) {} KisPaintDeviceSP thumbDev; QSize thumbnailSize; }; }; OverviewWidget::OverviewWidget(QWidget * parent) : QWidget(parent) , m_canvas(0) , m_dragging(false) , m_imageIdleWatcher(250) { setMouseTracking(true); KisConfig cfg(true); slotThemeChanged(); } OverviewWidget::~OverviewWidget() { } void OverviewWidget::setCanvas(KoCanvasBase * canvas) { if (m_canvas) { m_canvas->image()->disconnect(this); } m_canvas = dynamic_cast(canvas); if (m_canvas) { m_imageIdleWatcher.setTrackedImage(m_canvas->image()); connect(&m_imageIdleWatcher, &KisIdleWatcher::startedIdleMode, this, &OverviewWidget::generateThumbnail); connect(m_canvas->image(), SIGNAL(sigImageUpdated(QRect)),SLOT(startUpdateCanvasProjection())); connect(m_canvas->image(), SIGNAL(sigSizeChanged(QPointF,QPointF)),SLOT(startUpdateCanvasProjection())); connect(m_canvas->canvasController()->proxyObject, SIGNAL(canvasOffsetXChanged(int)), this, SLOT(update()), Qt::UniqueConnection); connect(m_canvas->viewManager()->mainWindow(), SIGNAL(themeChanged()), this, SLOT(slotThemeChanged())); generateThumbnail(); } } QSize OverviewWidget::recalculatePreviewSize() { QSize imageSize(m_canvas->image()->bounds().size()); const qreal hScale = 1.0 * this->width() / imageSize.width(); const qreal vScale = 1.0 * this->height() / imageSize.height(); m_previewScale = qMin(hScale, vScale); return imageSize * m_previewScale; } QPointF OverviewWidget::previewOrigin() { const QSize previewSize = recalculatePreviewSize(); return QPointF((width() - previewSize.width()) / 2.0f, (height() - previewSize.height()) / 2.0f); } QPolygonF OverviewWidget::previewPolygon() { if (m_canvas) { const QRectF &canvasRect = QRectF(m_canvas->canvasWidget()->rect()); return canvasToPreviewTransform().map(canvasRect); } return QPolygonF(); } QTransform OverviewWidget::previewToCanvasTransform() { QTransform previewToImage = QTransform::fromTranslate(-this->width() / 2.0, -this->height() / 2.0) * QTransform::fromScale(1.0 / m_previewScale, 1.0 / m_previewScale) * QTransform::fromTranslate(m_canvas->image()->width() / 2.0, m_canvas->image()->height() / 2.0); return previewToImage * m_canvas->coordinatesConverter()->imageToWidgetTransform(); } QTransform OverviewWidget::canvasToPreviewTransform() { return previewToCanvasTransform().inverted(); } void OverviewWidget::startUpdateCanvasProjection() { m_imageIdleWatcher.startCountdown(); } void OverviewWidget::showEvent(QShowEvent *event) { Q_UNUSED(event); m_imageIdleWatcher.startCountdown(); } void OverviewWidget::resizeEvent(QResizeEvent *event) { Q_UNUSED(event); if (m_canvas) { if (!m_oldPixmap.isNull()) { QSize newSize = recalculatePreviewSize(); m_pixmap = m_oldPixmap.scaled(newSize, Qt::KeepAspectRatio, Qt::SmoothTransformation); } m_imageIdleWatcher.startCountdown(); } } void OverviewWidget::mousePressEvent(QMouseEvent* event) { if (m_canvas) { QPointF previewPos = event->pos(); if (!previewPolygon().containsPoint(previewPos, Qt::WindingFill)) { const QRect& canvasRect = m_canvas->canvasWidget()->rect(); const QPointF newCanvasPos = previewToCanvasTransform().map(previewPos) - QPointF(canvasRect.width() / 2.0f, canvasRect.height() / 2.0f); m_canvas->canvasController()->pan(newCanvasPos.toPoint()); } m_lastPos = previewPos; m_dragging = true; } event->accept(); update(); } void OverviewWidget::mouseMoveEvent(QMouseEvent* event) { if (m_dragging) { QPointF previewPos = event->pos(); const QPointF lastCanvasPos = previewToCanvasTransform().map(m_lastPos); const QPointF newCanvasPos = previewToCanvasTransform().map(event->pos()); QPointF diff = newCanvasPos - lastCanvasPos; m_canvas->canvasController()->pan(diff.toPoint()); m_lastPos = previewPos; } event->accept(); } void OverviewWidget::mouseReleaseEvent(QMouseEvent* event) { m_dragging = false; event->accept(); update(); } void OverviewWidget::wheelEvent(QWheelEvent* event) { if (m_canvas) { float delta = event->delta(); if (delta > 0) { m_canvas->viewManager()->zoomController()->zoomAction()->zoomIn(); } else { m_canvas->viewManager()->zoomController()->zoomAction()->zoomOut(); } } } void OverviewWidget::generateThumbnail() { if (isVisible()) { QMutexLocker locker(&mutex); if (m_canvas) { QSize previewSize = recalculatePreviewSize(); if(previewSize.isValid()){ KisImageSP image = m_canvas->image(); if (!strokeId.isNull()) { image->cancelStroke(strokeId); strokeId.clear(); } OverviewThumbnailStrokeStrategy* stroke = new OverviewThumbnailStrokeStrategy(image); connect(stroke, SIGNAL(thumbnailUpdated(QImage)), this, SLOT(updateThumbnail(QImage))); strokeId = image->startStroke(stroke); KisPaintDeviceSP dev = image->projection(); KisPaintDeviceSP thumbDev = new KisPaintDevice(dev->colorSpace()); //creating a special stroke that computes thumbnail image in small chunks that can be quickly interrupted //if user starts painting QList jobs = OverviewThumbnailStrokeStrategy::createJobsData(dev, image->bounds(), thumbDev, previewSize); Q_FOREACH (KisStrokeJobData *jd, jobs) { image->addJob(strokeId, jd); } image->endStroke(strokeId); } } } } void OverviewWidget::updateThumbnail(QImage pixmap) { m_pixmap = QPixmap::fromImage(pixmap); m_oldPixmap = m_pixmap.copy(); update(); } void OverviewWidget::slotThemeChanged() { m_outlineColor = qApp->palette().color(QPalette::Highlight); } void OverviewWidget::paintEvent(QPaintEvent* event) { QWidget::paintEvent(event); if (m_canvas) { QPainter p(this); const QSize previewSize = recalculatePreviewSize(); const QRectF previewRect = QRectF(previewOrigin(), previewSize); p.drawPixmap(previewRect.toRect(), m_pixmap); QRect r = rect(); QPolygonF outline; outline << r.topLeft() << r.topRight() << r.bottomRight() << r.bottomLeft(); QPen pen; pen.setColor(m_outlineColor); pen.setStyle(Qt::DashLine); p.setPen(pen); p.drawPolygon(outline.intersected(previewPolygon())); pen.setStyle(Qt::SolidLine); p.setPen(pen); p.drawPolygon(previewPolygon()); } } OverviewThumbnailStrokeStrategy::OverviewThumbnailStrokeStrategy(KisImageWSP image) - : KisSimpleStrokeStrategy("OverviewThumbnail"), m_image(image) + : KisSimpleStrokeStrategy(QLatin1String("OverviewThumbnail")), + m_image(image) { enableJob(KisSimpleStrokeStrategy::JOB_INIT, true, KisStrokeJobData::BARRIER, KisStrokeJobData::EXCLUSIVE); enableJob(KisSimpleStrokeStrategy::JOB_DOSTROKE); //enableJob(KisSimpleStrokeStrategy::JOB_FINISH); enableJob(KisSimpleStrokeStrategy::JOB_CANCEL, true, KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::EXCLUSIVE); setRequestsOtherStrokesToEnd(false); setClearsRedoOnStart(false); setCanForgetAboutMe(true); } QList OverviewThumbnailStrokeStrategy::createJobsData(KisPaintDeviceSP dev, const QRect& imageRect, KisPaintDeviceSP thumbDev, const QSize& thumbnailSize) { QSize thumbnailOversampledSize = oversample * thumbnailSize; if ((thumbnailOversampledSize.width() > imageRect.width()) || (thumbnailOversampledSize.height() > imageRect.height())) { thumbnailOversampledSize.scale(imageRect.size(), Qt::KeepAspectRatio); } QVector tileRects = KritaUtils::splitRectIntoPatches(QRect(QPoint(0, 0), thumbnailOversampledSize), QSize(thumbnailTileDim, thumbnailTileDim)); QList jobsData; Q_FOREACH (const QRect &tileRectangle, tileRects) { jobsData << new OverviewThumbnailStrokeStrategy::Private::ProcessData(dev, thumbDev, thumbnailOversampledSize, tileRectangle); } jobsData << new OverviewThumbnailStrokeStrategy::Private::FinishProcessing(thumbDev, thumbnailSize); return jobsData; } OverviewThumbnailStrokeStrategy::~OverviewThumbnailStrokeStrategy() { } void OverviewThumbnailStrokeStrategy::initStrokeCallback() { } void OverviewThumbnailStrokeStrategy::doStrokeCallback(KisStrokeJobData *data) { Private::ProcessData *d_pd = dynamic_cast(data); if (d_pd) { //we aren't going to use oversample capability of createThumbnailDevice because it recomputes exact bounds for each small patch, which is //slow. We'll handle scaling separately. KisPaintDeviceSP thumbnailTile = d_pd->dev->createThumbnailDeviceOversampled(d_pd->thumbnailSize.width(), d_pd->thumbnailSize.height(), 1, m_image->bounds(), d_pd->tileRect); { QMutexLocker locker(&m_thumbnailMergeMutex); KisPainter gc(d_pd->thumbDev); gc.bitBlt(QPoint(d_pd->tileRect.x(), d_pd->tileRect.y()), thumbnailTile, d_pd->tileRect); } return; } Private::FinishProcessing *d_fp = dynamic_cast(data); if (d_fp) { QImage overviewImage; KoDummyUpdater updater; KisTransformWorker worker(d_fp->thumbDev, 1 / oversample, 1 / oversample, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, &updater, KisFilterStrategyRegistry::instance()->value("Bilinear")); worker.run(); overviewImage = d_fp->thumbDev->convertToQImage(KoColorSpaceRegistry::instance()->rgb8()->profile(), QRect(QPoint(0,0), d_fp->thumbnailSize)); emit thumbnailUpdated(overviewImage); return; } } void OverviewThumbnailStrokeStrategy::finishStrokeCallback() { } void OverviewThumbnailStrokeStrategy::cancelStrokeCallback() { } diff --git a/plugins/extensions/animationrenderer/AnimationRenderer.cpp b/plugins/extensions/animationrenderer/AnimationRenderer.cpp index 718941a02e..c053c3c537 100644 --- a/plugins/extensions/animationrenderer/AnimationRenderer.cpp +++ b/plugins/extensions/animationrenderer/AnimationRenderer.cpp @@ -1,206 +1,206 @@ /* * Copyright (c) 2016 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 "AnimationRenderer.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "DlgAnimationRenderer.h" #include #include "video_saver.h" #include "KisAnimationRenderingOptions.h" K_PLUGIN_FACTORY_WITH_JSON(AnimaterionRendererFactory, "kritaanimationrenderer.json", registerPlugin();) AnimaterionRenderer::AnimaterionRenderer(QObject *parent, const QVariantList &) : KisActionPlugin(parent) { // Shows the big dialog KisAction *action = createAction("render_animation"); action->setActivationFlags(KisAction::IMAGE_HAS_ANIMATION); connect(action, SIGNAL(triggered()), this, SLOT(slotRenderAnimation())); // Re-renders the image sequence as defined in the last render action = createAction("render_animation_again"); action->setActivationFlags(KisAction::IMAGE_HAS_ANIMATION); connect(action, SIGNAL(triggered()), this, SLOT(slotRenderSequenceAgain())); } AnimaterionRenderer::~AnimaterionRenderer() { } void AnimaterionRenderer::slotRenderAnimation() { KisImageWSP image = viewManager()->image(); if (!image) return; if (!image->animationInterface()->hasAnimation()) return; KisDocument *doc = viewManager()->document(); DlgAnimationRenderer dlgAnimationRenderer(doc, viewManager()->mainWindow()); dlgAnimationRenderer.setCaption(i18n("Render Animation")); if (dlgAnimationRenderer.exec() == QDialog::Accepted) { KisAnimationRenderingOptions encoderOptions = dlgAnimationRenderer.getEncoderOptions(); renderAnimationImpl(doc, encoderOptions); } } void AnimaterionRenderer::slotRenderSequenceAgain() { KisImageWSP image = viewManager()->image(); if (!image) return; if (!image->animationInterface()->hasAnimation()) return; KisDocument *doc = viewManager()->document(); KisConfig cfg(true); KisPropertiesConfigurationSP settings = cfg.exportConfiguration("ANIMATION_EXPORT"); KisAnimationRenderingOptions encoderOptions; encoderOptions.fromProperties(settings); renderAnimationImpl(doc, encoderOptions); } void AnimaterionRenderer::renderAnimationImpl(KisDocument *doc, KisAnimationRenderingOptions encoderOptions) { const QString frameMimeType = encoderOptions.frameMimeType; const QString framesDirectory = encoderOptions.resolveAbsoluteFramesDirectory(); const QString extension = KisMimeDatabase::suffixesForMimeType(frameMimeType).first(); const QString baseFileName = QString("%1/%2.%3").arg(framesDirectory) .arg(encoderOptions.basename) .arg(extension); /** * The dialog should ensure that the size of the video is even */ KIS_SAFE_ASSERT_RECOVER( !((encoderOptions.width & 0x1 || encoderOptions.height & 0x1) && (encoderOptions.videoMimeType == "video/mp4" || encoderOptions.videoMimeType == "video/x-matroska"))) { encoderOptions.width = encoderOptions.width + (encoderOptions.width & 0x1); encoderOptions.height = encoderOptions.height + (encoderOptions.height & 0x1); } const QSize scaledSize = doc->image()->bounds().size().scaled( encoderOptions.width, encoderOptions.height, Qt::KeepAspectRatio); if ((scaledSize.width() & 0x1 || scaledSize.height() & 0x1) && (encoderOptions.videoMimeType == "video/mp4" || encoderOptions.videoMimeType == "video/x-matroska")) { QString m = "Mastroska (.mkv)"; if (encoderOptions.videoMimeType == "video/mp4") { m = "Mpeg4 (.mp4)"; } qWarning() << m <<"requires width and height to be even, resize and try again!"; doc->setErrorMessage(i18n("%1 requires width and height to be even numbers. Please resize or crop the image before exporting.", m)); QMessageBox::critical(0, i18nc("@title:window", "Krita"), i18n("Could not render animation:\n%1", doc->errorMessage())); return; } const bool batchMode = false; // TODO: fetch correctly! KisAsyncAnimationFramesSaveDialog exporter(doc->image(), KisTimeRange::fromTime(encoderOptions.firstFrame, encoderOptions.lastFrame), baseFileName, encoderOptions.sequenceStart, encoderOptions.frameExportConfig); exporter.setBatchMode(batchMode); KisAsyncAnimationFramesSaveDialog::Result result = exporter.regenerateRange(viewManager()->mainWindow()->viewManager()); // the folder could have been read-only or something else could happen if (encoderOptions.shouldEncodeVideo && result == KisAsyncAnimationFramesSaveDialog::RenderComplete) { const QString savedFilesMask = exporter.savedFilesMask(); const QString resultFile = encoderOptions.resolveAbsoluteVideoFilePath(); - KIS_SAFE_ASSERT_RECOVER_NOOP(QFileInfo(resultFile).isAbsolute()) + KIS_SAFE_ASSERT_RECOVER_NOOP(QFileInfo(resultFile).isAbsolute()); { const QFileInfo info(resultFile); QDir dir(info.absolutePath()); if (!dir.exists()) { dir.mkpath(info.absolutePath()); } KIS_SAFE_ASSERT_RECOVER_NOOP(dir.exists()); } KisImportExportErrorCode res; QFile fi(resultFile); if (!fi.open(QIODevice::WriteOnly)) { qWarning() << "Could not open" << fi.fileName() << "for writing!"; res = KisImportExportErrorCannotWrite(fi.error()); } else { fi.close(); } QScopedPointer encoder(new VideoSaver(doc, batchMode)); res = encoder->convert(doc, savedFilesMask, encoderOptions, batchMode); if (!res.isOk()) { QMessageBox::critical(0, i18nc("@title:window", "Krita"), i18n("Could not render animation:\n%1", res.errorMessage())); } if (encoderOptions.shouldDeleteSequence) { QDir d(framesDirectory); QStringList sequenceFiles = d.entryList(QStringList() << encoderOptions.basename + "*." + extension, QDir::Files); Q_FOREACH(const QString &f, sequenceFiles) { d.remove(f); } } } else if (result == KisAsyncAnimationFramesSaveDialog::RenderFailed) { viewManager()->mainWindow()->viewManager()->showFloatingMessage(i18n("Failed to render animation frames!"), QIcon()); } } #include "AnimationRenderer.moc" diff --git a/plugins/impex/libkra/kis_kra_savexml_visitor.cpp b/plugins/impex/libkra/kis_kra_savexml_visitor.cpp index 35343d3adf..0620f90504 100644 --- a/plugins/impex/libkra/kis_kra_savexml_visitor.cpp +++ b/plugins/impex/libkra/kis_kra_savexml_visitor.cpp @@ -1,500 +1,500 @@ /* * Copyright (c) 2002 Patrick Julien * Copyright (c) 2005 C. Boemann * * 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_kra_savexml_visitor.h" #include "kis_kra_tags.h" #include "kis_kra_utils.h" #include "kis_layer_properties_icons.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kis_keyframe_channel.h" #include "kis_dom_utils.h" using namespace KRA; KisSaveXmlVisitor::KisSaveXmlVisitor(QDomDocument doc, const QDomElement & element, quint32 &count, const QString &url, bool root) : KisNodeVisitor() , m_doc(doc) , m_count(count) , m_url(url) , m_root(root) { Q_ASSERT(!element.isNull()); m_elem = element; } void KisSaveXmlVisitor::setSelectedNodes(vKisNodeSP selectedNodes) { m_selectedNodes = selectedNodes; } QStringList KisSaveXmlVisitor::errorMessages() const { return m_errorMessages; } bool KisSaveXmlVisitor::visit(KisExternalLayer * layer) { if (layer->inherits("KisReferenceImagesLayer")) { return saveReferenceImagesLayer(layer); } else if (layer->inherits("KisShapeLayer")) { QDomElement layerElement = m_doc.createElement(LAYER); saveLayer(layerElement, SHAPE_LAYER, layer); m_elem.appendChild(layerElement); m_count++; return saveMasks(layer, layerElement); } else if (layer->inherits("KisFileLayer")) { QDomElement layerElement = m_doc.createElement(LAYER); saveLayer(layerElement, FILE_LAYER, layer); KisFileLayer *fileLayer = dynamic_cast(layer); QString path = fileLayer->path(); QDir d(QFileInfo(m_url).absolutePath()); layerElement.setAttribute("source", d.relativeFilePath(path)); if (fileLayer->scalingMethod() == KisFileLayer::ToImagePPI) { layerElement.setAttribute("scale", "true"); } else { layerElement.setAttribute("scale", "false"); } layerElement.setAttribute("scalingmethod", (int)fileLayer->scalingMethod()); layerElement.setAttribute(COLORSPACE_NAME, layer->original()->colorSpace()->id()); m_elem.appendChild(layerElement); m_count++; return saveMasks(layer, layerElement); } return false; } QDomElement KisSaveXmlVisitor::savePaintLayerAttributes(KisPaintLayer *layer, QDomDocument &doc) { QDomElement element = doc.createElement(LAYER); saveLayer(element, PAINT_LAYER, layer); element.setAttribute(CHANNEL_LOCK_FLAGS, flagsToString(layer->channelLockFlags())); element.setAttribute(COLORSPACE_NAME, layer->paintDevice()->colorSpace()->id()); element.setAttribute(ONION_SKIN_ENABLED, layer->onionSkinEnabled()); element.setAttribute(VISIBLE_IN_TIMELINE, layer->useInTimeline()); return element; } void KisSaveXmlVisitor::loadPaintLayerAttributes(const QDomElement &el, KisPaintLayer *layer) { loadLayerAttributes(el, layer); if (el.hasAttribute(CHANNEL_LOCK_FLAGS)) { layer->setChannelLockFlags(stringToFlags(el.attribute(CHANNEL_LOCK_FLAGS))); } } bool KisSaveXmlVisitor::visit(KisPaintLayer *layer) { QDomElement layerElement = savePaintLayerAttributes(layer, m_doc); m_elem.appendChild(layerElement); m_count++; return saveMasks(layer, layerElement); } bool KisSaveXmlVisitor::visit(KisGroupLayer *layer) { QDomElement layerElement; if (m_root) // if this is the root we fake so not to save it layerElement = m_elem; else { layerElement = m_doc.createElement(LAYER); saveLayer(layerElement, GROUP_LAYER, layer); layerElement.setAttribute(PASS_THROUGH_MODE, layer->passThroughMode()); m_elem.appendChild(layerElement); } QDomElement elem = m_doc.createElement(LAYERS); Q_ASSERT(!layerElement.isNull()); layerElement.appendChild(elem); KisSaveXmlVisitor visitor(m_doc, elem, m_count, m_url, false); visitor.setSelectedNodes(m_selectedNodes); m_count++; bool success = visitor.visitAllInverse(layer); m_errorMessages.append(visitor.errorMessages()); if (!m_errorMessages.isEmpty()) { return false; } QMapIterator i(visitor.nodeFileNames()); while (i.hasNext()) { i.next(); m_nodeFileNames[i.key()] = i.value(); } i = QMapIterator(visitor.keyframeFileNames()); while (i.hasNext()) { i.next(); m_keyframeFileNames[i.key()] = i.value(); } return success; } bool KisSaveXmlVisitor::visit(KisAdjustmentLayer* layer) { if (!layer->filter()) { return false; } QDomElement layerElement = m_doc.createElement(LAYER); saveLayer(layerElement, ADJUSTMENT_LAYER, layer); layerElement.setAttribute(FILTER_NAME, layer->filter()->name()); layerElement.setAttribute(FILTER_VERSION, layer->filter()->version()); m_elem.appendChild(layerElement); m_count++; return saveMasks(layer, layerElement); } bool KisSaveXmlVisitor::visit(KisGeneratorLayer *layer) { QDomElement layerElement = m_doc.createElement(LAYER); saveLayer(layerElement, GENERATOR_LAYER, layer); layerElement.setAttribute(GENERATOR_NAME, layer->filter()->name()); layerElement.setAttribute(GENERATOR_VERSION, layer->filter()->version()); m_elem.appendChild(layerElement); m_count++; return saveMasks(layer, layerElement); } bool KisSaveXmlVisitor::visit(KisCloneLayer *layer) { QDomElement layerElement = m_doc.createElement(LAYER); saveLayer(layerElement, CLONE_LAYER, layer); layerElement.setAttribute(CLONE_FROM, layer->copyFromInfo().name()); layerElement.setAttribute(CLONE_FROM_UUID, layer->copyFromInfo().uuid().toString()); layerElement.setAttribute(CLONE_TYPE, layer->copyType()); m_elem.appendChild(layerElement); m_count++; return saveMasks(layer, layerElement); } bool KisSaveXmlVisitor::visit(KisFilterMask *mask) { Q_ASSERT(mask); if (!mask->filter()) { return false; } QDomElement el = m_doc.createElement(MASK); saveMask(el, FILTER_MASK, mask); el.setAttribute(FILTER_NAME, mask->filter()->name()); el.setAttribute(FILTER_VERSION, mask->filter()->version()); m_elem.appendChild(el); m_count++; return true; } bool KisSaveXmlVisitor::visit(KisTransformMask *mask) { Q_ASSERT(mask); QDomElement el = m_doc.createElement(MASK); saveMask(el, TRANSFORM_MASK, mask); m_elem.appendChild(el); m_count++; return true; } bool KisSaveXmlVisitor::visit(KisTransparencyMask *mask) { Q_ASSERT(mask); QDomElement el = m_doc.createElement(MASK); saveMask(el, TRANSPARENCY_MASK, mask); m_elem.appendChild(el); m_count++; return true; } bool KisSaveXmlVisitor::visit(KisColorizeMask *mask) { Q_ASSERT(mask); QDomElement el = m_doc.createElement(MASK); saveMask(el, COLORIZE_MASK, mask); m_elem.appendChild(el); m_count++; return true; } bool KisSaveXmlVisitor::visit(KisSelectionMask *mask) { Q_ASSERT(mask); QDomElement el = m_doc.createElement(MASK); saveMask(el, SELECTION_MASK, mask); m_elem.appendChild(el); m_count++; return true; } void KisSaveXmlVisitor::loadLayerAttributes(const QDomElement &el, KisLayer *layer) { if (el.hasAttribute(NAME)) { QString layerName = el.attribute(NAME); if (layerName != layer->name()) { // Make the EXR layername leading in case of conflicts layer->setName(layerName); } } if (el.hasAttribute(CHANNEL_FLAGS)) { layer->setChannelFlags(stringToFlags(el.attribute(CHANNEL_FLAGS))); } if (el.hasAttribute(OPACITY)) { layer->setOpacity(el.attribute(OPACITY).toInt()); } if (el.hasAttribute(COMPOSITE_OP)) { layer->setCompositeOpId(el.attribute(COMPOSITE_OP)); } if (el.hasAttribute(VISIBLE)) { layer->setVisible(el.attribute(VISIBLE).toInt()); } if (el.hasAttribute(LOCKED)) { layer->setUserLocked(el.attribute(LOCKED).toInt()); } if (el.hasAttribute(X)) { layer->setX(el.attribute(X).toInt()); } if (el.hasAttribute(Y)) { layer->setY(el.attribute(Y).toInt()); } if (el.hasAttribute(UUID)) { layer->setUuid(el.attribute(UUID)); } if (el.hasAttribute(COLLAPSED)) { layer->setCollapsed(el.attribute(COLLAPSED).toInt()); } if (el.hasAttribute(COLOR_LABEL)) { layer->setColorLabelIndex(el.attribute(COLOR_LABEL).toInt()); } if (el.hasAttribute(VISIBLE_IN_TIMELINE)) { layer->setUseInTimeline(el.attribute(VISIBLE_IN_TIMELINE).toInt()); } if (el.hasAttribute(LAYER_STYLE_UUID)) { QString uuidString = el.attribute(LAYER_STYLE_UUID); QUuid uuid(uuidString); if (!uuid.isNull()) { KisPSDLayerStyleSP dumbLayerStyle(new KisPSDLayerStyle()); dumbLayerStyle->setUuid(uuid); layer->setLayerStyle(dumbLayerStyle); } else { warnKrita << "WARNING: Layer style for layer" << layer->name() << "contains invalid UUID" << uuidString; } } } void KisSaveXmlVisitor::saveNodeKeyframes(const KisNode* node, QString nodeFilename, QDomElement& nodeElement) { if (node->isAnimated()) { QString keyframeFile = nodeFilename + ".keyframes.xml"; m_keyframeFileNames[node] = keyframeFile; nodeElement.setAttribute(KEYFRAME_FILE, keyframeFile); } } void KisSaveXmlVisitor::saveLayer(QDomElement & el, const QString & layerType, const KisLayer * layer) { QString filename = LAYER + QString::number(m_count); el.setAttribute(CHANNEL_FLAGS, flagsToString(layer->channelFlags())); el.setAttribute(NAME, layer->name()); el.setAttribute(OPACITY, layer->opacity()); el.setAttribute(COMPOSITE_OP, layer->compositeOp()->id()); el.setAttribute(VISIBLE, layer->visible()); el.setAttribute(LOCKED, layer->userLocked()); el.setAttribute(NODE_TYPE, layerType); el.setAttribute(FILE_NAME, filename); el.setAttribute(X, layer->x()); el.setAttribute(Y, layer->y()); el.setAttribute(UUID, layer->uuid().toString()); el.setAttribute(COLLAPSED, layer->collapsed()); el.setAttribute(COLOR_LABEL, layer->colorLabelIndex()); el.setAttribute(VISIBLE_IN_TIMELINE, layer->useInTimeline()); if (layer->layerStyle()) { el.setAttribute(LAYER_STYLE_UUID, layer->layerStyle()->uuid().toString()); } Q_FOREACH (KisNodeSP node, m_selectedNodes) { if (node.data() == layer) { el.setAttribute("selected", "true"); break; } } saveNodeKeyframes(layer, filename, el); m_nodeFileNames[layer] = filename; dbgFile << "Saved layer " << layer->name() << " of type " << layerType << " with filename " << LAYER + QString::number(m_count); } void KisSaveXmlVisitor::saveMask(QDomElement & el, const QString & maskType, const KisMaskSP mask) { QString filename = MASK + QString::number(m_count); el.setAttribute(NAME, mask->name()); el.setAttribute(VISIBLE, mask->visible()); el.setAttribute(LOCKED, mask->userLocked()); el.setAttribute(NODE_TYPE, maskType); el.setAttribute(FILE_NAME, filename); el.setAttribute(X, mask->x()); el.setAttribute(Y, mask->y()); el.setAttribute(UUID, mask->uuid().toString()); if (maskType == SELECTION_MASK) { el.setAttribute(ACTIVE, mask->nodeProperties().boolProperty("active")); } else if (maskType == COLORIZE_MASK) { el.setAttribute(COLORSPACE_NAME, mask->colorSpace()->id()); el.setAttribute(COMPOSITE_OP, mask->compositeOpId()); el.setAttribute(COLORIZE_EDIT_KEYSTROKES, KisLayerPropertiesIcons::nodeProperty(mask, KisLayerPropertiesIcons::colorizeEditKeyStrokes, true).toBool()); el.setAttribute(COLORIZE_SHOW_COLORING, KisLayerPropertiesIcons::nodeProperty(mask, KisLayerPropertiesIcons::colorizeShowColoring, true).toBool()); const KisColorizeMask *colorizeMask = dynamic_cast(mask.data()); KIS_SAFE_ASSERT_RECOVER_NOOP(colorizeMask); if (colorizeMask) { el.setAttribute(COLORIZE_USE_EDGE_DETECTION, colorizeMask->useEdgeDetection()); el.setAttribute(COLORIZE_EDGE_DETECTION_SIZE, KisDomUtils::toString(colorizeMask->edgeDetectionSize())); el.setAttribute(COLORIZE_FUZZY_RADIUS, KisDomUtils::toString(colorizeMask->fuzzyRadius())); el.setAttribute(COLORIZE_CLEANUP, int(100 * colorizeMask->cleanUpAmount())); el.setAttribute(COLORIZE_LIMIT_TO_DEVICE, colorizeMask->limitToDeviceBounds()); } } saveNodeKeyframes(mask, filename, el); m_nodeFileNames[mask] = filename; dbgFile << "Saved mask " << mask->name() << " of type " << maskType << " with filename " << filename; } bool KisSaveXmlVisitor::saveMasks(KisNode * node, QDomElement & layerElement) { if (node->childCount() > 0) { QDomElement elem = m_doc.createElement(MASKS); Q_ASSERT(!layerElement.isNull()); layerElement.appendChild(elem); KisSaveXmlVisitor visitor(m_doc, elem, m_count, m_url, false); visitor.setSelectedNodes(m_selectedNodes); bool success = visitor.visitAllInverse(node); m_errorMessages.append(visitor.errorMessages()); if (!m_errorMessages.isEmpty()) { return false; } QMapIterator i(visitor.nodeFileNames()); while (i.hasNext()) { i.next(); m_nodeFileNames[i.key()] = i.value(); } i = QMapIterator(visitor.keyframeFileNames()); while (i.hasNext()) { i.next(); m_keyframeFileNames[i.key()] = i.value(); } return success; } return true; } bool KisSaveXmlVisitor::saveReferenceImagesLayer(KisExternalLayer *layer) { auto *referencesLayer = dynamic_cast(layer); - KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(referencesLayer, false) + KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(referencesLayer, false); QDomElement layerElement = m_doc.createElement(LAYER); layerElement.setAttribute(NODE_TYPE, REFERENCE_IMAGES_LAYER); int nextId = 0; Q_FOREACH(KoShape *shape, referencesLayer->shapes()) { auto *reference = dynamic_cast(shape); KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(reference, false); reference->saveXml(m_doc, layerElement, nextId); nextId++; } m_elem.appendChild(layerElement); m_count++; return true; } diff --git a/plugins/paintops/defaultpaintops/brush/KisDabRenderingJob.cpp b/plugins/paintops/defaultpaintops/brush/KisDabRenderingJob.cpp index 586dbdee2f..37d77da314 100644 --- a/plugins/paintops/defaultpaintops/brush/KisDabRenderingJob.cpp +++ b/plugins/paintops/defaultpaintops/brush/KisDabRenderingJob.cpp @@ -1,169 +1,176 @@ /* * Copyright (c) 2017 Dmitry Kazakov * * 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 "KisDabRenderingJob.h" #include #include #include #include "KisDabCacheUtils.h" #include "KisDabRenderingQueue.h" #include KisDabRenderingJob::KisDabRenderingJob() { } KisDabRenderingJob::KisDabRenderingJob(int _seqNo, KisDabCacheUtils::DabGenerationInfo _generationInfo, KisDabRenderingJob::JobType _type) : seqNo(_seqNo), generationInfo(_generationInfo), type(_type) { } KisDabRenderingJob::KisDabRenderingJob(const KisDabRenderingJob &rhs) : seqNo(rhs.seqNo), generationInfo(rhs.generationInfo), type(rhs.type), originalDevice(rhs.originalDevice), postprocessedDevice(rhs.postprocessedDevice), status(rhs.status), opacity(rhs.opacity), flow(rhs.flow) { } KisDabRenderingJob &KisDabRenderingJob::operator=(const KisDabRenderingJob &rhs) { seqNo = rhs.seqNo; generationInfo = rhs.generationInfo; type = rhs.type; originalDevice = rhs.originalDevice; postprocessedDevice = rhs.postprocessedDevice; status = rhs.status; opacity = rhs.opacity; flow = rhs.flow; return *this; } QPoint KisDabRenderingJob::dstDabOffset() const { - return generationInfo.dstDabRect.topLeft(); + /// Recenter generated low-res dab around the center + /// of the idel theoretical dab rect + const QPoint p1 = generationInfo.dstDabRect.topLeft(); + const QPoint s1 = QPoint(generationInfo.dstDabRect.width(), + generationInfo.dstDabRect.height()); + const QPoint s2 = QPoint(postprocessedDevice->bounds().width(), + postprocessedDevice->bounds().height()); + return p1 + (s1 - s2) / 2; } KisDabRenderingJobRunner::KisDabRenderingJobRunner(KisDabRenderingJobSP job, KisDabRenderingQueue *parentQueue, KisRunnableStrokeJobsInterface *runnableJobsInterface) : m_job(job), m_parentQueue(parentQueue), m_runnableJobsInterface(runnableJobsInterface) { } KisDabRenderingJobRunner::~KisDabRenderingJobRunner() { } int KisDabRenderingJobRunner::executeOneJob(KisDabRenderingJob *job, KisDabCacheUtils::DabRenderingResources *resources, KisDabRenderingQueue *parentQueue) { using namespace KisDabCacheUtils; KIS_SAFE_ASSERT_RECOVER_NOOP(job->type == KisDabRenderingJob::Dab || job->type == KisDabRenderingJob::Postprocess); QElapsedTimer executionTime; executionTime.start(); resources->syncResourcesToSeqNo(job->seqNo, job->generationInfo.info); if (job->type == KisDabRenderingJob::Dab) { // TODO: thing about better interface for the reverse queue link job->originalDevice = parentQueue->fetchCachedPaintDevce(); generateDab(job->generationInfo, resources, &job->originalDevice); } // by now the original device should be already prepared KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(job->originalDevice, 0); if (job->type == KisDabRenderingJob::Dab || job->type == KisDabRenderingJob::Postprocess) { if (job->generationInfo.needsPostprocessing) { // TODO: cache postprocessed device if (!job->postprocessedDevice || *job->originalDevice->colorSpace() != *job->postprocessedDevice->colorSpace()) { job->postprocessedDevice = parentQueue->fetchCachedPaintDevce(); *job->postprocessedDevice = *job->originalDevice; } else { *job->postprocessedDevice = *job->originalDevice; } postProcessDab(job->postprocessedDevice, job->generationInfo.dstDabRect.topLeft(), job->generationInfo.info, resources); } else { job->postprocessedDevice = job->originalDevice; } } return executionTime.nsecsElapsed() / 1000; } void KisDabRenderingJobRunner::run() { int executionTime = 0; KisDabCacheUtils::DabRenderingResources *resources = m_parentQueue->fetchResourcesFromCache(); executionTime = executeOneJob(m_job.data(), resources, m_parentQueue); QList jobs = m_parentQueue->notifyJobFinished(m_job->seqNo, executionTime); while (!jobs.isEmpty()) { QVector dataList; // start all-but-the-first jobs asynchronously for (int i = 1; i < jobs.size(); i++) { dataList.append(new FreehandStrokeRunnableJobDataWithUpdate( new KisDabRenderingJobRunner(jobs[i], m_parentQueue, m_runnableJobsInterface), KisStrokeJobData::CONCURRENT)); } m_runnableJobsInterface->addRunnableJobs(dataList); // execute the first job in the current thread KisDabRenderingJobSP job = jobs.first(); executionTime = executeOneJob(job.data(), resources, m_parentQueue); jobs = m_parentQueue->notifyJobFinished(job->seqNo, executionTime); } m_parentQueue->putResourcesToCache(resources); } diff --git a/plugins/tools/basictools/KisMoveBoundsCalculationJob.cpp b/plugins/tools/basictools/KisMoveBoundsCalculationJob.cpp index 36eab97e30..ca6ac8943f 100644 --- a/plugins/tools/basictools/KisMoveBoundsCalculationJob.cpp +++ b/plugins/tools/basictools/KisMoveBoundsCalculationJob.cpp @@ -1,60 +1,68 @@ /* * Copyright (c) 2019 Dmitry Kazakov * * 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 "KisMoveBoundsCalculationJob.h" #include "kis_node.h" #include "kis_selection.h" #include "kis_layer_utils.h" KisMoveBoundsCalculationJob::KisMoveBoundsCalculationJob(KisNodeList nodes, KisSelectionSP selection, QObject *requestedBy) : m_nodes(nodes), m_selection(selection), m_requestedBy(requestedBy) { setExclusive(true); } void KisMoveBoundsCalculationJob::run() { QRect handlesRect; Q_FOREACH (KisNodeSP node, m_nodes) { handlesRect |= KisLayerUtils::recursiveNodeExactBounds(node); } if (m_selection) { handlesRect &= m_selection->selectedExactRect(); } emit sigCalcualtionFinished(handlesRect); } bool KisMoveBoundsCalculationJob::overrides(const KisSpontaneousJob *_otherJob) { const KisMoveBoundsCalculationJob *otherJob = dynamic_cast(_otherJob); return otherJob && otherJob->m_requestedBy == m_requestedBy; } int KisMoveBoundsCalculationJob::levelOfDetail() const { return 0; } + +QString KisMoveBoundsCalculationJob::debugName() const +{ + QString result; + QDebug dbg(&result); + dbg << "KisMoveBoundsCalculationJob" << ppVar(m_requestedBy) << m_nodes; + return result; +} diff --git a/plugins/tools/basictools/KisMoveBoundsCalculationJob.h b/plugins/tools/basictools/KisMoveBoundsCalculationJob.h index 0c1f2037c3..a408512fc6 100644 --- a/plugins/tools/basictools/KisMoveBoundsCalculationJob.h +++ b/plugins/tools/basictools/KisMoveBoundsCalculationJob.h @@ -1,46 +1,48 @@ /* * Copyright (c) 2019 Dmitry Kazakov * * 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. */ #ifndef KISMOVEBOUNDSCALCULATIONJOB_H #define KISMOVEBOUNDSCALCULATIONJOB_H #include #include "kis_spontaneous_job.h" #include "kis_types.h" #include "kis_selection.h" class KisMoveBoundsCalculationJob : public QObject, public KisSpontaneousJob { Q_OBJECT public: KisMoveBoundsCalculationJob(KisNodeList nodes, KisSelectionSP selection, QObject *requestedBy); void run() override; bool overrides(const KisSpontaneousJob *otherJob) override; int levelOfDetail() const override; + QString debugName() const override; + Q_SIGNALS: void sigCalcualtionFinished(const QRect &bounds); private: KisNodeList m_nodes; KisSelectionSP m_selection; QObject *m_requestedBy; }; #endif // KISMOVEBOUNDSCALCULATIONJOB_H diff --git a/plugins/tools/basictools/kis_tool_move.cc b/plugins/tools/basictools/kis_tool_move.cc index a905497b3e..1b1af76d28 100644 --- a/plugins/tools/basictools/kis_tool_move.cc +++ b/plugins/tools/basictools/kis_tool_move.cc @@ -1,714 +1,715 @@ /* * Copyright (c) 1999 Matthias Elter * 1999 Michael Koch * 2002 Patrick Julien * 2004 Boudewijn Rempt * 2016 Michael Abrahams * * 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_tool_move.h" #include #include "kis_cursor.h" #include "kis_selection.h" #include "kis_canvas2.h" #include "kis_image.h" #include "kis_tool_utils.h" #include "kis_paint_layer.h" #include "strokes/move_stroke_strategy.h" #include "kis_tool_movetooloptionswidget.h" #include "strokes/move_selection_stroke_strategy.h" #include "kis_resources_snapshot.h" #include "kis_action_registry.h" #include "krita_utils.h" #include #include #include "kis_node_manager.h" #include "kis_selection_manager.h" #include "kis_signals_blocker.h" #include #include "KisMoveBoundsCalculationJob.h" struct KisToolMoveState : KisToolChangesTrackerData, boost::equality_comparable { KisToolMoveState(QPoint _accumulatedOffset) : accumulatedOffset(_accumulatedOffset) {} KisToolChangesTrackerData* clone() const override { return new KisToolMoveState(*this); } bool operator ==(const KisToolMoveState &rhs) { return accumulatedOffset == rhs.accumulatedOffset; } QPoint accumulatedOffset; }; KisToolMove::KisToolMove(KoCanvasBase *canvas) : KisTool(canvas, KisCursor::moveCursor()) , m_updateCursorCompressor(100, KisSignalCompressor::FIRST_ACTIVE) { setObjectName("tool_move"); m_showCoordinatesAction = action("movetool-show-coordinates"); m_showCoordinatesAction = action("movetool-show-coordinates"); connect(&m_updateCursorCompressor, SIGNAL(timeout()), this, SLOT(resetCursorStyle())); m_optionsWidget = new MoveToolOptionsWidget(0, currentImage()->xRes(), toolId()); // See https://bugs.kde.org/show_bug.cgi?id=316896 QWidget *specialSpacer = new QWidget(m_optionsWidget); specialSpacer->setObjectName("SpecialSpacer"); specialSpacer->setFixedSize(0, 0); m_optionsWidget->layout()->addWidget(specialSpacer); m_optionsWidget->setFixedHeight(m_optionsWidget->sizeHint().height()); m_showCoordinatesAction->setChecked(m_optionsWidget->showCoordinates()); m_optionsWidget->slotSetTranslate(m_handlesRect.topLeft() + currentOffset()); connect(m_optionsWidget, SIGNAL(sigSetTranslateX(int)), SLOT(moveBySpinX(int)), Qt::UniqueConnection); connect(m_optionsWidget, SIGNAL(sigSetTranslateY(int)), SLOT(moveBySpinY(int)), Qt::UniqueConnection); connect(m_optionsWidget, SIGNAL(sigRequestCommitOffsetChanges()), this, SLOT(commitChanges()), Qt::UniqueConnection); connect(this, SIGNAL(moveInNewPosition(QPoint)), m_optionsWidget, SLOT(slotSetTranslate(QPoint)), Qt::UniqueConnection); - - connect(qobject_cast(canvas)->viewManager()->nodeManager(), SIGNAL(sigUiNeedChangeSelectedNodes(KisNodeList)), this, SLOT(slotNodeChanged(KisNodeList)), Qt::UniqueConnection); - connect(qobject_cast(canvas)->viewManager()->selectionManager(), SIGNAL(currentSelectionChanged()), this, SLOT(slotSelectionChanged()), Qt::UniqueConnection); } KisToolMove::~KisToolMove() { endStroke(); } void KisToolMove::resetCursorStyle() { KisTool::resetCursorStyle(); if (!isActive()) return; KisImageSP image = this->image(); KisResourcesSnapshotSP resources = new KisResourcesSnapshot(image, currentNode(), canvas()->resourceManager()); KisSelectionSP selection = resources->activeSelection(); KisNodeList nodes = fetchSelectedNodes(moveToolMode(), &m_lastCursorPos, selection); if (nodes.isEmpty()) { canvas()->setCursor(Qt::ForbiddenCursor); } } KisNodeList KisToolMove::fetchSelectedNodes(MoveToolMode mode, const QPoint *pixelPoint, KisSelectionSP selection) { KisNodeList nodes; KisImageSP image = this->image(); if (mode != MoveSelectedLayer && pixelPoint) { const bool wholeGroup = !selection && mode == MoveGroup; KisNodeSP node = KisToolUtils::findNode(image->root(), *pixelPoint, wholeGroup); if (node) { nodes = {node}; } } if (nodes.isEmpty()) { nodes = this->selectedNodes(); KritaUtils::filterContainer(nodes, [](KisNodeSP node) { return node->isEditable(); }); } return nodes; } bool KisToolMove::startStrokeImpl(MoveToolMode mode, const QPoint *pos) { KisNodeSP node; KisImageSP image = this->image(); KisResourcesSnapshotSP resources = new KisResourcesSnapshot(image, currentNode(), canvas()->resourceManager()); KisSelectionSP selection = resources->activeSelection(); KisNodeList nodes = fetchSelectedNodes(mode, pos, selection); if (nodes.size() == 1) { node = nodes.first(); } if (nodes.isEmpty()) { return false; } /** * If the target node has changed, the stroke should be * restarted. Otherwise just continue processing current node. */ if (m_strokeId && !tryEndPreviousStroke(nodes)) { return true; } KisStrokeStrategy *strategy; KisPaintLayerSP paintLayer = node ? dynamic_cast(node.data()) : 0; bool isMoveSelection = false; if (paintLayer && selection && (!selection->selectedRect().isEmpty() && !selection->selectedExactRect().isEmpty())) { MoveSelectionStrokeStrategy *moveStrategy = new MoveSelectionStrokeStrategy(paintLayer, selection, image.data(), image.data()); connect(moveStrategy, SIGNAL(sigHandlesRectCalculated(const QRect&)), SLOT(slotHandlesRectCalculated(const QRect&))); strategy = moveStrategy; isMoveSelection = true; } else { MoveStrokeStrategy *moveStrategy = new MoveStrokeStrategy(nodes, image.data(), image.data()); connect(moveStrategy, SIGNAL(sigHandlesRectCalculated(const QRect&)), SLOT(slotHandlesRectCalculated(const QRect&))); strategy = moveStrategy; } // disable outline feedback until the stroke calcualtes // correct bounding rect m_handlesRect = QRect(); m_strokeId = image->startStroke(strategy); m_currentlyProcessingNodes = nodes; m_accumulatedOffset = QPoint(); if (!isMoveSelection) { m_asyncUpdateHelper.startUpdateStream(image.data(), m_strokeId); } KIS_SAFE_ASSERT_RECOVER(m_changesTracker.isEmpty()) { m_changesTracker.reset(); } commitChanges(); return true; } QPoint KisToolMove::currentOffset() const { return m_accumulatedOffset + m_dragPos - m_dragStart; } void KisToolMove::notifyGuiAfterMove(bool showFloatingMessage) { if (!m_optionsWidget) return; if (m_handlesRect.isEmpty()) return; const QPoint currentTopLeft = m_handlesRect.topLeft() + currentOffset(); KisSignalsBlocker b(m_optionsWidget); emit moveInNewPosition(currentTopLeft); // TODO: fetch this info not from options widget, but from config const bool showCoordinates = m_optionsWidget->showCoordinates(); if (showCoordinates && showFloatingMessage) { KisCanvas2 *kisCanvas = static_cast(canvas()); kisCanvas->viewManager()-> showFloatingMessage( i18nc("floating message in move tool", "X: %1 px, Y: %2 px", QLocale().toString(currentTopLeft.x()), QLocale().toString(currentTopLeft.y())), QIcon(), 1000, KisFloatingMessage::High); } } bool KisToolMove::tryEndPreviousStroke(const KisNodeList &nodes) { if (!m_strokeId) return false; bool strokeEnded = false; if (!KritaUtils::compareListsUnordered(nodes, m_currentlyProcessingNodes)) { endStroke(); strokeEnded = true; } return strokeEnded; } void KisToolMove::commitChanges() { KIS_SAFE_ASSERT_RECOVER_RETURN(m_strokeId); QSharedPointer newState(new KisToolMoveState(m_accumulatedOffset)); KisToolMoveState *lastState = dynamic_cast(m_changesTracker.lastState().data()); if (lastState && *lastState == *newState) return; m_changesTracker.commitConfig(newState); } void KisToolMove::slotHandlesRectCalculated(const QRect &handlesRect) { m_handlesRect = handlesRect; notifyGuiAfterMove(false); } void KisToolMove::moveDiscrete(MoveDirection direction, bool big) { if (mode() == KisTool::PAINT_MODE) return; // Don't interact with dragging if (!currentNode()) return; if (!image()) return; if (!currentNode()->isEditable()) return; // Don't move invisible nodes if (startStrokeImpl(MoveSelectedLayer, 0)) { setMode(KisTool::PAINT_MODE); } // Larger movement if "shift" key is pressed. qreal scale = big ? m_optionsWidget->moveScale() : 1.0; qreal moveStep = m_optionsWidget->moveStep() * scale; const QPoint offset = direction == Up ? QPoint( 0, -moveStep) : direction == Down ? QPoint( 0, moveStep) : direction == Left ? QPoint(-moveStep, 0) : QPoint( moveStep, 0) ; m_accumulatedOffset += offset; image()->addJob(m_strokeId, new MoveStrokeStrategy::Data(m_accumulatedOffset)); notifyGuiAfterMove(); commitChanges(); setMode(KisTool::HOVER_MODE); } void KisToolMove::activate(ToolActivation toolActivation, const QSet &shapes) { KisTool::activate(toolActivation, shapes); m_actionConnections.addConnection(action("movetool-move-up"), SIGNAL(triggered(bool)), this, SLOT(slotMoveDiscreteUp())); m_actionConnections.addConnection(action("movetool-move-down"), SIGNAL(triggered(bool)), this, SLOT(slotMoveDiscreteDown())); m_actionConnections.addConnection(action("movetool-move-left"), SIGNAL(triggered(bool)), this, SLOT(slotMoveDiscreteLeft())); m_actionConnections.addConnection(action("movetool-move-right"), SIGNAL(triggered(bool)), this, SLOT(slotMoveDiscreteRight())); m_actionConnections.addConnection(action("movetool-move-up-more"), SIGNAL(triggered(bool)), this, SLOT(slotMoveDiscreteUpMore())); m_actionConnections.addConnection(action("movetool-move-down-more"), SIGNAL(triggered(bool)), this, SLOT(slotMoveDiscreteDownMore())); m_actionConnections.addConnection(action("movetool-move-left-more"), SIGNAL(triggered(bool)), this, SLOT(slotMoveDiscreteLeftMore())); m_actionConnections.addConnection(action("movetool-move-right-more"), SIGNAL(triggered(bool)), this, SLOT(slotMoveDiscreteRightMore())); + m_canvasConnections.addUniqueConnection(qobject_cast(canvas())->viewManager()->nodeManager(), SIGNAL(sigUiNeedChangeSelectedNodes(KisNodeList)), this, SLOT(slotNodeChanged(KisNodeList))); + m_canvasConnections.addUniqueConnection(qobject_cast(canvas())->viewManager()->selectionManager(), SIGNAL(currentSelectionChanged()), this, SLOT(slotSelectionChanged())); + connect(m_showCoordinatesAction, SIGNAL(triggered(bool)), m_optionsWidget, SLOT(setShowCoordinates(bool)), Qt::UniqueConnection); connect(m_optionsWidget, SIGNAL(showCoordinatesChanged(bool)), m_showCoordinatesAction, SLOT(setChecked(bool)), Qt::UniqueConnection); connect(&m_changesTracker, SIGNAL(sigConfigChanged(KisToolChangesTrackerDataSP)), SLOT(slotTrackerChangedConfig(KisToolChangesTrackerDataSP))); slotNodeChanged(this->selectedNodes()); } void KisToolMove::paint(QPainter& gc, const KoViewConverter &converter) { Q_UNUSED(converter); if (m_strokeId && !m_handlesRect.isEmpty()) { QPainterPath handles; handles.addRect(m_handlesRect.translated(currentOffset())); QPainterPath path = pixelToView(handles); paintToolOutline(&gc, path); } } void KisToolMove::deactivate() { m_actionConnections.clear(); + m_canvasConnections.clear(); disconnect(m_showCoordinatesAction, 0, this, 0); disconnect(m_optionsWidget, 0, this, 0); endStroke(); KisTool::deactivate(); } void KisToolMove::requestStrokeEnd() { endStroke(); } void KisToolMove::requestStrokeCancellation() { cancelStroke(); } void KisToolMove::requestUndoDuringStroke() { if (!m_strokeId) return; if (m_changesTracker.isEmpty()) { cancelStroke(); } else { m_changesTracker.requestUndo(); } } void KisToolMove::beginPrimaryAction(KoPointerEvent *event) { startAction(event, moveToolMode()); } void KisToolMove::continuePrimaryAction(KoPointerEvent *event) { continueAction(event); } void KisToolMove::endPrimaryAction(KoPointerEvent *event) { endAction(event); } void KisToolMove::beginAlternateAction(KoPointerEvent *event, AlternateAction action) { // Ctrl+Right click toggles between moving current layer and moving layer w/ content if (action == PickFgNode || action == PickBgImage) { MoveToolMode mode = moveToolMode(); if (mode == MoveSelectedLayer) { mode = MoveFirstLayer; } else if (mode == MoveFirstLayer) { mode = MoveSelectedLayer; } startAction(event, mode); } else { startAction(event, MoveGroup); } } void KisToolMove::continueAlternateAction(KoPointerEvent *event, AlternateAction action) { Q_UNUSED(action) continueAction(event); } void KisToolMove::endAlternateAction(KoPointerEvent *event, AlternateAction action) { Q_UNUSED(action) endAction(event); } void KisToolMove::mouseMoveEvent(KoPointerEvent *event) { m_lastCursorPos = convertToPixelCoord(event).toPoint(); KisTool::mouseMoveEvent(event); if (moveToolMode() == MoveFirstLayer) { m_updateCursorCompressor.start(); } } void KisToolMove::startAction(KoPointerEvent *event, MoveToolMode mode) { QPoint pos = convertToPixelCoordAndSnap(event).toPoint(); m_dragStart = pos; m_dragPos = pos; if (startStrokeImpl(mode, &pos)) { setMode(KisTool::PAINT_MODE); } else { event->ignore(); m_dragPos = QPoint(); m_dragStart = QPoint(); } qobject_cast(canvas())->updateCanvas(); } void KisToolMove::continueAction(KoPointerEvent *event) { CHECK_MODE_SANITY_OR_RETURN(KisTool::PAINT_MODE); if (!m_strokeId) return; QPoint pos = convertToPixelCoordAndSnap(event).toPoint(); pos = applyModifiers(event->modifiers(), pos); m_dragPos = pos; drag(pos); notifyGuiAfterMove(); qobject_cast(canvas())->updateCanvas(); } void KisToolMove::endAction(KoPointerEvent *event) { CHECK_MODE_SANITY_OR_RETURN(KisTool::PAINT_MODE); setMode(KisTool::HOVER_MODE); if (!m_strokeId) return; QPoint pos = convertToPixelCoordAndSnap(event).toPoint(); pos = applyModifiers(event->modifiers(), pos); drag(pos); m_accumulatedOffset += pos - m_dragStart; m_dragStart = QPoint(); m_dragPos = QPoint(); commitChanges(); notifyGuiAfterMove(); qobject_cast(canvas())->updateCanvas(); } void KisToolMove::drag(const QPoint& newPos) { KisImageWSP image = currentImage(); QPoint offset = m_accumulatedOffset + newPos - m_dragStart; image->addJob(m_strokeId, new MoveStrokeStrategy::Data(offset)); } void KisToolMove::endStroke() { if (!m_strokeId) return; if (m_asyncUpdateHelper.isActive()) { m_asyncUpdateHelper.endUpdateStream(); } KisImageSP image = currentImage(); image->endStroke(m_strokeId); m_strokeId.clear(); m_changesTracker.reset(); m_currentlyProcessingNodes.clear(); m_accumulatedOffset = QPoint(); qobject_cast(canvas())->updateCanvas(); } void KisToolMove::slotTrackerChangedConfig(KisToolChangesTrackerDataSP state) { KIS_SAFE_ASSERT_RECOVER_RETURN(m_strokeId); KisToolMoveState *newState = dynamic_cast(state.data()); KIS_SAFE_ASSERT_RECOVER_RETURN(newState); if (mode() == KisTool::PAINT_MODE) return; // Don't interact with dragging m_accumulatedOffset = newState->accumulatedOffset; image()->addJob(m_strokeId, new MoveStrokeStrategy::Data(m_accumulatedOffset)); notifyGuiAfterMove(); } void KisToolMove::slotMoveDiscreteLeft() { moveDiscrete(MoveDirection::Left, false); } void KisToolMove::slotMoveDiscreteRight() { moveDiscrete(MoveDirection::Right, false); } void KisToolMove::slotMoveDiscreteUp() { moveDiscrete(MoveDirection::Up, false); } void KisToolMove::slotMoveDiscreteDown() { moveDiscrete(MoveDirection::Down, false); } void KisToolMove::slotMoveDiscreteLeftMore() { moveDiscrete(MoveDirection::Left, true); } void KisToolMove::slotMoveDiscreteRightMore() { moveDiscrete(MoveDirection::Right, true); } void KisToolMove::slotMoveDiscreteUpMore() { moveDiscrete(MoveDirection::Up, true); } void KisToolMove::slotMoveDiscreteDownMore() { moveDiscrete(MoveDirection::Down, true); } void KisToolMove::cancelStroke() { if (!m_strokeId) return; if (m_asyncUpdateHelper.isActive()) { m_asyncUpdateHelper.cancelUpdateStream(); } KisImageSP image = currentImage(); image->cancelStroke(m_strokeId); m_strokeId.clear(); m_changesTracker.reset(); m_currentlyProcessingNodes.clear(); m_accumulatedOffset = QPoint(); notifyGuiAfterMove(); qobject_cast(canvas())->updateCanvas(); } QWidget* KisToolMove::createOptionWidget() { return m_optionsWidget; } KisToolMove::MoveToolMode KisToolMove::moveToolMode() const { if (m_optionsWidget) return m_optionsWidget->mode(); return MoveSelectedLayer; } QPoint KisToolMove::applyModifiers(Qt::KeyboardModifiers modifiers, QPoint pos) { QPoint move = pos - m_dragStart; // Snap to axis if (modifiers & Qt::ShiftModifier) { move = snapToClosestAxis(move); } // "Precision mode" - scale down movement by 1/5 if (modifiers & Qt::AltModifier) { const qreal SCALE_FACTOR = .2; move = SCALE_FACTOR * move; } return m_dragStart + move; } void KisToolMove::moveBySpinX(int newX) { if (mode() == KisTool::PAINT_MODE) return; // Don't interact with dragging if (!currentNode()->isEditable()) return; // Don't move invisible nodes if (startStrokeImpl(MoveSelectedLayer, 0)) { setMode(KisTool::PAINT_MODE); } m_accumulatedOffset.rx() = newX - m_handlesRect.x(); image()->addJob(m_strokeId, new MoveStrokeStrategy::Data(m_accumulatedOffset)); notifyGuiAfterMove(false); setMode(KisTool::HOVER_MODE); } void KisToolMove::moveBySpinY(int newY) { if (mode() == KisTool::PAINT_MODE) return; // Don't interact with dragging if (!currentNode()->isEditable()) return; // Don't move invisible nodes if (startStrokeImpl(MoveSelectedLayer, 0)) { setMode(KisTool::PAINT_MODE); } m_accumulatedOffset.ry() = newY - m_handlesRect.y(); image()->addJob(m_strokeId, new MoveStrokeStrategy::Data(m_accumulatedOffset)); notifyGuiAfterMove(false); setMode(KisTool::HOVER_MODE); } void KisToolMove::requestHandlesRectUpdate() { KisResourcesSnapshotSP resources = new KisResourcesSnapshot(image(), currentNode(), canvas()->resourceManager()); KisSelectionSP selection = resources->activeSelection(); KisMoveBoundsCalculationJob *job = new KisMoveBoundsCalculationJob(this->selectedNodes(), selection, this); connect(job, SIGNAL(sigCalcualtionFinished(const QRect&)), SLOT(slotHandlesRectCalculated(const QRect &))); KisImageSP image = this->image(); image->addSpontaneousJob(job); notifyGuiAfterMove(false); } void KisToolMove::slotNodeChanged(const KisNodeList &nodes) { if (m_strokeId && !tryEndPreviousStroke(nodes)) { return; } requestHandlesRectUpdate(); } void KisToolMove::slotSelectionChanged() { if (m_strokeId) return; requestHandlesRectUpdate(); } QList KisToolMoveFactory::createActionsImpl() { KisActionRegistry *actionRegistry = KisActionRegistry::instance(); QList actions = KisToolPaintFactoryBase::createActionsImpl(); actions << actionRegistry->makeQAction("movetool-move-up"); actions << actionRegistry->makeQAction("movetool-move-down"); actions << actionRegistry->makeQAction("movetool-move-left"); actions << actionRegistry->makeQAction("movetool-move-right"); actions << actionRegistry->makeQAction("movetool-move-up-more"); actions << actionRegistry->makeQAction("movetool-move-down-more"); actions << actionRegistry->makeQAction("movetool-move-left-more"); actions << actionRegistry->makeQAction("movetool-move-right-more"); actions << actionRegistry->makeQAction("movetool-show-coordinates"); return actions; } diff --git a/plugins/tools/basictools/kis_tool_move.h b/plugins/tools/basictools/kis_tool_move.h index d6271f06bb..f7f07666c8 100644 --- a/plugins/tools/basictools/kis_tool_move.h +++ b/plugins/tools/basictools/kis_tool_move.h @@ -1,208 +1,209 @@ /* * Copyright (c) 1999 Matthias Elter * 1999 Michael Koch * 2003 Patrick Julien * * 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. */ #ifndef KIS_TOOL_MOVE_H_ #define KIS_TOOL_MOVE_H_ #include #include #include #include #include #include #include #include #include #include "KisToolChangesTracker.h" #include "kis_signal_compressor.h" #include "kis_signal_auto_connection.h" #include "KisAsyncronousStrokeUpdateHelper.h" #include "kis_canvas2.h" class KoCanvasBase; class MoveToolOptionsWidget; class KisDocument; class KisToolMove : public KisTool { Q_OBJECT Q_ENUMS(MoveToolMode); public: KisToolMove(KoCanvasBase * canvas); ~KisToolMove() override; /** * @brief wantsAutoScroll * reimplemented from KoToolBase * there's an issue where autoscrolling with this tool never makes the * stroke end, so we return false here so that users don't get stuck with * the tool. See bug 362659 * @return false */ bool wantsAutoScroll() const override { return false; } public Q_SLOTS: void activate(ToolActivation toolActivation, const QSet &shapes) override; void deactivate() override; public Q_SLOTS: void requestStrokeEnd() override; void requestStrokeCancellation() override; void requestUndoDuringStroke() override; protected Q_SLOTS: void resetCursorStyle() override; public: enum MoveToolMode { MoveSelectedLayer, MoveFirstLayer, MoveGroup }; enum MoveDirection { Up, Down, Left, Right }; void beginPrimaryAction(KoPointerEvent *event) override; void continuePrimaryAction(KoPointerEvent *event) override; void endPrimaryAction(KoPointerEvent *event) override; void beginAlternateAction(KoPointerEvent *event, AlternateAction action) override; void continueAlternateAction(KoPointerEvent *event, AlternateAction action) override; void endAlternateAction(KoPointerEvent *event, AlternateAction action) override; void mouseMoveEvent(KoPointerEvent *event) override; void startAction(KoPointerEvent *event, MoveToolMode mode); void continueAction(KoPointerEvent *event); void endAction(KoPointerEvent *event); void paint(QPainter& gc, const KoViewConverter &converter) override; QWidget *createOptionWidget() override; void updateUIUnit(int newUnit); MoveToolMode moveToolMode() const; void setShowCoordinates(bool value); public Q_SLOTS: void moveDiscrete(MoveDirection direction, bool big); void moveBySpinX(int newX); void moveBySpinY(int newY); void slotNodeChanged(const KisNodeList &nodes); void slotSelectionChanged(); void commitChanges(); void slotHandlesRectCalculated(const QRect &handlesRect); Q_SIGNALS: void moveToolModeChanged(); void moveInNewPosition(QPoint); private: void drag(const QPoint& newPos); void cancelStroke(); QPoint applyModifiers(Qt::KeyboardModifiers modifiers, QPoint pos); bool startStrokeImpl(MoveToolMode mode, const QPoint *pos); QPoint currentOffset() const; void notifyGuiAfterMove(bool showFloatingMessage = true); bool tryEndPreviousStroke(const KisNodeList &nodes); KisNodeList fetchSelectedNodes(MoveToolMode mode, const QPoint *pixelPoint, KisSelectionSP selection); void requestHandlesRectUpdate(); private Q_SLOTS: void endStroke(); void slotTrackerChangedConfig(KisToolChangesTrackerDataSP state); void slotMoveDiscreteLeft(); void slotMoveDiscreteRight(); void slotMoveDiscreteUp(); void slotMoveDiscreteDown(); void slotMoveDiscreteLeftMore(); void slotMoveDiscreteRightMore(); void slotMoveDiscreteUpMore(); void slotMoveDiscreteDownMore(); private: MoveToolOptionsWidget* m_optionsWidget {0}; QPoint m_dragStart; ///< Point where current cursor dragging began QPoint m_accumulatedOffset; ///< Total offset including multiple clicks, up/down/left/right keys, etc. added together KisStrokeId m_strokeId; KisNodeList m_currentlyProcessingNodes; int m_resolution; QAction *m_showCoordinatesAction {0}; QPoint m_dragPos; QRect m_handlesRect; KisToolChangesTracker m_changesTracker; QPoint m_lastCursorPos; KisSignalCompressor m_updateCursorCompressor; KisSignalAutoConnectionsStore m_actionConnections; + KisSignalAutoConnectionsStore m_canvasConnections; KisAsyncronousStrokeUpdateHelper m_asyncUpdateHelper; }; class KisToolMoveFactory : public KisToolPaintFactoryBase { public: KisToolMoveFactory() : KisToolPaintFactoryBase("KritaTransform/KisToolMove") { setToolTip(i18n("Move Tool")); setSection(TOOL_TYPE_TRANSFORM); setActivationShapeId(KRITA_TOOL_ACTIVATION_ID); setPriority(3); setIconName(koIconNameCStr("krita_tool_move")); setShortcut(QKeySequence(Qt::Key_T)); } ~KisToolMoveFactory() override {} KoToolBase * createTool(KoCanvasBase *canvas) override { return new KisToolMove(canvas); } QList createActionsImpl() override; }; #endif // KIS_TOOL_MOVE_H_ diff --git a/plugins/tools/defaulttool/referenceimagestool/ToolReferenceImages.cpp b/plugins/tools/defaulttool/referenceimagestool/ToolReferenceImages.cpp index 9bea3bdc80..1293d8f3a3 100644 --- a/plugins/tools/defaulttool/referenceimagestool/ToolReferenceImages.cpp +++ b/plugins/tools/defaulttool/referenceimagestool/ToolReferenceImages.cpp @@ -1,307 +1,307 @@ /* * Copyright (c) 2017 Boudewijn Rempt * * This library is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; 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 Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser 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 "ToolReferenceImages.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "ToolReferenceImagesWidget.h" #include "KisReferenceImageCollection.h" ToolReferenceImages::ToolReferenceImages(KoCanvasBase * canvas) : DefaultTool(canvas, false) { setObjectName("ToolReferenceImages"); } ToolReferenceImages::~ToolReferenceImages() { } void ToolReferenceImages::activate(ToolActivation toolActivation, const QSet &shapes) { DefaultTool::activate(toolActivation, shapes); auto kisCanvas = dynamic_cast(canvas()); connect(kisCanvas->image(), SIGNAL(sigNodeAddedAsync(KisNodeSP)), this, SLOT(slotNodeAdded(KisNodeSP))); connect(kisCanvas->imageView()->document(), &KisDocument::sigReferenceImagesLayerChanged, this, &ToolReferenceImages::slotNodeAdded); auto referenceImageLayer = document()->referenceImagesLayer(); if (referenceImageLayer) { setReferenceImageLayer(referenceImageLayer); } } void ToolReferenceImages::deactivate() { DefaultTool::deactivate(); } void ToolReferenceImages::slotNodeAdded(KisNodeSP node) { auto *referenceImagesLayer = dynamic_cast(node.data()); if (referenceImagesLayer) { setReferenceImageLayer(referenceImagesLayer); } } void ToolReferenceImages::setReferenceImageLayer(KisSharedPtr layer) { m_layer = layer; connect(layer.data(), SIGNAL(selectionChanged()), this, SLOT(slotSelectionChanged())); } void ToolReferenceImages::addReferenceImage() { auto kisCanvas = dynamic_cast(canvas()); - KIS_ASSERT_RECOVER_RETURN(kisCanvas) + KIS_ASSERT_RECOVER_RETURN(kisCanvas); KoFileDialog dialog(kisCanvas->viewManager()->mainWindow(), KoFileDialog::OpenFile, "OpenReferenceImage"); dialog.setCaption(i18n("Select a Reference Image")); QStringList locations = QStandardPaths::standardLocations(QStandardPaths::PicturesLocation); if (!locations.isEmpty()) { dialog.setDefaultDir(locations.first()); } QString filename = dialog.filename(); if (filename.isEmpty()) return; if (!QFileInfo(filename).exists()) return; auto *reference = KisReferenceImage::fromFile(filename, *kisCanvas->coordinatesConverter(), canvas()->canvasWidget()); if (reference) { KisDocument *doc = document(); doc->addCommand(KisReferenceImagesLayer::addReferenceImages(doc, {reference})); } } void ToolReferenceImages::pasteReferenceImage() { KisCanvas2* kisCanvas = dynamic_cast(canvas()); - KIS_ASSERT_RECOVER_RETURN(kisCanvas) + KIS_ASSERT_RECOVER_RETURN(kisCanvas); KisReferenceImage* reference = KisReferenceImage::fromClipboard(*kisCanvas->coordinatesConverter()); if(reference) { KisDocument *doc = document(); doc->addCommand(KisReferenceImagesLayer::addReferenceImages(doc, {reference})); } } void ToolReferenceImages::removeAllReferenceImages() { auto layer = m_layer.toStrongRef(); if (!layer) return; canvas()->addCommand(layer->removeReferenceImages(document(), layer->shapes())); } void ToolReferenceImages::loadReferenceImages() { auto kisCanvas = dynamic_cast(canvas()); - KIS_ASSERT_RECOVER_RETURN(kisCanvas) + KIS_ASSERT_RECOVER_RETURN(kisCanvas); KoFileDialog dialog(kisCanvas->viewManager()->mainWindow(), KoFileDialog::OpenFile, "OpenReferenceImageCollection"); dialog.setMimeTypeFilters(QStringList() << "application/x-krita-reference-images"); dialog.setCaption(i18n("Load Reference Images")); QStringList locations = QStandardPaths::standardLocations(QStandardPaths::PicturesLocation); if (!locations.isEmpty()) { dialog.setDefaultDir(locations.first()); } QString filename = dialog.filename(); if (filename.isEmpty()) return; if (!QFileInfo(filename).exists()) return; QFile file(filename); if (!file.open(QIODevice::ReadOnly)) { QMessageBox::critical(nullptr, i18nc("@title:window", "Krita"), i18n("Could not open '%1'.", filename)); return; } KisReferenceImageCollection collection; if (collection.load(&file)) { QList shapes; Q_FOREACH(auto *reference, collection.referenceImages()) { shapes.append(reference); } KisDocument *doc = document(); doc->addCommand(KisReferenceImagesLayer::addReferenceImages(doc, shapes)); } else { QMessageBox::critical(nullptr, i18nc("@title:window", "Krita"), i18n("Could not load reference images from '%1'.", filename)); } file.close(); } void ToolReferenceImages::saveReferenceImages() { auto layer = m_layer.toStrongRef(); if (!layer || layer->shapeCount() == 0) return; auto kisCanvas = dynamic_cast(canvas()); - KIS_ASSERT_RECOVER_RETURN(kisCanvas) + KIS_ASSERT_RECOVER_RETURN(kisCanvas); KoFileDialog dialog(kisCanvas->viewManager()->mainWindow(), KoFileDialog::SaveFile, "SaveReferenceImageCollection"); dialog.setMimeTypeFilters(QStringList() << "application/x-krita-reference-images"); dialog.setCaption(i18n("Save Reference Images")); QStringList locations = QStandardPaths::standardLocations(QStandardPaths::PicturesLocation); if (!locations.isEmpty()) { dialog.setDefaultDir(locations.first()); } QString filename = dialog.filename(); if (filename.isEmpty()) return; QFile file(filename); if (!file.open(QIODevice::WriteOnly)) { QMessageBox::critical(nullptr, i18nc("@title:window", "Krita"), i18n("Could not open '%1' for saving.", filename)); return; } KisReferenceImageCollection collection(layer->referenceImages()); bool ok = collection.save(&file); file.close(); if (!ok) { QMessageBox::critical(nullptr, i18nc("@title:window", "Krita"), i18n("Failed to save reference images.")); } } void ToolReferenceImages::slotSelectionChanged() { auto layer = m_layer.toStrongRef(); if (!layer) return; m_optionsWidget->selectionChanged(layer->shapeManager()->selection()); updateActions(); } QList> ToolReferenceImages::createOptionWidgets() { // Instead of inheriting DefaultTool's multi-tab implementation, inherit straight from KoToolBase return KoToolBase::createOptionWidgets(); } QWidget *ToolReferenceImages::createOptionWidget() { if (!m_optionsWidget) { m_optionsWidget = new ToolReferenceImagesWidget(this); // See https://bugs.kde.org/show_bug.cgi?id=316896 QWidget *specialSpacer = new QWidget(m_optionsWidget); specialSpacer->setObjectName("SpecialSpacer"); specialSpacer->setFixedSize(0, 0); m_optionsWidget->layout()->addWidget(specialSpacer); } return m_optionsWidget; } bool ToolReferenceImages::isValidForCurrentLayer() const { return true; } KoShapeManager *ToolReferenceImages::shapeManager() const { auto layer = m_layer.toStrongRef(); return layer ? layer->shapeManager() : nullptr; } KoSelection *ToolReferenceImages::koSelection() const { auto manager = shapeManager(); return manager ? manager->selection() : nullptr; } void ToolReferenceImages::updateDistinctiveActions(const QList &) { action("object_group")->setEnabled(false); action("object_unite")->setEnabled(false); action("object_intersect")->setEnabled(false); action("object_subtract")->setEnabled(false); action("object_split")->setEnabled(false); action("object_ungroup")->setEnabled(false); } void ToolReferenceImages::deleteSelection() { auto layer = m_layer.toStrongRef(); if (!layer) return; QList shapes = koSelection()->selectedShapes(); if (!shapes.empty()) { canvas()->addCommand(layer->removeReferenceImages(document(), shapes)); } } KisDocument *ToolReferenceImages::document() const { auto kisCanvas = dynamic_cast(canvas()); return kisCanvas->imageView()->document(); } QList ToolReferenceImagesFactory::createActionsImpl() { QList defaultActions = DefaultToolFactory::createActionsImpl(); QList actions; QStringList actionNames; actionNames << "object_order_front" << "object_order_raise" << "object_order_lower" << "object_order_back" << "object_transform_rotate_90_cw" << "object_transform_rotate_90_ccw" << "object_transform_rotate_180" << "object_transform_mirror_horizontally" << "object_transform_mirror_vertically" << "object_transform_reset"; Q_FOREACH(QAction *action, defaultActions) { if (actionNames.contains(action->objectName())) { actions << action; } } return actions; } diff --git a/plugins/tools/selectiontools/KisToolSelectMagnetic.cc b/plugins/tools/selectiontools/KisToolSelectMagnetic.cc index 44f3bc1093..a25b234455 100644 --- a/plugins/tools/selectiontools/KisToolSelectMagnetic.cc +++ b/plugins/tools/selectiontools/KisToolSelectMagnetic.cc @@ -1,739 +1,739 @@ /* * Copyright (c) 2019 Kuntal Majumder * * 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 "KisToolSelectMagnetic.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kis_painter.h" #include #include "canvas/kis_canvas2.h" #include "kis_pixel_selection.h" #include "kis_selection_tool_helper.h" #include "kis_algebra_2d.h" #include "KisHandlePainterHelper.h" #include #define FEEDBACK_LINE_WIDTH 2 KisToolSelectMagnetic::KisToolSelectMagnetic(KoCanvasBase *canvas) : KisToolSelect(canvas, KisCursor::load("tool_magnetic_selection_cursor.png", 5, 5), i18n("Magnetic Selection")), m_continuedMode(false), m_complete(false), m_selected(false), m_finished(false), m_worker(image()->projection()), m_threshold(70), m_searchRadius(30), m_anchorGap(30), m_filterRadius(3.0), m_mouseHoverCompressor(100, KisSignalCompressor::FIRST_ACTIVE) { } void KisToolSelectMagnetic::keyPressEvent(QKeyEvent *event) { if (event->key() == Qt::Key_Control) { m_continuedMode = true; } KisToolSelect::keyPressEvent(event); } /* * Calculates the checkpoints responsible to determining the last point from where * the edge is calculated. * Takes 3 point, min, median and max, searches for an edge point from median to max, if fails, * searches for the same from median to min, if fails, median becomes that edge point. */ void KisToolSelectMagnetic::calculateCheckPoints(vQPointF points) { qreal totalDistance = 0.0; int checkPoint = 0; int finalPoint = 2; int midPoint = 1; int minPoint = 0; qreal maxFactor = 2; for (; finalPoint < points.count(); finalPoint++) { totalDistance += kisDistance(points[finalPoint], points[finalPoint - 1]); if (totalDistance <= m_anchorGap / 3) { minPoint = finalPoint; } if (totalDistance <= m_anchorGap) { midPoint = finalPoint; } if (totalDistance > maxFactor * m_anchorGap) { break; } } if (totalDistance > maxFactor * m_anchorGap) { bool foundSomething = false; for (int i = midPoint; i < finalPoint; i++) { if (m_worker.intensity(points.at(i).toPoint()) >= m_threshold) { m_lastAnchor = points.at(i).toPoint(); m_anchorPoints.push_back(m_lastAnchor); vQPointF temp; for (int j = 0; j <= i; j++) { temp.push_back(points[j]); } m_pointCollection.push_back(temp); foundSomething = true; checkPoint = i; break; } } if (!foundSomething) { for (int i = midPoint - 1; i >= minPoint; i--) { if (m_worker.intensity(points.at(i).toPoint()) >= m_threshold) { m_lastAnchor = points.at(i).toPoint(); m_anchorPoints.push_back(m_lastAnchor); vQPointF temp; for (int j = midPoint - 1; j >= i; j--) { temp.push_front(points[j]); } m_pointCollection.push_back(temp); foundSomething = true; checkPoint = i; break; } } } if (!foundSomething) { m_lastAnchor = points[midPoint].toPoint(); m_anchorPoints.push_back(m_lastAnchor); vQPointF temp; for (int j = 0; j <= midPoint; j++) { temp.push_back(points[j]); } m_pointCollection.push_back(temp); checkPoint = midPoint; foundSomething = true; } } totalDistance = 0.0; reEvaluatePoints(); for (; finalPoint < points.count(); finalPoint++) { totalDistance += kisDistance(points[finalPoint], points[checkPoint]); if (totalDistance > maxFactor * m_anchorGap) { points.remove(0, checkPoint + 1); calculateCheckPoints(points); break; } } } // KisToolSelectMagnetic::calculateCheckPoints void KisToolSelectMagnetic::keyReleaseEvent(QKeyEvent *event) { if (event->key() == Qt::Key_Control || !(event->modifiers() & Qt::ControlModifier)) { m_continuedMode = false; if (mode() != PAINT_MODE && !m_points.isEmpty()) { finishSelectionAction(); } } KisToolSelect::keyReleaseEvent(event); } vQPointF KisToolSelectMagnetic::computeEdgeWrapper(QPoint a, QPoint b) { return m_worker.computeEdge(m_searchRadius, a, b, m_filterRadius); } // the cursor is still tracked even when no mousebutton is pressed void KisToolSelectMagnetic::mouseMoveEvent(KoPointerEvent *event) { m_lastCursorPos = convertToPixelCoord(event); KisToolSelect::mouseMoveEvent(event); updatePaintPath(); } // KisToolSelectMagnetic::mouseMoveEvent // press primary mouse button void KisToolSelectMagnetic::beginPrimaryAction(KoPointerEvent *event) { setMode(KisTool::PAINT_MODE); QPointF temp(convertToPixelCoord(event)); if (!image()->bounds().contains(temp.toPoint())) { return; } m_cursorOnPress = temp; checkIfAnchorIsSelected(temp); if (m_complete || m_selected) { return; } if (m_anchorPoints.count() != 0) { vQPointF edge = computeEdgeWrapper(m_anchorPoints.last(), temp.toPoint()); m_points.append(edge); m_pointCollection.push_back(edge); } else { updateInitialAnchorBounds(temp.toPoint()); } m_lastAnchor = temp.toPoint(); m_anchorPoints.push_back(m_lastAnchor); m_lastCursorPos = temp; reEvaluatePoints(); updateCanvasPixelRect(image()->bounds()); } // KisToolSelectMagnetic::beginPrimaryAction void KisToolSelectMagnetic::checkIfAnchorIsSelected(QPointF temp) { Q_FOREACH (const QPoint pt, m_anchorPoints) { qreal zoomLevel = canvas()->viewConverter()->zoom(); int sides = (int) std::ceil(10.0 / zoomLevel); QRect r = QRect(QPoint(0, 0), QSize(sides, sides)); r.moveCenter(pt); if (r.contains(temp.toPoint())) { m_selected = true; m_selectedAnchor = m_anchorPoints.lastIndexOf(pt); return; } } } void KisToolSelectMagnetic::beginPrimaryDoubleClickAction(KoPointerEvent *event) { QPointF temp = convertToPixelCoord(event); if (!image()->bounds().contains(temp.toPoint())) { return; } checkIfAnchorIsSelected(temp); if (m_selected) { deleteSelectedAnchor(); return; } if (m_complete) { int pointA = 0, pointB = 1; double dist = std::numeric_limits::max(); int total = m_anchorPoints.count(); for (int i = 0; i < total; i++) { double distToCompare = kisDistance(m_anchorPoints[i], temp) + kisDistance(temp, m_anchorPoints[(i + 1) % total]); if (distToCompare < dist) { pointA = i; pointB = (i + 1) % total; dist = distToCompare; } } vQPointF path1 = computeEdgeWrapper(m_anchorPoints[pointA], temp.toPoint()); vQPointF path2 = computeEdgeWrapper(temp.toPoint(), m_anchorPoints[pointB]); m_pointCollection[pointA] = path1; m_pointCollection.insert(pointB, path2); m_anchorPoints.insert(pointB, temp.toPoint()); reEvaluatePoints(); } } // KisToolSelectMagnetic::beginPrimaryDoubleClickAction // drag while primary mouse button is pressed void KisToolSelectMagnetic::continuePrimaryAction(KoPointerEvent *event) { if (m_selected) { m_anchorPoints[m_selectedAnchor] = convertToPixelCoord(event).toPoint(); } else if (!m_complete) { m_lastCursorPos = convertToPixelCoord(event); if(kisDistance(m_lastCursorPos, m_cursorOnPress) >= m_anchorGap) m_mouseHoverCompressor.start(); } KisToolSelectBase::continuePrimaryAction(event); } void KisToolSelectMagnetic::slotCalculateEdge() { QPoint current = m_lastCursorPos.toPoint(); if (!image()->bounds().contains(current)) return; if(kisDistance(m_lastAnchor, current) < m_anchorGap) return; vQPointF pointSet = computeEdgeWrapper(m_lastAnchor, current); calculateCheckPoints(pointSet); } // release primary mouse button void KisToolSelectMagnetic::endPrimaryAction(KoPointerEvent *event) { if (m_selected && convertToPixelCoord(event) != m_cursorOnPress) { if (!image()->bounds().contains(m_anchorPoints[m_selectedAnchor])) { deleteSelectedAnchor(); } else { updateSelectedAnchor(); } } else if (m_selected) { QPointF temp(convertToPixelCoord(event)); if (!image()->bounds().contains(temp.toPoint())) { return; } if (m_snapBound.contains(temp) && m_anchorPoints.count() > 1) { if(m_complete){ finishSelectionAction(); return; } vQPointF edge = computeEdgeWrapper(m_anchorPoints.last(), temp.toPoint()); m_points.append(edge); m_pointCollection.push_back(edge); m_complete = true; } } if (m_mouseHoverCompressor.isActive()) { m_mouseHoverCompressor.stop(); slotCalculateEdge(); } m_selected = false; KisToolSelectBase::endPrimaryAction(event); } // KisToolSelectMagnetic::endPrimaryAction void KisToolSelectMagnetic::deleteSelectedAnchor() { if (m_anchorPoints.isEmpty()) return; // if it is the initial anchor if (m_selectedAnchor == 0) { m_anchorPoints.pop_front(); if (m_anchorPoints.isEmpty()) { // it was the only point lol resetVariables(); reEvaluatePoints(); return; } m_pointCollection.pop_front(); if (m_complete) { m_pointCollection[0] = computeEdgeWrapper(m_anchorPoints.first(), m_anchorPoints.last()); } reEvaluatePoints(); return; } // if it is the last anchor if (m_selectedAnchor == m_anchorPoints.count() - 1) { m_anchorPoints.pop_back(); m_pointCollection.pop_back(); if (m_complete) { m_pointCollection[m_selectedAnchor] = computeEdgeWrapper(m_anchorPoints.last(), m_anchorPoints.first()); } reEvaluatePoints(); return; } // it is in the middle m_anchorPoints.remove(m_selectedAnchor); m_pointCollection.remove(m_selectedAnchor); m_pointCollection[m_selectedAnchor - 1] = computeEdgeWrapper(m_anchorPoints[m_selectedAnchor - 1], m_anchorPoints[m_selectedAnchor]); reEvaluatePoints(); } // KisToolSelectMagnetic::deleteSelectedAnchor void KisToolSelectMagnetic::updateSelectedAnchor() { // initial if (m_selectedAnchor == 0 && m_anchorPoints.count() > 1) { m_pointCollection[m_selectedAnchor] = computeEdgeWrapper(m_anchorPoints[0], m_anchorPoints[1]); if (m_complete) { m_pointCollection[m_anchorPoints.count() - 1] = computeEdgeWrapper(m_anchorPoints.last(), m_anchorPoints.first()); } reEvaluatePoints(); return; } // last if (m_selectedAnchor == m_anchorPoints.count() - 1) { m_pointCollection[m_selectedAnchor - 1] = computeEdgeWrapper(m_anchorPoints[m_selectedAnchor - 1], m_anchorPoints.last()); if (m_complete) { m_pointCollection[m_selectedAnchor] = computeEdgeWrapper(m_anchorPoints.last(), m_anchorPoints.first()); } reEvaluatePoints(); return; } // middle m_pointCollection[m_selectedAnchor - 1] = computeEdgeWrapper(m_anchorPoints[m_selectedAnchor - 1], m_anchorPoints[m_selectedAnchor]); m_pointCollection[m_selectedAnchor] = computeEdgeWrapper(m_anchorPoints[m_selectedAnchor], m_anchorPoints[m_selectedAnchor + 1]); reEvaluatePoints(); } int KisToolSelectMagnetic::updateInitialAnchorBounds(QPoint pt) { qreal zoomLevel = canvas()->viewConverter()->zoom(); int sides = (int) std::ceil(10.0 / zoomLevel); m_snapBound = QRectF(QPoint(0, 0), QSize(sides, sides)); m_snapBound.moveCenter(pt); return sides; } void KisToolSelectMagnetic::reEvaluatePoints() { m_points.clear(); Q_FOREACH (const vQPointF vec, m_pointCollection) { m_points.append(vec); } updatePaintPath(); } void KisToolSelectMagnetic::finishSelectionAction() { KisCanvas2 *kisCanvas = dynamic_cast(canvas()); - KIS_ASSERT_RECOVER_RETURN(kisCanvas) + KIS_ASSERT_RECOVER_RETURN(kisCanvas); kisCanvas->updateCanvas(); setMode(KisTool::HOVER_MODE); m_complete = false; m_finished = true; // just for testing out // m_worker.saveTheImage(m_points); QRectF boundingViewRect = pixelToView(KisAlgebra2D::accumulateBounds(m_points)); KisSelectionToolHelper helper(kisCanvas, kundo2_i18n("Magnetic Selection")); if (m_points.count() > 2 && !helper.tryDeselectCurrentSelection(boundingViewRect, selectionAction())) { QApplication::setOverrideCursor(KisCursor::waitCursor()); const SelectionMode mode = helper.tryOverrideSelectionMode(kisCanvas->viewManager()->selection(), selectionMode(), selectionAction()); if (mode == PIXEL_SELECTION) { KisPixelSelectionSP tmpSel = KisPixelSelectionSP(new KisPixelSelection()); KisPainter painter(tmpSel); painter.setPaintColor(KoColor(Qt::black, tmpSel->colorSpace())); painter.setAntiAliasPolygonFill(antiAliasSelection()); painter.setFillStyle(KisPainter::FillStyleForegroundColor); painter.setStrokeStyle(KisPainter::StrokeStyleNone); painter.paintPolygon(m_points); QPainterPath cache; cache.addPolygon(m_points); cache.closeSubpath(); tmpSel->setOutlineCache(cache); helper.selectPixelSelection(tmpSel, selectionAction()); } else { KoPathShape *path = new KoPathShape(); path->setShapeId(KoPathShapeId); QTransform resolutionMatrix; resolutionMatrix.scale(1 / currentImage()->xRes(), 1 / currentImage()->yRes()); path->moveTo(resolutionMatrix.map(m_points[0])); for (int i = 1; i < m_points.count(); i++) path->lineTo(resolutionMatrix.map(m_points[i])); path->close(); path->normalize(); helper.addSelectionShape(path, selectionAction()); } QApplication::restoreOverrideCursor(); } resetVariables(); } // KisToolSelectMagnetic::finishSelectionAction void KisToolSelectMagnetic::resetVariables() { m_points.clear(); m_anchorPoints.clear(); m_pointCollection.clear(); m_paintPath = QPainterPath(); } void KisToolSelectMagnetic::updatePaintPath() { m_paintPath = QPainterPath(); if (m_points.size() > 0) { m_paintPath.moveTo(pixelToView(m_points[0])); } for (int i = 1; i < m_points.count(); i++) { m_paintPath.lineTo(pixelToView(m_points[i])); } updateFeedback(); if (m_continuedMode && mode() != PAINT_MODE) { updateContinuedMode(); } updateCanvasPixelRect(image()->bounds()); } void KisToolSelectMagnetic::paint(QPainter& gc, const KoViewConverter &converter) { Q_UNUSED(converter) updatePaintPath(); if ((mode() == KisTool::PAINT_MODE || m_continuedMode) && !m_anchorPoints.isEmpty()) { QPainterPath outline = m_paintPath; if (m_continuedMode && mode() != KisTool::PAINT_MODE) { outline.lineTo(pixelToView(m_lastCursorPos)); } paintToolOutline(&gc, outline); drawAnchors(gc); } } void KisToolSelectMagnetic::drawAnchors(QPainter &gc) { int sides = updateInitialAnchorBounds(m_anchorPoints.first()); Q_FOREACH (const QPoint pt, m_anchorPoints) { KisHandlePainterHelper helper(&gc, handleRadius()); QRect r(QPoint(0, 0), QSize(sides, sides)); r.moveCenter(pt); if (r.contains(m_lastCursorPos.toPoint())) { helper.setHandleStyle(KisHandleStyle::highlightedPrimaryHandles()); } else { helper.setHandleStyle(KisHandleStyle::primarySelection()); } helper.drawHandleRect(pixelToView(pt), 4, QPoint(0, 0)); } } void KisToolSelectMagnetic::updateFeedback() { if (m_points.count() > 1) { qint32 lastPointIndex = m_points.count() - 1; QRectF updateRect = QRectF(m_points[lastPointIndex - 1], m_points[lastPointIndex]).normalized(); updateRect = kisGrowRect(updateRect, FEEDBACK_LINE_WIDTH); updateCanvasPixelRect(updateRect); } } void KisToolSelectMagnetic::updateContinuedMode() { if (!m_points.isEmpty()) { qint32 lastPointIndex = m_points.count() - 1; QRectF updateRect = QRectF(m_points[lastPointIndex - 1], m_lastCursorPos).normalized(); updateRect = kisGrowRect(updateRect, FEEDBACK_LINE_WIDTH); updateCanvasPixelRect(updateRect); } } void KisToolSelectMagnetic::activate(KoToolBase::ToolActivation activation, const QSet &shapes) { m_worker = KisMagneticWorker(image()->projection()); m_configGroup = KSharedConfig::openConfig()->group(toolId()); connect(action("undo_polygon_selection"), SIGNAL(triggered()), SLOT(undoPoints()), Qt::UniqueConnection); connect(&m_mouseHoverCompressor, SIGNAL(timeout()), this, SLOT(slotCalculateEdge())); KisToolSelect::activate(activation, shapes); } void KisToolSelectMagnetic::deactivate() { KisCanvas2 *kisCanvas = dynamic_cast(canvas()); - KIS_ASSERT_RECOVER_RETURN(kisCanvas) + KIS_ASSERT_RECOVER_RETURN(kisCanvas); kisCanvas->updateCanvas(); resetVariables(); m_continuedMode = false; disconnect(action("undo_polygon_selection"), nullptr, this, nullptr); KisTool::deactivate(); } void KisToolSelectMagnetic::undoPoints() { if (m_complete) return; if(m_anchorPoints.count() <= 1){ resetVariables(); return; } m_anchorPoints.pop_back(); m_pointCollection.pop_back(); reEvaluatePoints(); } void KisToolSelectMagnetic::requestStrokeEnd() { if (m_finished || m_anchorPoints.count() < 2) return; finishSelectionAction(); m_finished = false; } void KisToolSelectMagnetic::requestStrokeCancellation() { m_complete = false; m_finished = false; resetVariables(); } QWidget * KisToolSelectMagnetic::createOptionWidget() { KisToolSelectBase::createOptionWidget(); KisSelectionOptions *selectionWidget = selectionOptionWidget(); QHBoxLayout *f1 = new QHBoxLayout(); QLabel *filterRadiusLabel = new QLabel(i18n("Filter Radius: "), selectionWidget); f1->addWidget(filterRadiusLabel); KisDoubleSliderSpinBox *filterRadiusInput = new KisDoubleSliderSpinBox(selectionWidget); filterRadiusInput->setObjectName("radius"); filterRadiusInput->setRange(2.5, 100.0, 2); filterRadiusInput->setSingleStep(0.5); filterRadiusInput->setToolTip("Radius of the filter for the detecting edges, might take some time to calculate"); f1->addWidget(filterRadiusInput); connect(filterRadiusInput, SIGNAL(valueChanged(qreal)), this, SLOT(slotSetFilterRadius(qreal))); QHBoxLayout *f2 = new QHBoxLayout(); QLabel *thresholdLabel = new QLabel(i18n("Threshold: "), selectionWidget); f2->addWidget(thresholdLabel); KisSliderSpinBox *thresholdInput = new KisSliderSpinBox(selectionWidget); thresholdInput->setObjectName("threshold"); thresholdInput->setRange(1, 255); thresholdInput->setSingleStep(10); thresholdInput->setToolTip("Threshold for determining the minimum intensity of the edges"); f2->addWidget(thresholdInput); connect(thresholdInput, SIGNAL(valueChanged(int)), this, SLOT(slotSetThreshold(int))); QHBoxLayout *f3 = new QHBoxLayout(); QLabel *searchRadiusLabel = new QLabel(i18n("Search Radius: "), selectionWidget); f3->addWidget(searchRadiusLabel); KisSliderSpinBox *searchRadiusInput = new KisSliderSpinBox(selectionWidget); searchRadiusInput->setObjectName("frequency"); searchRadiusInput->setRange(20, 200); searchRadiusInput->setSingleStep(10); searchRadiusInput->setToolTip("Extra area to be searched"); searchRadiusInput->setSuffix(" px"); f3->addWidget(searchRadiusInput); connect(searchRadiusInput, SIGNAL(valueChanged(int)), this, SLOT(slotSetSearchRadius(int))); QHBoxLayout *f4 = new QHBoxLayout(); QLabel *anchorGapLabel = new QLabel(i18n("Anchor Gap: "), selectionWidget); f4->addWidget(anchorGapLabel); KisSliderSpinBox *anchorGapInput = new KisSliderSpinBox(selectionWidget); anchorGapInput->setObjectName("anchorgap"); anchorGapInput->setRange(20, 200); anchorGapInput->setSingleStep(10); anchorGapInput->setToolTip("Gap between 2 anchors in interative mode"); anchorGapInput->setSuffix(" px"); f4->addWidget(anchorGapInput); connect(anchorGapInput, SIGNAL(valueChanged(int)), this, SLOT(slotSetAnchorGap(int))); QVBoxLayout *l = dynamic_cast(selectionWidget->layout()); l->insertLayout(1, f1); l->insertLayout(2, f2); l->insertLayout(3, f3); l->insertLayout(5, f4); filterRadiusInput->setValue(m_configGroup.readEntry("filterradius", 3.0)); thresholdInput->setValue(m_configGroup.readEntry("threshold", 100)); searchRadiusInput->setValue(m_configGroup.readEntry("searchradius", 30)); anchorGapInput->setValue(m_configGroup.readEntry("anchorgap", 20)); return selectionWidget; } // KisToolSelectMagnetic::createOptionWidget void KisToolSelectMagnetic::slotSetFilterRadius(qreal r) { m_filterRadius = r; m_configGroup.writeEntry("filterradius", r); } void KisToolSelectMagnetic::slotSetThreshold(int t) { m_threshold = t; m_configGroup.writeEntry("threshold", t); } void KisToolSelectMagnetic::slotSetSearchRadius(int r) { m_searchRadius = r; m_configGroup.writeEntry("searchradius", r); } void KisToolSelectMagnetic::slotSetAnchorGap(int g) { m_anchorGap = g; m_configGroup.writeEntry("anchorgap", g); } void KisToolSelectMagnetic::resetCursorStyle() { if (selectionAction() == SELECTION_ADD) { useCursor(KisCursor::load("tool_magnetic_selection_cursor_add.png", 6, 6)); } else if (selectionAction() == SELECTION_SUBTRACT) { useCursor(KisCursor::load("tool_magnetic_selection_cursor_sub.png", 6, 6)); } else { KisToolSelect::resetCursorStyle(); } } diff --git a/plugins/tools/tool_polygon/kis_tool_polygon.cc b/plugins/tools/tool_polygon/kis_tool_polygon.cc index 9644f53682..c886ab38fc 100644 --- a/plugins/tools/tool_polygon/kis_tool_polygon.cc +++ b/plugins/tools/tool_polygon/kis_tool_polygon.cc @@ -1,81 +1,86 @@ /* * kis_tool_polygon.cc -- part of Krita * * Copyright (c) 2004 Michael Thaler * Copyright (c) 2009 Lukáš Tvrdý * Copyright (c) 2010 Cyrille Berger * Copyright (C) 2010 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_tool_polygon.h" #include #include #include #include #include #include "kis_figure_painting_tool_helper.h" KisToolPolygon::KisToolPolygon(KoCanvasBase *canvas) : KisToolPolylineBase(canvas, KisToolPolylineBase::PAINT, KisCursor::load("tool_polygon_cursor.png", 6, 6)) { setObjectName("tool_polygon"); setSupportOutline(true); } KisToolPolygon::~KisToolPolygon() { } void KisToolPolygon::resetCursorStyle() { KisToolPolylineBase::resetCursorStyle(); overrideCursorIfNotEditable(); } void KisToolPolygon::finishPolyline(const QVector& points) { const KisToolShape::ShapeAddInfo info = shouldAddShape(currentNode()); if (!info.shouldAddShape) { KisFigurePaintingToolHelper helper(kundo2_i18n("Draw Polygon"), image(), currentNode(), canvas()->resourceManager(), strokeStyle(), fillStyle()); helper.paintPolygon(points); } else { + // remove the last point if it overlaps with the first + QVector newPoints = points; + if (newPoints.size() > 1 && newPoints.first() == newPoints.last()) { + newPoints.removeLast(); + } KoPathShape* path = new KoPathShape(); path->setShapeId(KoPathShapeId); QTransform resolutionMatrix; resolutionMatrix.scale(1 / currentImage()->xRes(), 1 / currentImage()->yRes()); - path->moveTo(resolutionMatrix.map(points[0])); - for (int i = 1; i < points.count(); i++) - path->lineTo(resolutionMatrix.map(points[i])); + path->moveTo(resolutionMatrix.map(newPoints[0])); + for (int i = 1; i < newPoints.size(); i++) + path->lineTo(resolutionMatrix.map(newPoints[i])); path->close(); path->normalize(); info.markAsSelectionShapeIfNeeded(path); addShape(path); } } diff --git a/plugins/tools/tool_transform2/strokes/transform_stroke_strategy.cpp b/plugins/tools/tool_transform2/strokes/transform_stroke_strategy.cpp index ba97d4f312..9390384de0 100644 --- a/plugins/tools/tool_transform2/strokes/transform_stroke_strategy.cpp +++ b/plugins/tools/tool_transform2/strokes/transform_stroke_strategy.cpp @@ -1,681 +1,697 @@ /* * Copyright (c) 2013 Dmitry Kazakov * * 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 "transform_stroke_strategy.h" #include #include "kundo2commandextradata.h" #include "kis_node_progress_proxy.h" #include #include #include #include #include #include #include #include #include "kis_transform_mask_adapter.h" #include "kis_transform_utils.h" #include "kis_abstract_projection_plane.h" #include "kis_recalculate_transform_mask_job.h" #include "kis_projection_leaf.h" #include "kis_modify_transform_mask_command.h" #include "kis_sequential_iterator.h" #include "kis_selection_mask.h" #include "kis_image_config.h" #include "kis_layer_utils.h" #include #include #include "transform_transaction_properties.h" #include "krita_container_utils.h" #include "commands_new/kis_saved_commands.h" #include "kis_command_ids.h" #include "KisRunnableStrokeJobUtils.h" #include "commands_new/KisHoldUIUpdatesCommand.h" TransformStrokeStrategy::TransformStrokeStrategy(ToolTransformArgs::TransformMode mode, bool workRecursively, const QString &filterId, bool forceReset, KisNodeSP rootNode, KisSelectionSP selection, KisStrokeUndoFacade *undoFacade, KisUpdatesFacade *updatesFacade) : KisStrokeStrategyUndoCommandBased(kundo2_i18n("Transform"), false, undoFacade), m_updatesFacade(updatesFacade), m_mode(mode), m_workRecursively(workRecursively), m_filterId(filterId), m_forceReset(forceReset), m_selection(selection) { KIS_SAFE_ASSERT_RECOVER_NOOP(!selection || !dynamic_cast(rootNode.data())); m_rootNode = rootNode; setMacroId(KisCommandUtils::TransformToolId); } TransformStrokeStrategy::~TransformStrokeStrategy() { } KisPaintDeviceSP TransformStrokeStrategy::createDeviceCache(KisPaintDeviceSP dev) { KisPaintDeviceSP cache; if (m_selection) { QRect srcRect = m_selection->selectedExactRect(); cache = dev->createCompositionSourceDevice(); KisPainter gc(cache); gc.setSelection(m_selection); gc.bitBlt(srcRect.topLeft(), dev, srcRect); } else { cache = dev->createCompositionSourceDevice(dev); } return cache; } bool TransformStrokeStrategy::haveDeviceInCache(KisPaintDeviceSP src) { QMutexLocker l(&m_devicesCacheMutex); return m_devicesCacheHash.contains(src.data()); } void TransformStrokeStrategy::putDeviceCache(KisPaintDeviceSP src, KisPaintDeviceSP cache) { QMutexLocker l(&m_devicesCacheMutex); m_devicesCacheHash.insert(src.data(), cache); } KisPaintDeviceSP TransformStrokeStrategy::getDeviceCache(KisPaintDeviceSP src) { QMutexLocker l(&m_devicesCacheMutex); KisPaintDeviceSP cache = m_devicesCacheHash.value(src.data()); if (!cache) { warnKrita << "WARNING: Transform Stroke: the device is absent in cache!"; } return cache; } bool TransformStrokeStrategy::checkBelongsToSelection(KisPaintDeviceSP device) const { return m_selection && (device == m_selection->pixelSelection().data() || device == m_selection->projection().data()); } void TransformStrokeStrategy::doStrokeCallback(KisStrokeJobData *data) { TransformData *td = dynamic_cast(data); ClearSelectionData *csd = dynamic_cast(data); PreparePreviewData *ppd = dynamic_cast(data); TransformAllData *runAllData = dynamic_cast(data); if (runAllData) { // here we only save the passed args, actual // transformation will be performed during // finish job m_savedTransformArgs = runAllData->config; } else if (ppd) { KisNodeSP rootNode = m_rootNode; KisNodeList processedNodes = m_processedNodes; KisPaintDeviceSP previewDevice; if (rootNode->childCount() || !rootNode->paintDevice()) { if (KisTransformMask* tmask = dynamic_cast(rootNode.data())) { previewDevice = createDeviceCache(tmask->buildPreviewDevice()); KIS_SAFE_ASSERT_RECOVER(!m_selection) { m_selection = 0; } } else if (KisGroupLayer *group = dynamic_cast(rootNode.data())) { const QRect bounds = group->image()->bounds(); KisImageSP clonedImage = new KisImage(0, bounds.width(), bounds.height(), group->colorSpace(), "transformed_image"); KisGroupLayerSP clonedGroup = dynamic_cast(group->clone().data()); // In case the group is pass-through, it needs to be disabled for the preview, // otherwise it will crash (no parent for a preview leaf). // Also it needs to be done before setting the root layer for clonedImage. // Result: preview for pass-through group is the same as for standard group // (i.e. filter layers in the group won't affect the layer stack for a moment). clonedGroup->setPassThroughMode(false); clonedImage->setRootLayer(clonedGroup); QQueue linearizedSrcNodes; KisLayerUtils::recursiveApplyNodes(rootNode, [&linearizedSrcNodes] (KisNodeSP node) { linearizedSrcNodes.enqueue(node); }); KisLayerUtils::recursiveApplyNodes(KisNodeSP(clonedGroup), [&linearizedSrcNodes, processedNodes] (KisNodeSP node) { KisNodeSP srcNode = linearizedSrcNodes.dequeue(); if (!processedNodes.contains(srcNode)) { node->setVisible(false); } }); clonedImage->refreshGraph(); KisLayerUtils::refreshHiddenAreaAsync(clonedImage, clonedGroup, clonedImage->bounds()); KisLayerUtils::forceAllDelayedNodesUpdate(clonedGroup); clonedImage->waitForDone(); previewDevice = createDeviceCache(clonedImage->projection()); previewDevice->setDefaultBounds(group->projection()->defaultBounds()); // we delete the cloned image in GUI thread to ensure // no signals are still pending makeKisDeleteLaterWrapper(clonedImage)->deleteLater(); } else { rootNode->projectionLeaf()->explicitlyRegeneratePassThroughProjection(); previewDevice = createDeviceCache(rootNode->projection()); } } else { KisPaintDeviceSP cacheDevice = createDeviceCache(rootNode->paintDevice()); if (dynamic_cast(rootNode.data())) { KIS_SAFE_ASSERT_RECOVER (cacheDevice->colorSpace()->colorModelId() == GrayAColorModelID && cacheDevice->colorSpace()->colorDepthId() == Integer8BitsColorDepthID) { cacheDevice->convertTo(KoColorSpaceRegistry::instance()->colorSpace(GrayAColorModelID.id(), Integer8BitsColorDepthID.id())); } previewDevice = new KisPaintDevice(KoColorSpaceRegistry::instance()->rgb8()); const QRect srcRect = cacheDevice->exactBounds(); KisSequentialConstIterator srcIt(cacheDevice, srcRect); KisSequentialIterator dstIt(previewDevice, srcRect); const int pixelSize = previewDevice->colorSpace()->pixelSize(); KisImageConfig cfg(true); KoColor pixel(cfg.selectionOverlayMaskColor(), previewDevice->colorSpace()); const qreal coeff = 1.0 / 255.0; const qreal baseOpacity = 0.5; while (srcIt.nextPixel() && dstIt.nextPixel()) { qreal gray = srcIt.rawDataConst()[0]; qreal alpha = srcIt.rawDataConst()[1]; pixel.setOpacity(quint8(gray * alpha * baseOpacity * coeff)); memcpy(dstIt.rawData(), pixel.data(), pixelSize); } } else { previewDevice = cacheDevice; } putDeviceCache(rootNode->paintDevice(), cacheDevice); } emit sigPreviewDeviceReady(previewDevice); } else if(td) { if (td->destination == TransformData::PAINT_DEVICE) { QRect oldExtent = td->node->extent(); KisPaintDeviceSP device = td->node->paintDevice(); if (device && !checkBelongsToSelection(device)) { KisPaintDeviceSP cachedPortion = getDeviceCache(device); Q_ASSERT(cachedPortion); KisTransaction transaction(device); KisProcessingVisitor::ProgressHelper helper(td->node); transformAndMergeDevice(td->config, cachedPortion, device, &helper); runAndSaveCommand(KUndo2CommandSP(transaction.endAndTake()), KisStrokeJobData::CONCURRENT, KisStrokeJobData::NORMAL); td->node->setDirty(oldExtent | td->node->extent()); } else if (KisExternalLayer *extLayer = dynamic_cast(td->node.data())) { if (td->config.mode() == ToolTransformArgs::FREE_TRANSFORM || (td->config.mode() == ToolTransformArgs::PERSPECTIVE_4POINT && extLayer->supportsPerspectiveTransform())) { QVector3D transformedCenter; KisTransformWorker w = KisTransformUtils::createTransformWorker(td->config, 0, 0, &transformedCenter); QTransform t = w.transform(); runAndSaveCommand(KUndo2CommandSP(extLayer->transform(t)), KisStrokeJobData::CONCURRENT, KisStrokeJobData::NORMAL); } } else if (KisTransformMask *transformMask = dynamic_cast(td->node.data())) { runAndSaveCommand(KUndo2CommandSP( new KisModifyTransformMaskCommand(transformMask, KisTransformMaskParamsInterfaceSP( new KisTransformMaskAdapter(td->config)))), KisStrokeJobData::CONCURRENT, KisStrokeJobData::NORMAL); } } else if (m_selection) { /** * We use usual transaction here, because we cannot calsulate * transformation for perspective and warp workers. */ KisTransaction transaction(m_selection->pixelSelection()); KisProcessingVisitor::ProgressHelper helper(td->node); KisTransformUtils::transformDevice(td->config, m_selection->pixelSelection(), &helper); runAndSaveCommand(KUndo2CommandSP(transaction.endAndTake()), KisStrokeJobData::CONCURRENT, KisStrokeJobData::NORMAL); } } else if (csd) { KisPaintDeviceSP device = csd->node->paintDevice(); if (device && !checkBelongsToSelection(device)) { if (!haveDeviceInCache(device)) { putDeviceCache(device, createDeviceCache(device)); } clearSelection(device); /** * Selection masks might have an overlay enabled, we should disable that */ if (KisSelectionMask *mask = dynamic_cast(csd->node.data())) { KisSelectionSP selection = mask->selection(); if (selection) { selection->setVisible(false); m_deactivatedSelections.append(selection); mask->setDirty(); } } } else if (KisExternalLayer *externalLayer = dynamic_cast(csd->node.data())) { externalLayer->projectionLeaf()->setTemporaryHiddenFromRendering(true); externalLayer->setDirty(); m_hiddenProjectionLeaves.append(csd->node); } else if (KisTransformMask *transformMask = dynamic_cast(csd->node.data())) { runAndSaveCommand(KUndo2CommandSP( new KisModifyTransformMaskCommand(transformMask, KisTransformMaskParamsInterfaceSP( new KisDumbTransformMaskParams(true)))), KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::NORMAL); } } else { KisStrokeStrategyUndoCommandBased::doStrokeCallback(data); } } void TransformStrokeStrategy::clearSelection(KisPaintDeviceSP device) { KisTransaction transaction(device); if (m_selection) { device->clearSelection(m_selection); } else { QRect oldExtent = device->extent(); device->clear(); device->setDirty(oldExtent); } runAndSaveCommand(KUndo2CommandSP(transaction.endAndTake()), KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::NORMAL); } void TransformStrokeStrategy::transformAndMergeDevice(const ToolTransformArgs &config, KisPaintDeviceSP src, KisPaintDeviceSP dst, KisProcessingVisitor::ProgressHelper *helper) { KoUpdaterPtr mergeUpdater = src != dst ? helper->updater() : 0; KisTransformUtils::transformDevice(config, src, helper); if (src != dst) { QRect mergeRect = src->extent(); KisPainter painter(dst); painter.setProgress(mergeUpdater); painter.bitBlt(mergeRect.topLeft(), src, mergeRect); painter.end(); } } struct TransformExtraData : public KUndo2CommandExtraData { ToolTransformArgs savedTransformArgs; KisNodeSP rootNode; KisNodeList transformedNodes; KUndo2CommandExtraData* clone() const override { return new TransformExtraData(*this); } }; void TransformStrokeStrategy::postProcessToplevelCommand(KUndo2Command *command) { KIS_SAFE_ASSERT_RECOVER_RETURN(m_savedTransformArgs); TransformExtraData *data = new TransformExtraData(); data->savedTransformArgs = *m_savedTransformArgs; data->rootNode = m_rootNode; data->transformedNodes = m_processedNodes; command->setExtraData(data); KisSavedMacroCommand *macroCommand = dynamic_cast(command); KIS_SAFE_ASSERT_RECOVER_NOOP(macroCommand); if (m_overriddenCommand && macroCommand) { macroCommand->setOverrideInfo(m_overriddenCommand, m_skippedWhileMergeCommands); } KisStrokeStrategyUndoCommandBased::postProcessToplevelCommand(command); } bool TransformStrokeStrategy::fetchArgsFromCommand(const KUndo2Command *command, ToolTransformArgs *args, KisNodeSP *rootNode, KisNodeList *transformedNodes) { const TransformExtraData *data = dynamic_cast(command->extraData()); if (data) { *args = data->savedTransformArgs; *rootNode = data->rootNode; *transformedNodes = data->transformedNodes; } return bool(data); } QList TransformStrokeStrategy::fetchNodesList(ToolTransformArgs::TransformMode mode, KisNodeSP root, bool recursive) { QList result; auto fetchFunc = [&result, mode, root] (KisNodeSP node) { if (node->isEditable(node == root) && (!node->inherits("KisShapeLayer") || mode == ToolTransformArgs::FREE_TRANSFORM) && !node->inherits("KisFileLayer") && (!node->inherits("KisTransformMask") || node == root)) { result << node; } }; if (recursive) { KisLayerUtils::recursiveApplyNodes(root, fetchFunc); } else { fetchFunc(root); } return result; } bool TransformStrokeStrategy::tryInitArgsFromNode(KisNodeSP node, ToolTransformArgs *args) { bool result = false; if (KisTransformMaskSP mask = dynamic_cast(node.data())) { KisTransformMaskParamsInterfaceSP savedParams = mask->transformParams(); KisTransformMaskAdapter *adapter = dynamic_cast(savedParams.data()); if (adapter) { *args = adapter->transformArgs(); result = true; } } return result; } bool TransformStrokeStrategy::tryFetchArgsFromCommandAndUndo(ToolTransformArgs *outArgs, ToolTransformArgs::TransformMode mode, KisNodeSP currentNode, KisNodeList selectedNodes, QVector *undoJobs) { bool result = false; const KUndo2Command *lastCommand = undoFacade()->lastExecutedCommand(); KisNodeSP oldRootNode; KisNodeList oldTransformedNodes; ToolTransformArgs args; if (lastCommand && TransformStrokeStrategy::fetchArgsFromCommand(lastCommand, &args, &oldRootNode, &oldTransformedNodes) && args.mode() == mode && oldRootNode == currentNode) { if (KritaUtils::compareListsUnordered(oldTransformedNodes, selectedNodes)) { args.saveContinuedState(); *outArgs = args; const KisSavedMacroCommand *command = dynamic_cast(lastCommand); KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(command, false); // the jobs are fetched as !shouldGoToHistory, // so there is no need to put them into // m_skippedWhileMergeCommands command->getCommandExecutionJobs(undoJobs, true, false); m_overriddenCommand = command; result = true; } } return result; } void TransformStrokeStrategy::initStrokeCallback() { KisStrokeStrategyUndoCommandBased::initStrokeCallback(); if (m_selection) { m_selection->setVisible(false); m_deactivatedSelections.append(m_selection); } + KisSelectionMaskSP overlaySelectionMask = + dynamic_cast(m_rootNode->graphListener()->graphOverlayNode()); + if (overlaySelectionMask) { + overlaySelectionMask->selection()->setVisible(false); + overlaySelectionMask->setDirty(); + m_deactivatedOverlaySelectionMask = overlaySelectionMask; + } + + ToolTransformArgs initialTransformArgs; m_processedNodes = fetchNodesList(m_mode, m_rootNode, m_workRecursively); bool argsAreInitialized = false; QVector lastCommandUndoJobs; if (!m_forceReset && tryFetchArgsFromCommandAndUndo(&initialTransformArgs, m_mode, m_rootNode, m_processedNodes, &lastCommandUndoJobs)) { argsAreInitialized = true; } else if (!m_forceReset && tryInitArgsFromNode(m_rootNode, &initialTransformArgs)) { argsAreInitialized = true; } QVector extraInitJobs; extraInitJobs << new Data(new KisHoldUIUpdatesCommand(m_updatesFacade, KisCommandUtils::FlipFlopCommand::INITIALIZING), false, KisStrokeJobData::BARRIER); extraInitJobs << lastCommandUndoJobs; KritaUtils::addJobSequential(extraInitJobs, [this]() { /** * We must request shape layers to rerender areas outside image bounds */ KisLayerUtils::forceAllHiddenOriginalsUpdate(m_rootNode); }); KritaUtils::addJobBarrier(extraInitJobs, [this]() { /** * We must ensure that the currently selected subtree * has finished all its updates. */ KisLayerUtils::forceAllDelayedNodesUpdate(m_rootNode); }); KritaUtils::addJobBarrier(extraInitJobs, [this, initialTransformArgs, argsAreInitialized]() mutable { QRect srcRect; if (m_selection) { srcRect = m_selection->selectedExactRect(); } else { srcRect = QRect(); Q_FOREACH (KisNodeSP node, m_processedNodes) { // group layers may have a projection of layers // that are locked and will not be transformed if (node->inherits("KisGroupLayer")) continue; if (const KisTransformMask *mask = dynamic_cast(node.data())) { srcRect |= mask->sourceDataBounds(); + } else if (const KisSelectionMask *mask = dynamic_cast(node.data())) { + srcRect |= mask->selection()->selectedExactRect(); } else { srcRect |= node->exactBounds(); } } } TransformTransactionProperties transaction(srcRect, &initialTransformArgs, m_rootNode, m_processedNodes); if (!argsAreInitialized) { initialTransformArgs = KisTransformUtils::resetArgsForMode(m_mode, m_filterId, transaction); } this->m_initialTransformArgs = initialTransformArgs; emit this->sigTransactionGenerated(transaction, initialTransformArgs, this); }); extraInitJobs << new PreparePreviewData(); Q_FOREACH (KisNodeSP node, m_processedNodes) { extraInitJobs << new ClearSelectionData(node); } extraInitJobs << new Data(toQShared(new KisHoldUIUpdatesCommand(m_updatesFacade, KisCommandUtils::FlipFlopCommand::FINALIZING)), false, KisStrokeJobData::BARRIER); if (!lastCommandUndoJobs.isEmpty()) { KIS_SAFE_ASSERT_RECOVER_NOOP(m_overriddenCommand); for (auto it = extraInitJobs.begin(); it != extraInitJobs.end(); ++it) { (*it)->setCancellable(false); } } addMutatedJobs(extraInitJobs); } void TransformStrokeStrategy::finishStrokeImpl(bool applyTransform, const ToolTransformArgs &args) { /** * Since our finishStrokeCallback() initiates new jobs, * cancellation request may come even after * finishStrokeCallback() (cancellations may be called * until there are no jobs left in the stroke's queue). * * Therefore we should check for double-entry here and * make sure the finilizing jobs are no cancellable. */ if (m_finalizingActionsStarted) return; m_finalizingActionsStarted = true; QVector mutatedJobs; if (applyTransform) { Q_FOREACH (KisNodeSP node, m_processedNodes) { mutatedJobs << new TransformData(TransformData::PAINT_DEVICE, args, node); } mutatedJobs << new TransformData(TransformData::SELECTION, args, m_rootNode); } KritaUtils::addJobBarrier(mutatedJobs, [this, applyTransform]() { Q_FOREACH (KisSelectionSP selection, m_deactivatedSelections) { selection->setVisible(true); } Q_FOREACH (KisNodeSP node, m_hiddenProjectionLeaves) { node->projectionLeaf()->setTemporaryHiddenFromRendering(false); } + if (m_deactivatedOverlaySelectionMask) { + m_deactivatedOverlaySelectionMask->selection()->setVisible(true); + m_deactivatedOverlaySelectionMask->setDirty(); + } + if (applyTransform) { KisStrokeStrategyUndoCommandBased::finishStrokeCallback(); } else { KisStrokeStrategyUndoCommandBased::cancelStrokeCallback(); } }); for (auto it = mutatedJobs.begin(); it != mutatedJobs.end(); ++it) { (*it)->setCancellable(false); } addMutatedJobs(mutatedJobs); } void TransformStrokeStrategy::finishStrokeCallback() { if (!m_savedTransformArgs || m_savedTransformArgs->isIdentity()) { cancelStrokeCallback(); return; } finishStrokeImpl(true, *m_savedTransformArgs); } void TransformStrokeStrategy::cancelStrokeCallback() { const bool shouldRecoverSavedInitialState = !m_initialTransformArgs.isIdentity(); if (shouldRecoverSavedInitialState) { m_savedTransformArgs = m_initialTransformArgs; } finishStrokeImpl(shouldRecoverSavedInitialState, *m_savedTransformArgs); } diff --git a/plugins/tools/tool_transform2/strokes/transform_stroke_strategy.h b/plugins/tools/tool_transform2/strokes/transform_stroke_strategy.h index 7ef746f2a7..6e1d6dc276 100644 --- a/plugins/tools/tool_transform2/strokes/transform_stroke_strategy.h +++ b/plugins/tools/tool_transform2/strokes/transform_stroke_strategy.h @@ -1,177 +1,178 @@ /* * Copyright (c) 2013 Dmitry Kazakov * * 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. */ #ifndef __TRANSFORM_STROKE_STRATEGY_H #define __TRANSFORM_STROKE_STRATEGY_H #include #include #include #include #include #include "tool_transform_args.h" #include #include #include class KisPostExecutionUndoAdapter; class TransformTransactionProperties; class KisUpdatesFacade; class TransformStrokeStrategy : public QObject, public KisStrokeStrategyUndoCommandBased { Q_OBJECT public: struct TransformAllData : public KisStrokeJobData { TransformAllData(const ToolTransformArgs &_config) : KisStrokeJobData(SEQUENTIAL, NORMAL), config(_config) {} ToolTransformArgs config; }; class TransformData : public KisStrokeJobData { public: enum Destination { PAINT_DEVICE, SELECTION, }; public: TransformData(Destination _destination, const ToolTransformArgs &_config, KisNodeSP _node) : KisStrokeJobData(CONCURRENT, NORMAL), destination(_destination), config(_config), node(_node) { } Destination destination; ToolTransformArgs config; KisNodeSP node; }; class ClearSelectionData : public KisStrokeJobData { public: ClearSelectionData(KisNodeSP _node) : KisStrokeJobData(SEQUENTIAL, NORMAL), node(_node) { } KisNodeSP node; }; class PreparePreviewData : public KisStrokeJobData { public: PreparePreviewData() : KisStrokeJobData(BARRIER, NORMAL) { } }; public: TransformStrokeStrategy(ToolTransformArgs::TransformMode mode, bool workRecursively, const QString &filterId, bool forceReset, KisNodeSP rootNode, KisSelectionSP selection, KisStrokeUndoFacade *undoFacade, KisUpdatesFacade *updatesFacade); ~TransformStrokeStrategy() override; void initStrokeCallback() override; void finishStrokeCallback() override; void cancelStrokeCallback() override; void doStrokeCallback(KisStrokeJobData *data) override; static bool fetchArgsFromCommand(const KUndo2Command *command, ToolTransformArgs *args, KisNodeSP *rootNode, KisNodeList *transformedNodes); Q_SIGNALS: void sigTransactionGenerated(TransformTransactionProperties transaction, ToolTransformArgs args, void *cookie); void sigPreviewDeviceReady(KisPaintDeviceSP device); protected: void postProcessToplevelCommand(KUndo2Command *command) override; private: KoUpdaterPtr fetchUpdater(KisNodeSP node); void transformAndMergeDevice(const ToolTransformArgs &config, KisPaintDeviceSP src, KisPaintDeviceSP dst, KisProcessingVisitor::ProgressHelper *helper); void transformDevice(const ToolTransformArgs &config, KisPaintDeviceSP device, KisProcessingVisitor::ProgressHelper *helper); void clearSelection(KisPaintDeviceSP device); //void transformDevice(KisPaintDeviceSP src, KisPaintDeviceSP dst, KisProcessingVisitor::ProgressHelper *helper); bool checkBelongsToSelection(KisPaintDeviceSP device) const; KisPaintDeviceSP createDeviceCache(KisPaintDeviceSP src); bool haveDeviceInCache(KisPaintDeviceSP src); void putDeviceCache(KisPaintDeviceSP src, KisPaintDeviceSP cache); KisPaintDeviceSP getDeviceCache(KisPaintDeviceSP src); QList fetchNodesList(ToolTransformArgs::TransformMode mode, KisNodeSP root, bool recursive); ToolTransformArgs resetArgsForMode(ToolTransformArgs::TransformMode mode, const QString &filterId, const TransformTransactionProperties &transaction); bool tryInitArgsFromNode(KisNodeSP node, ToolTransformArgs *args); bool tryFetchArgsFromCommandAndUndo(ToolTransformArgs *args, ToolTransformArgs::TransformMode mode, KisNodeSP currentNode, KisNodeList selectedNodes, QVector *undoJobs); void finishStrokeImpl(bool applyTransform, const ToolTransformArgs &args); private: KisUpdatesFacade *m_updatesFacade; ToolTransformArgs::TransformMode m_mode; bool m_workRecursively; QString m_filterId; bool m_forceReset; KisSelectionSP m_selection; QMutex m_devicesCacheMutex; QHash m_devicesCacheHash; KisTransformMaskSP writeToTransformMask; ToolTransformArgs m_initialTransformArgs; boost::optional m_savedTransformArgs; KisNodeSP m_rootNode; KisNodeList m_processedNodes; QList m_deactivatedSelections; QList m_hiddenProjectionLeaves; + KisSelectionMaskSP m_deactivatedOverlaySelectionMask; const KisSavedMacroCommand *m_overriddenCommand = 0; QVector m_skippedWhileMergeCommands; bool m_finalizingActionsStarted = false; }; #endif /* __TRANSFORM_STROKE_STRATEGY_H */