diff --git a/libs/global/kis_thread_safe_signal_compressor.cpp b/libs/global/kis_thread_safe_signal_compressor.cpp index 4a2ceb59aa..6a06a13585 100644 --- a/libs/global/kis_thread_safe_signal_compressor.cpp +++ b/libs/global/kis_thread_safe_signal_compressor.cpp @@ -1,44 +1,49 @@ /* * 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_thread_safe_signal_compressor.h" #include KisThreadSafeSignalCompressor::KisThreadSafeSignalCompressor(int delay, KisSignalCompressor::Mode mode) : m_compressor(new KisSignalCompressor(delay, mode, this)) { connect(this, SIGNAL(internalRequestSignal()), m_compressor, SLOT(start()), Qt::QueuedConnection); connect(this, SIGNAL(internalStopSignal()), m_compressor, SLOT(stop()), Qt::QueuedConnection); connect(m_compressor, SIGNAL(timeout()), SIGNAL(timeout())); // due to this line the object *must not* be deleted explicitly! this->setObjectName("KisThreadSafeSignalCompressor"); this->moveToThread(QApplication::instance()->thread()); } +bool KisThreadSafeSignalCompressor::isActive() const +{ + return m_compressor->isActive(); +} + void KisThreadSafeSignalCompressor::start() { emit internalRequestSignal(); } void KisThreadSafeSignalCompressor::stop() { emit internalStopSignal(); } diff --git a/libs/global/kis_thread_safe_signal_compressor.h b/libs/global/kis_thread_safe_signal_compressor.h index 5d57ce2937..c7f1efce04 100644 --- a/libs/global/kis_thread_safe_signal_compressor.h +++ b/libs/global/kis_thread_safe_signal_compressor.h @@ -1,58 +1,60 @@ /* * 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_THREAD_SAFE_SIGNAL_COMPRESSOR_H #define __KIS_THREAD_SAFE_SIGNAL_COMPRESSOR_H #include #include "kritaglobal_export.h" #include "kis_signal_compressor.h" /** * A special class which works exactly like KisSignalCompressor, but * supports calling \p start() method from within the context of * another thread. If it happens, it posts a message to Qt's event * loop and the \p start() signal is delivered when event loop gets * executes again. * * WARNING: After creation this object moves itself into the main * thread, so one must *not* delete it explicitly. Use * deleteLater() instead. Moving into another thread is * another reason why it cannot have parent QObject. */ class KRITAGLOBAL_EXPORT KisThreadSafeSignalCompressor : public QObject { Q_OBJECT public: KisThreadSafeSignalCompressor(int delay, KisSignalCompressor::Mode mode); + bool isActive() const; + public Q_SLOTS: void start(); void stop(); Q_SIGNALS: void timeout(); void internalRequestSignal(); void internalStopSignal(); private: KisSignalCompressor *m_compressor; }; #endif /* __KIS_THREAD_SAFE_SIGNAL_COMPRESSOR_H */ diff --git a/libs/image/kis_transform_mask.cpp b/libs/image/kis_transform_mask.cpp index 66671f5216..fc37b545c6 100644 --- a/libs/image/kis_transform_mask.cpp +++ b/libs/image/kis_transform_mask.cpp @@ -1,466 +1,465 @@ /* * 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 #include #include #include "kis_layer.h" #include "kis_transform_mask.h" #include "filter/kis_filter.h" #include "filter/kis_filter_configuration.h" #include "filter/kis_filter_registry.h" #include "kis_selection.h" #include "kis_processing_information.h" #include "kis_node.h" #include "kis_node_visitor.h" #include "kis_processing_visitor.h" #include "kis_node_progress_proxy.h" #include "kis_transaction.h" #include "kis_painter.h" #include "kis_busy_progress_indicator.h" #include "kis_perspectivetransform_worker.h" #include "kis_transform_mask_params_interface.h" #include "kis_transform_mask_params_factory_registry.h" #include "kis_recalculate_transform_mask_job.h" -#include "kis_signal_compressor.h" +#include "kis_thread_safe_signal_compressor.h" #include "kis_algebra_2d.h" #include "kis_safe_transform.h" #include "kis_keyframe_channel.h" #include "kis_image_config.h" //#include "kis_paint_device_debug_utils.h" //#define DEBUG_RENDERING //#define DUMP_RECT QRect(0,0,512,512) #define UPDATE_DELAY 3000 /*ms */ struct Q_DECL_HIDDEN KisTransformMask::Private { - Private(KisTransformMask *q) + Private() : worker(0, QTransform(), 0), staticCacheValid(false), recalculatingStaticImage(false), - updateSignalCompressor(UPDATE_DELAY, KisSignalCompressor::POSTPONE, q), + updateSignalCompressor(UPDATE_DELAY, KisSignalCompressor::POSTPONE), offBoundsReadArea(0.5) { } - Private(const Private &rhs, KisTransformMask *q) + Private(const Private &rhs) : worker(rhs.worker), params(rhs.params), staticCacheValid(false), recalculatingStaticImage(rhs.recalculatingStaticImage), - updateSignalCompressor(UPDATE_DELAY, KisSignalCompressor::POSTPONE, q) + updateSignalCompressor(UPDATE_DELAY, KisSignalCompressor::POSTPONE), + offBoundsReadArea(rhs.offBoundsReadArea) { } void reloadParameters() { QTransform affineTransform; if (params->isAffine()) { affineTransform = params->finalAffineTransform(); } worker.setForwardTransform(affineTransform); params->clearChangedFlag(); staticCacheValid = false; } KisPerspectiveTransformWorker worker; KisTransformMaskParamsInterfaceSP params; bool staticCacheValid; bool recalculatingStaticImage; KisPaintDeviceSP staticCacheDevice; - KisSignalCompressor updateSignalCompressor; + KisThreadSafeSignalCompressor updateSignalCompressor; qreal offBoundsReadArea; }; KisTransformMask::KisTransformMask() : KisEffectMask(), - m_d(new Private(this)) + m_d(new Private()) { setTransformParams( KisTransformMaskParamsInterfaceSP( new KisDumbTransformMaskParams())); - connect(this, SIGNAL(initiateDelayedStaticUpdate()), &m_d->updateSignalCompressor, SLOT(start())); - connect(this, SIGNAL(forceTerminateDelayedStaticUpdate()), &m_d->updateSignalCompressor, SLOT(stop())); connect(&m_d->updateSignalCompressor, SIGNAL(timeout()), SLOT(slotDelayedStaticUpdate())); KisImageConfig cfg; m_d->offBoundsReadArea = cfg.transformMaskOffBoundsReadArea(); } KisTransformMask::~KisTransformMask() { } KisTransformMask::KisTransformMask(const KisTransformMask& rhs) : KisEffectMask(rhs), - m_d(new Private(*rhs.m_d, this)) + m_d(new Private(*rhs.m_d)) { connect(&m_d->updateSignalCompressor, SIGNAL(timeout()), SLOT(slotDelayedStaticUpdate())); } KisPaintDeviceSP KisTransformMask::paintDevice() const { return 0; } QIcon KisTransformMask::icon() const { return KisIconUtils::loadIcon("transformMask"); } void KisTransformMask::setTransformParams(KisTransformMaskParamsInterfaceSP params) { KIS_ASSERT_RECOVER(params) { params = KisTransformMaskParamsInterfaceSP( new KisDumbTransformMaskParams()); } m_d->params = params; m_d->reloadParameters(); - emit forceTerminateDelayedStaticUpdate(); + m_d->updateSignalCompressor.stop(); } KisTransformMaskParamsInterfaceSP KisTransformMask::transformParams() const { return m_d->params; } void KisTransformMask::slotDelayedStaticUpdate() { /** * The mask might have been deleted from the layers stack in the * meanwhile. Just ignore the updates in the case. */ KisLayerSP parentLayer = qobject_cast(parent().data()); if (!parentLayer) return; KisImageSP image = parentLayer->image(); if (image) { image->addSpontaneousJob(new KisRecalculateTransformMaskJob(this)); } } KisPaintDeviceSP KisTransformMask::buildPreviewDevice() { /** * Note: this function must be called from within the scheduler's * context. We are accessing parent's updateProjection(), which * is not entirely safe. */ KisLayerSP parentLayer = qobject_cast(parent().data()); KIS_ASSERT_RECOVER(parentLayer) { return new KisPaintDevice(colorSpace()); } KisPaintDeviceSP device = new KisPaintDevice(parentLayer->original()->colorSpace()); QRect requestedRect = parentLayer->original()->exactBounds(); parentLayer->buildProjectionUpToNode(device, this, requestedRect); return device; } void KisTransformMask::recaclulateStaticImage() { /** * Note: this function must be called from within the scheduler's * context. We are accessing parent's updateProjection(), which * is not entirely safe. */ KisLayerSP parentLayer = qobject_cast(parent().data()); KIS_ASSERT_RECOVER_RETURN(parentLayer); if (!m_d->staticCacheDevice) { m_d->staticCacheDevice = new KisPaintDevice(parentLayer->original()->colorSpace()); } m_d->recalculatingStaticImage = true; /** * updateProjection() is assuming that the requestedRect takes * into account all the change rects of all the masks. Usually, * this work is done by the walkers. */ QRect requestedRect = parentLayer->changeRect(parentLayer->original()->exactBounds()); /** * Here we use updateProjection() to regenerate the projection of * the layer and after that a special update call (no-filthy) will * be issued to pass the changed further through the stack. */ parentLayer->updateProjection(requestedRect, this); m_d->recalculatingStaticImage = false; m_d->staticCacheValid = true; } QRect KisTransformMask::decorateRect(KisPaintDeviceSP &src, KisPaintDeviceSP &dst, const QRect & rc, PositionToFilthy maskPos) const { Q_ASSERT_X(src != dst, "KisTransformMask::decorateRect", "src must be != dst, because we can't create transactions " "during merge, as it breaks reentrancy"); KIS_ASSERT_RECOVER(m_d->params) { return rc; } if (m_d->params->isHidden()) return rc; KIS_ASSERT_RECOVER_NOOP(maskPos == N_FILTHY || maskPos == N_ABOVE_FILTHY || maskPos == N_BELOW_FILTHY); if (m_d->params->hasChanged()) m_d->reloadParameters(); if (!m_d->recalculatingStaticImage && (maskPos == N_FILTHY || maskPos == N_ABOVE_FILTHY)) { m_d->staticCacheValid = false; - emit initiateDelayedStaticUpdate(); + m_d->updateSignalCompressor.start(); } if (m_d->recalculatingStaticImage) { m_d->staticCacheDevice->clear(); m_d->params->transformDevice(const_cast(this), src, m_d->staticCacheDevice); QRect updatedRect = m_d->staticCacheDevice->extent(); KisPainter::copyAreaOptimized(updatedRect.topLeft(), m_d->staticCacheDevice, dst, updatedRect); #ifdef DEBUG_RENDERING qDebug() << "Recalculate" << name() << ppVar(src->exactBounds()) << ppVar(dst->exactBounds()) << ppVar(rc); KIS_DUMP_DEVICE_2(src, DUMP_RECT, "recalc_src", "dd"); KIS_DUMP_DEVICE_2(dst, DUMP_RECT, "recalc_dst", "dd"); #endif /* DEBUG_RENDERING */ } else if (!m_d->staticCacheValid && m_d->params->isAffine()) { m_d->worker.runPartialDst(src, dst, rc); #ifdef DEBUG_RENDERING qDebug() << "Partial" << name() << ppVar(src->exactBounds()) << ppVar(src->extent()) << ppVar(dst->exactBounds()) << ppVar(dst->extent()) << ppVar(rc); KIS_DUMP_DEVICE_2(src, DUMP_RECT, "partial_src", "dd"); KIS_DUMP_DEVICE_2(dst, DUMP_RECT, "partial_dst", "dd"); #endif /* DEBUG_RENDERING */ } else if (m_d->staticCacheDevice && m_d->staticCacheValid) { KisPainter::copyAreaOptimized(rc.topLeft(), m_d->staticCacheDevice, dst, rc); #ifdef DEBUG_RENDERING qDebug() << "Fetch" << name() << ppVar(src->exactBounds()) << ppVar(dst->exactBounds()) << ppVar(rc); KIS_DUMP_DEVICE_2(src, DUMP_RECT, "fetch_src", "dd"); KIS_DUMP_DEVICE_2(dst, DUMP_RECT, "fetch_dst", "dd"); #endif /* DEBUG_RENDERING */ } KIS_ASSERT_RECOVER_NOOP(this->busyProgressIndicator()); this->busyProgressIndicator()->update(); return rc; } bool KisTransformMask::accept(KisNodeVisitor &v) { return v.visit(this); } void KisTransformMask::accept(KisProcessingVisitor &visitor, KisUndoAdapter *undoAdapter) { return visitor.visit(this, undoAdapter); } QRect KisTransformMask::changeRect(const QRect &rect, PositionToFilthy pos) const { Q_UNUSED(pos); /** * FIXME: This check of the emptiness should be done * on the higher/lower level */ if (rect.isEmpty()) return rect; QRect changeRect = rect; if (m_d->params->isAffine()) { QRect bounds; QRect interestRect; KisNodeSP parentNode = parent(); if (parentNode) { bounds = parentNode->original()->defaultBounds()->bounds(); interestRect = parentNode->original()->extent(); } else { bounds = QRect(0,0,777,777); interestRect = QRect(0,0,888,888); warnKrita << "WARNING: transform mask has no parent (change rect)." << "Cannot run safe transformations." << "Will limit bounds to" << ppVar(bounds); } const QRect limitingRect = KisAlgebra2D::blowRect(bounds, m_d->offBoundsReadArea); if (m_d->params->hasChanged()) m_d->reloadParameters(); KisSafeTransform transform(m_d->worker.forwardTransform(), limitingRect, interestRect); changeRect = transform.mapRectForward(rect); } else { QRect interestRect; interestRect = parent() ? parent()->original()->extent() : QRect(); changeRect = m_d->params->nonAffineChangeRect(rect); } return changeRect; } QRect KisTransformMask::needRect(const QRect& rect, PositionToFilthy pos) const { Q_UNUSED(pos); /** * FIXME: This check of the emptiness should be done * on the higher/lower level */ if (rect.isEmpty()) return rect; if (!m_d->params->isAffine()) return rect; QRect bounds; QRect interestRect; KisNodeSP parentNode = parent(); if (parentNode) { bounds = parentNode->original()->defaultBounds()->bounds(); interestRect = parentNode->original()->extent(); } else { bounds = QRect(0,0,777,777); interestRect = QRect(0,0,888,888); warnKrita << "WARNING: transform mask has no parent (need rect)." << "Cannot run safe transformations." << "Will limit bounds to" << ppVar(bounds); } QRect needRect = rect; if (m_d->params->isAffine()) { const QRect limitingRect = KisAlgebra2D::blowRect(bounds, m_d->offBoundsReadArea); if (m_d->params->hasChanged()) m_d->reloadParameters(); KisSafeTransform transform(m_d->worker.forwardTransform(), limitingRect, interestRect); needRect = transform.mapRectBackward(rect); } else { needRect = m_d->params->nonAffineNeedRect(rect, interestRect); } return needRect; } QRect KisTransformMask::extent() const { QRect rc = KisMask::extent(); QRect partialChangeRect; QRect existentProjection; KisLayerSP parentLayer = qobject_cast(parent().data()); if (parentLayer) { partialChangeRect = parentLayer->partialChangeRect(const_cast(this), rc); existentProjection = parentLayer->projection()->extent(); } return changeRect(partialChangeRect) | existentProjection; } QRect KisTransformMask::exactBounds() const { QRect rc = KisMask::exactBounds(); QRect partialChangeRect; QRect existentProjection; KisLayerSP parentLayer = qobject_cast(parent().data()); if (parentLayer) { partialChangeRect = parentLayer->partialChangeRect(const_cast(this), rc); existentProjection = parentLayer->projection()->exactBounds(); } return changeRect(partialChangeRect) | existentProjection; } void KisTransformMask::setX(qint32 x) { m_d->params->translate(QPointF(x - this->x(), 0)); setTransformParams(m_d->params); KisEffectMask::setX(x); } void KisTransformMask::setY(qint32 y) { m_d->params->translate(QPointF(0, y - this->y())); setTransformParams(m_d->params); KisEffectMask::setY(y); } void KisTransformMask::forceUpdateTimedNode() { if (m_d->updateSignalCompressor.isActive()) { KIS_SAFE_ASSERT_RECOVER_NOOP(!m_d->staticCacheValid); - emit forceTerminateDelayedStaticUpdate(); + m_d->updateSignalCompressor.stop(); slotDelayedStaticUpdate(); } } KisKeyframeChannel *KisTransformMask::requestKeyframeChannel(const QString &id) { if (id == KisKeyframeChannel::TransformArguments.id() || id == KisKeyframeChannel::TransformPositionX.id() || id == KisKeyframeChannel::TransformPositionY.id() || id == KisKeyframeChannel::TransformScaleX.id() || id == KisKeyframeChannel::TransformScaleY.id() || id == KisKeyframeChannel::TransformShearX.id() || id == KisKeyframeChannel::TransformShearY.id() || id == KisKeyframeChannel::TransformRotationX.id() || id == KisKeyframeChannel::TransformRotationY.id() || id == KisKeyframeChannel::TransformRotationZ.id()) { KisAnimatedTransformParamsInterface *animatedParams = dynamic_cast(m_d->params.data()); if (!animatedParams) { auto converted = KisTransformMaskParamsFactoryRegistry::instance()->animateParams(m_d->params); if (converted.isNull()) return KisEffectMask::requestKeyframeChannel(id); m_d->params = converted; animatedParams = dynamic_cast(converted.data()); } KisKeyframeChannel *channel = animatedParams->getKeyframeChannel(id, parent()->original()->defaultBounds()); if (channel) return channel; } return KisEffectMask::requestKeyframeChannel(id); } diff --git a/libs/image/kis_transform_mask.h b/libs/image/kis_transform_mask.h index e2d4c7489e..bdb8c44647 100644 --- a/libs/image/kis_transform_mask.h +++ b/libs/image/kis_transform_mask.h @@ -1,94 +1,90 @@ /* * 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. */ #ifndef _KIS_TRANSFORM_MASK_ #define _KIS_TRANSFORM_MASK_ #include #include "kis_types.h" #include "kis_effect_mask.h" #include "KisDelayedUpdateNodeInterface.h" /** Transform a layer according to a matrix transform */ class KRITAIMAGE_EXPORT KisTransformMask : public KisEffectMask, public KisDelayedUpdateNodeInterface { Q_OBJECT public: /** * Create an empty filter mask. */ KisTransformMask(); ~KisTransformMask() override; QIcon icon() const override; KisNodeSP clone() const override { return KisNodeSP(new KisTransformMask(*this)); } KisPaintDeviceSP paintDevice() const override; bool accept(KisNodeVisitor &v) override; void accept(KisProcessingVisitor &visitor, KisUndoAdapter *undoAdapter) override; KisTransformMask(const KisTransformMask& rhs); QRect decorateRect(KisPaintDeviceSP &src, KisPaintDeviceSP &dst, const QRect & rc, PositionToFilthy maskPos) const override; QRect changeRect(const QRect &rect, PositionToFilthy pos = N_FILTHY) const override; QRect needRect(const QRect &rect, PositionToFilthy pos = N_FILTHY) const override; QRect extent() const override; QRect exactBounds() const override; void setTransformParams(KisTransformMaskParamsInterfaceSP params); KisTransformMaskParamsInterfaceSP transformParams() const; void recaclulateStaticImage(); KisPaintDeviceSP buildPreviewDevice(); void setX(qint32 x) override; void setY(qint32 y) override; void forceUpdateTimedNode() override; protected: KisKeyframeChannel *requestKeyframeChannel(const QString &id) override; private Q_SLOTS: void slotDelayedStaticUpdate(); -Q_SIGNALS: - void initiateDelayedStaticUpdate() const; - void forceTerminateDelayedStaticUpdate() const; - private: struct Private; const QScopedPointer m_d; }; #endif //_KIS_TRANSFORM_MASK_ diff --git a/libs/image/layerstyles/kis_layer_style_projection_plane.cpp b/libs/image/layerstyles/kis_layer_style_projection_plane.cpp index 927845f071..7e97d99f51 100644 --- a/libs/image/layerstyles/kis_layer_style_projection_plane.cpp +++ b/libs/image/layerstyles/kis_layer_style_projection_plane.cpp @@ -1,249 +1,281 @@ /* * 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_layer_style_projection_plane.h" #include "kis_global.h" #include "kis_layer_style_filter_projection_plane.h" #include "kis_psd_layer_style.h" #include "kis_ls_drop_shadow_filter.h" #include "kis_ls_satin_filter.h" #include "kis_ls_overlay_filter.h" #include "kis_ls_stroke_filter.h" #include "kis_ls_bevel_emboss_filter.h" +#include "kis_projection_leaf.h" struct Q_DECL_HIDDEN KisLayerStyleProjectionPlane::Private { KisAbstractProjectionPlaneWSP sourceProjectionPlane; QVector stylesBefore; QVector stylesAfter; KisPSDLayerStyleSP style; + bool canHaveChildNodes = false; + bool dependsOnLowerNodes = false; }; KisLayerStyleProjectionPlane::KisLayerStyleProjectionPlane(KisLayer *sourceLayer) : m_d(new Private) { KisPSDLayerStyleSP style = sourceLayer->layerStyle(); KIS_ASSERT_RECOVER(style) { style = toQShared(new KisPSDLayerStyle()); } init(sourceLayer, style); } // for testing purposes only! KisLayerStyleProjectionPlane::KisLayerStyleProjectionPlane(KisLayer *sourceLayer, KisPSDLayerStyleSP layerStyle) : m_d(new Private) { init(sourceLayer, layerStyle); } void KisLayerStyleProjectionPlane::init(KisLayer *sourceLayer, KisPSDLayerStyleSP style) { Q_ASSERT(sourceLayer); m_d->sourceProjectionPlane = sourceLayer->internalProjectionPlane(); m_d->style = style; + m_d->canHaveChildNodes = sourceLayer->projectionLeaf()->canHaveChildLayers(); + m_d->dependsOnLowerNodes = sourceLayer->projectionLeaf()->dependsOnLowerNodes(); { KisLayerStyleFilterProjectionPlane *dropShadow = new KisLayerStyleFilterProjectionPlane(sourceLayer); dropShadow->setStyle(new KisLsDropShadowFilter(KisLsDropShadowFilter::DropShadow), style); m_d->stylesBefore << toQShared(dropShadow); } { KisLayerStyleFilterProjectionPlane *innerShadow = new KisLayerStyleFilterProjectionPlane(sourceLayer); innerShadow->setStyle(new KisLsDropShadowFilter(KisLsDropShadowFilter::InnerShadow), style); m_d->stylesAfter << toQShared(innerShadow); } { KisLayerStyleFilterProjectionPlane *outerGlow = new KisLayerStyleFilterProjectionPlane(sourceLayer); outerGlow->setStyle(new KisLsDropShadowFilter(KisLsDropShadowFilter::OuterGlow), style); m_d->stylesAfter << toQShared(outerGlow); } { KisLayerStyleFilterProjectionPlane *innerGlow = new KisLayerStyleFilterProjectionPlane(sourceLayer); innerGlow->setStyle(new KisLsDropShadowFilter(KisLsDropShadowFilter::InnerGlow), style); m_d->stylesAfter << toQShared(innerGlow); } { KisLayerStyleFilterProjectionPlane *satin = new KisLayerStyleFilterProjectionPlane(sourceLayer); satin->setStyle(new KisLsSatinFilter(), style); m_d->stylesAfter << toQShared(satin); } { KisLayerStyleFilterProjectionPlane *colorOverlay = new KisLayerStyleFilterProjectionPlane(sourceLayer); colorOverlay->setStyle(new KisLsOverlayFilter(KisLsOverlayFilter::Color), style); m_d->stylesAfter << toQShared(colorOverlay); } { KisLayerStyleFilterProjectionPlane *gradientOverlay = new KisLayerStyleFilterProjectionPlane(sourceLayer); gradientOverlay->setStyle(new KisLsOverlayFilter(KisLsOverlayFilter::Gradient), style); m_d->stylesAfter << toQShared(gradientOverlay); } { KisLayerStyleFilterProjectionPlane *patternOverlay = new KisLayerStyleFilterProjectionPlane(sourceLayer); patternOverlay->setStyle(new KisLsOverlayFilter(KisLsOverlayFilter::Pattern), style); m_d->stylesAfter << toQShared(patternOverlay); } { KisLayerStyleFilterProjectionPlane *stroke = new KisLayerStyleFilterProjectionPlane(sourceLayer); stroke->setStyle(new KisLsStrokeFilter(), style); m_d->stylesAfter << toQShared(stroke); } { KisLayerStyleFilterProjectionPlane *bevelEmboss = new KisLayerStyleFilterProjectionPlane(sourceLayer); bevelEmboss->setStyle(new KisLsBevelEmbossFilter(), style); m_d->stylesAfter << toQShared(bevelEmboss); } } KisLayerStyleProjectionPlane::~KisLayerStyleProjectionPlane() { } KisAbstractProjectionPlaneSP KisLayerStyleProjectionPlane::factoryObject(KisLayer *sourceLayer) { Q_ASSERT(sourceLayer); return toQShared(new KisLayerStyleProjectionPlane(sourceLayer)); } QRect KisLayerStyleProjectionPlane::recalculate(const QRect& rect, KisNodeSP filthyNode) { KisAbstractProjectionPlaneSP sourcePlane = m_d->sourceProjectionPlane.toStrongRef(); QRect result = sourcePlane->recalculate(rect, filthyNode); if (m_d->style->isEnabled()) { Q_FOREACH (const KisAbstractProjectionPlaneSP plane, m_d->stylesBefore) { plane->recalculate(rect, filthyNode); } Q_FOREACH (const KisAbstractProjectionPlaneSP plane, m_d->stylesAfter) { plane->recalculate(rect, filthyNode); } } return result; } void KisLayerStyleProjectionPlane::apply(KisPainter *painter, const QRect &rect) { KisAbstractProjectionPlaneSP sourcePlane = m_d->sourceProjectionPlane.toStrongRef(); if (m_d->style->isEnabled()) { Q_FOREACH (const KisAbstractProjectionPlaneSP plane, m_d->stylesBefore) { plane->apply(painter, rect); } sourcePlane->apply(painter, rect); Q_FOREACH (const KisAbstractProjectionPlaneSP plane, m_d->stylesAfter) { plane->apply(painter, rect); } } else { sourcePlane->apply(painter, rect); } } KisPaintDeviceList KisLayerStyleProjectionPlane::getLodCapableDevices() const { KisPaintDeviceList list; KisAbstractProjectionPlaneSP sourcePlane = m_d->sourceProjectionPlane.toStrongRef(); if (m_d->style->isEnabled()) { Q_FOREACH (const KisAbstractProjectionPlaneSP plane, m_d->stylesBefore) { list << plane->getLodCapableDevices(); } list << sourcePlane->getLodCapableDevices(); Q_FOREACH (const KisAbstractProjectionPlaneSP plane, m_d->stylesAfter) { list << plane->getLodCapableDevices(); } } else { list << sourcePlane->getLodCapableDevices(); } return list; } QRect KisLayerStyleProjectionPlane::needRect(const QRect &rect, KisLayer::PositionToFilthy pos) const { + /** + * Need rect should also be adjust for the layers that generate their 'original' + * based on the contents of the underlying/child layers like + * KisGroupLayer/KisAdjustmentLayer. + * + * \see bug 390299 + */ + KisAbstractProjectionPlaneSP sourcePlane = m_d->sourceProjectionPlane.toStrongRef(); - return sourcePlane->needRect(rect, pos); + const QRect layerNeedRect = sourcePlane->needRect(rect, pos); + QRect needRect = layerNeedRect; + + const bool dirtyGroupWithChildren = m_d->canHaveChildNodes && (pos & KisLayer::N_FILTHY); + const bool adjustmentAboveDirty = + m_d->canHaveChildNodes && + (pos & KisLayer::N_FILTHY || pos & KisLayer::N_ABOVE_FILTHY); + + if (m_d->style->isEnabled() && (dirtyGroupWithChildren || adjustmentAboveDirty)) { + + Q_FOREACH (const KisAbstractProjectionPlaneSP plane, m_d->stylesBefore) { + needRect |= plane->needRect(layerNeedRect, KisLayer::N_ABOVE_FILTHY); + } + + Q_FOREACH (const KisAbstractProjectionPlaneSP plane, m_d->stylesAfter) { + needRect |= plane->needRect(layerNeedRect, KisLayer::N_ABOVE_FILTHY); + } + } + + return needRect; } QRect KisLayerStyleProjectionPlane::changeRect(const QRect &rect, KisLayer::PositionToFilthy pos) const { KisAbstractProjectionPlaneSP sourcePlane = m_d->sourceProjectionPlane.toStrongRef(); QRect layerChangeRect = sourcePlane->changeRect(rect, pos); QRect changeRect = layerChangeRect; if (m_d->style->isEnabled()) { Q_FOREACH (const KisAbstractProjectionPlaneSP plane, m_d->stylesBefore) { changeRect |= plane->changeRect(layerChangeRect, KisLayer::N_ABOVE_FILTHY); } Q_FOREACH (const KisAbstractProjectionPlaneSP plane, m_d->stylesAfter) { changeRect |= plane->changeRect(layerChangeRect, KisLayer::N_ABOVE_FILTHY); } } return changeRect; } QRect KisLayerStyleProjectionPlane::accessRect(const QRect &rect, KisLayer::PositionToFilthy pos) const { KisAbstractProjectionPlaneSP sourcePlane = m_d->sourceProjectionPlane.toStrongRef(); QRect accessRect = sourcePlane->accessRect(rect, pos); if (m_d->style->isEnabled()) { Q_FOREACH (const KisAbstractProjectionPlaneSP plane, m_d->stylesBefore) { accessRect |= plane->accessRect(rect, KisLayer::N_ABOVE_FILTHY); } Q_FOREACH (const KisAbstractProjectionPlaneSP plane, m_d->stylesAfter) { accessRect |= plane->accessRect(rect, KisLayer::N_ABOVE_FILTHY); } } return accessRect; } diff --git a/libs/image/tests/kis_walkers_test.cpp b/libs/image/tests/kis_walkers_test.cpp index 9b110ec048..26eea93c21 100644 --- a/libs/image/tests/kis_walkers_test.cpp +++ b/libs/image/tests/kis_walkers_test.cpp @@ -1,1226 +1,1293 @@ /* * Copyright (c) 2009 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_walkers_test.h" #include "kis_base_rects_walker.h" #include "kis_refresh_subtree_walker.h" #include "kis_full_refresh_walker.h" #include #include #include #include "kis_paint_layer.h" #include "kis_group_layer.h" #include "kis_clone_layer.h" #include "kis_adjustment_layer.h" #include "kis_selection.h" #include "filter/kis_filter.h" #include "filter/kis_filter_configuration.h" #include "filter/kis_filter_registry.h" #include "kis_filter_mask.h" #include "kis_transparency_mask.h" -#define DEBUG_VISITORS +//#define DEBUG_VISITORS QString nodeTypeString(KisMergeWalker::NodePosition position); QString nodeTypePostfix(KisMergeWalker::NodePosition position); /************** Test Implementation Of A Walker *********************/ class KisTestWalker : public KisMergeWalker { public: KisTestWalker() :KisMergeWalker(QRect()) { } QStringList popResult() { QStringList order(m_order); m_order.clear(); return order; } using KisMergeWalker::startTrip; protected: void registerChangeRect(KisProjectionLeafSP node, NodePosition position) override { + Q_UNUSED(position); + QString postfix; if(!node->isLayer()) { postfix = "[skipped as not-a-layer]"; } #ifdef DEBUG_VISITORS - dbgKrita<< "FW:"<< node->node()->name() <<'\t'<< nodeTypeString(position) << postfix; + qDebug()<< "FW:"<< node->node()->name() <<'\t'<< nodeTypeString(position) << postfix; #endif if(postfix.isEmpty()) { m_order.append(node->node()->name()); } } void registerNeedRect(KisProjectionLeafSP node, NodePosition position) override { QString postfix; if(!node->isLayer()) { postfix = "[skipped as not-a-layer]"; } #ifdef DEBUG_VISITORS - dbgKrita<< "BW:"<< node->node()->name() <<'\t'<< nodeTypeString(position) << postfix; + qDebug()<< "BW:"<< node->node()->name() <<'\t'<< nodeTypeString(position) << postfix; #endif if(postfix.isEmpty()) { m_order.append(node->node()->name() + nodeTypePostfix(position)); } } protected: QStringList m_order; }; /************** Debug And Verify Code *******************************/ struct UpdateTestJob { QString updateAreaName; KisNodeSP startNode; QRect updateRect; QString referenceString; QRect accessRect; bool changeRectVaries; bool needRectVaries; }; void reportStartWith(QString nodeName, QRect rect = QRect()) { - dbgKrita; + qDebug(); if(!rect.isEmpty()) - dbgKrita << "Start with:" << nodeName << rect; + qDebug() << "Start with:" << nodeName << rect; else - dbgKrita << "Start with:" << nodeName; + qDebug() << "Start with:" << nodeName; } QString nodeTypeString(KisMergeWalker::NodePosition position) { QString string; if(position & KisMergeWalker::N_TOPMOST) string="TOP"; else if(position & KisMergeWalker::N_BOTTOMMOST) string="BOT"; else string="NOR"; if(position & KisMergeWalker::N_ABOVE_FILTHY) string+="_ABOVE "; else if(position & KisMergeWalker::N_FILTHY) string+="_FILTH*"; else if(position & KisMergeWalker::N_FILTHY_PROJECTION) string+="_PROJE*"; else if(position & KisMergeWalker::N_FILTHY_ORIGINAL) string+="_ORIGI*_WARNINIG!!!: NOT USED"; else if(position & KisMergeWalker::N_BELOW_FILTHY) string+="_BELOW "; else if(position & KisMergeWalker::N_EXTRA) string+="_EXTRA*"; else qFatal("Impossible happened"); return string; } QString nodeTypePostfix(KisMergeWalker::NodePosition position) { QString string('_'); if(position & KisMergeWalker::N_TOPMOST) string += 'T'; else if(position & KisMergeWalker::N_BOTTOMMOST) string += 'B'; else string += 'N'; if(position & KisMergeWalker::N_ABOVE_FILTHY) string += 'A'; else if(position & KisMergeWalker::N_FILTHY) string += 'F'; else if(position & KisMergeWalker::N_FILTHY_PROJECTION) string += 'P'; else if(position & KisMergeWalker::N_FILTHY_ORIGINAL) string += 'O'; else if(position & KisMergeWalker::N_BELOW_FILTHY) string += 'B'; else if(position & KisMergeWalker::N_EXTRA) string += 'E'; else qFatal("Impossible happened"); return string; } void KisWalkersTest::verifyResult(KisBaseRectsWalker &walker, struct UpdateTestJob &job) { QStringList list; if(!job.referenceString.isEmpty()) { list = job.referenceString.split(','); } verifyResult(walker, list, job.accessRect, job.changeRectVaries, job.needRectVaries); } void KisWalkersTest::verifyResult(KisBaseRectsWalker &walker, QStringList reference, QRect accessRect, bool changeRectVaries, bool needRectVaries) { KisMergeWalker::LeafStack &list = walker.leafStack(); QStringList::const_iterator iter = reference.constBegin(); if(reference.size() != list.size()) { - dbgKrita << "*** Seems like the walker returned stack of wrong size" + qDebug() << "*** Seems like the walker returned stack of wrong size" << "( ref:" << reference.size() << "act:" << list.size() << ")"; - dbgKrita << "*** We are going to crash soon... just wait..."; + qDebug() << "*** We are going to crash soon... just wait..."; } Q_FOREACH (const KisMergeWalker::JobItem &item, list) { #ifdef DEBUG_VISITORS - dbgKrita << item.m_leaf->node()->name() << '\t' + qDebug() << item.m_leaf->node()->name() << '\t' << item.m_applyRect << '\t' << nodeTypeString(item.m_position); #endif QCOMPARE(item.m_leaf->node()->name(), *iter); iter++; } #ifdef DEBUG_VISITORS - dbgKrita << "Result AR:\t" << walker.accessRect(); + qDebug() << "Result AR:\t" << walker.accessRect(); #endif QCOMPARE(walker.accessRect(), accessRect); QCOMPARE(walker.changeRectVaries(), changeRectVaries); QCOMPARE(walker.needRectVaries(), needRectVaries); } /************** Actual Testing **************************************/ /* +----------+ |root | | layer 5 | | group | | paint 4 | | paint 3 | | adj | | paint 2 | | paint 1 | +----------+ */ void KisWalkersTest::testUsualVisiting() { const KoColorSpace * colorSpace = KoColorSpaceRegistry::instance()->rgb8(); KisImageSP image = new KisImage(0, 512, 512, colorSpace, "walker test"); KisLayerSP paintLayer1 = new KisPaintLayer(image, "paint1", OPACITY_OPAQUE_U8); KisLayerSP paintLayer2 = new KisPaintLayer(image, "paint2", OPACITY_OPAQUE_U8); KisLayerSP paintLayer3 = new KisPaintLayer(image, "paint3", OPACITY_OPAQUE_U8); KisLayerSP paintLayer4 = new KisPaintLayer(image, "paint4", OPACITY_OPAQUE_U8); KisLayerSP paintLayer5 = new KisPaintLayer(image, "paint5", OPACITY_OPAQUE_U8); KisLayerSP groupLayer = new KisGroupLayer(image, "group", OPACITY_OPAQUE_U8); KisLayerSP adjustmentLayer = new KisAdjustmentLayer(image, "adj", 0, 0); image->addNode(paintLayer1, image->rootLayer()); image->addNode(groupLayer, image->rootLayer()); image->addNode(paintLayer5, image->rootLayer()); image->addNode(paintLayer2, groupLayer); image->addNode(adjustmentLayer, groupLayer); image->addNode(paintLayer3, groupLayer); image->addNode(paintLayer4, groupLayer); KisTestWalker walker; { QString order("paint3,paint4,group,paint5,root," "root_TF,paint5_TA,group_NF,paint1_BB," "paint4_TA,paint3_NF,adj_NB,paint2_BB"); QStringList orderList = order.split(','); reportStartWith("paint3"); walker.startTrip(paintLayer3->projectionLeaf()); QVERIFY(walker.popResult() == orderList); } { QString order("adj,paint3,paint4,group,paint5,root," "root_TF,paint5_TA,group_NF,paint1_BB," "paint4_TA,paint3_NA,adj_NF,paint2_BB"); QStringList orderList = order.split(','); reportStartWith("adj"); walker.startTrip(adjustmentLayer->projectionLeaf()); QVERIFY(walker.popResult() == orderList); } { QString order("group,paint5,root," "root_TF,paint5_TA,group_NF,paint1_BB"); QStringList orderList = order.split(','); reportStartWith("group"); walker.startTrip(groupLayer->projectionLeaf()); QVERIFY(walker.popResult() == orderList); } } /* +----------+ |root | | layer 5 | | group | | mask 1 | | paint 4 | | paint 3 | | adj | | paint 2 | | paint 1 | +----------+ */ void KisWalkersTest::testVisitingWithTopmostMask() { const KoColorSpace * colorSpace = KoColorSpaceRegistry::instance()->rgb8(); KisImageSP image = new KisImage(0, 512, 512, colorSpace, "walker test"); KisLayerSP paintLayer1 = new KisPaintLayer(image, "paint1", OPACITY_OPAQUE_U8); KisLayerSP paintLayer2 = new KisPaintLayer(image, "paint2", OPACITY_OPAQUE_U8); KisLayerSP paintLayer3 = new KisPaintLayer(image, "paint3", OPACITY_OPAQUE_U8); KisLayerSP paintLayer4 = new KisPaintLayer(image, "paint4", OPACITY_OPAQUE_U8); KisLayerSP paintLayer5 = new KisPaintLayer(image, "paint5", OPACITY_OPAQUE_U8); KisLayerSP groupLayer = new KisGroupLayer(image, "group", OPACITY_OPAQUE_U8); KisLayerSP adjustmentLayer = new KisAdjustmentLayer(image, "adj", 0, 0); KisFilterMaskSP filterMask1 = new KisFilterMask(); filterMask1->initSelection(groupLayer); KisFilterSP filter = KisFilterRegistry::instance()->value("blur"); Q_ASSERT(filter); KisFilterConfigurationSP configuration1 = filter->defaultConfiguration(); filterMask1->setFilter(configuration1); image->addNode(paintLayer1, image->rootLayer()); image->addNode(groupLayer, image->rootLayer()); image->addNode(paintLayer5, image->rootLayer()); image->addNode(paintLayer2, groupLayer); image->addNode(adjustmentLayer, groupLayer); image->addNode(paintLayer3, groupLayer); image->addNode(paintLayer4, groupLayer); // nasty mask! image->addNode(filterMask1, groupLayer); /** * The results must be the same as for testUsualVisiting */ KisTestWalker walker; { QString order("paint3,paint4,group,paint5,root," "root_TF,paint5_TA,group_NF,paint1_BB," "paint4_TA,paint3_NF,adj_NB,paint2_BB"); QStringList orderList = order.split(','); reportStartWith("paint3"); walker.startTrip(paintLayer3->projectionLeaf()); QVERIFY(walker.popResult() == orderList); } { QString order("adj,paint3,paint4,group,paint5,root," "root_TF,paint5_TA,group_NF,paint1_BB," "paint4_TA,paint3_NA,adj_NF,paint2_BB"); QStringList orderList = order.split(','); reportStartWith("adj"); walker.startTrip(adjustmentLayer->projectionLeaf()); QVERIFY(walker.popResult() == orderList); } { QString order("group,paint5,root," "root_TF,paint5_TA,group_NF,paint1_BB"); QStringList orderList = order.split(','); reportStartWith("group"); walker.startTrip(groupLayer->projectionLeaf()); QVERIFY(walker.popResult() == orderList); } } /* +----------+ |root | | layer 5 | | cplx 2 | | group | | paint 4 | | paint 3 | | cplx 1 | | paint 2 | | paint 1 | +----------+ */ void KisWalkersTest::testMergeVisiting() { const KoColorSpace * colorSpace = KoColorSpaceRegistry::instance()->rgb8(); KisImageSP image = new KisImage(0, 512, 512, colorSpace, "walker test"); KisLayerSP paintLayer1 = new KisPaintLayer(image, "paint1", OPACITY_OPAQUE_U8); KisLayerSP paintLayer2 = new KisPaintLayer(image, "paint2", OPACITY_OPAQUE_U8); KisLayerSP paintLayer3 = new KisPaintLayer(image, "paint3", OPACITY_OPAQUE_U8); KisLayerSP paintLayer4 = new KisPaintLayer(image, "paint4", OPACITY_OPAQUE_U8); KisLayerSP paintLayer5 = new KisPaintLayer(image, "paint5", OPACITY_OPAQUE_U8); KisLayerSP groupLayer = new KisGroupLayer(image, "group", OPACITY_OPAQUE_U8); KisLayerSP complexRectsLayer1 = new ComplexRectsLayer(image, "cplx1", OPACITY_OPAQUE_U8); KisLayerSP complexRectsLayer2 = new ComplexRectsLayer(image, "cplx2", OPACITY_OPAQUE_U8); image->addNode(paintLayer1, image->rootLayer()); image->addNode(groupLayer, image->rootLayer()); image->addNode(complexRectsLayer2, image->rootLayer()); image->addNode(paintLayer5, image->rootLayer()); image->addNode(paintLayer2, groupLayer); image->addNode(complexRectsLayer1, groupLayer); image->addNode(paintLayer3, groupLayer); image->addNode(paintLayer4, groupLayer); QRect testRect(10,10,10,10); // Empty rect to show we don't need any cropping QRect cropRect; KisMergeWalker walker(cropRect); { QString order("root,paint5,cplx2,group,paint1," "paint4,paint3,cplx1,paint2"); QStringList orderList = order.split(','); QRect accessRect(-7,-7,44,44); reportStartWith("paint3"); walker.collectRects(paintLayer3, testRect); verifyResult(walker, orderList, accessRect, true, true); } { QString order("root,paint5,cplx2,group,paint1," "paint4,paint3,cplx1,paint2"); QStringList orderList = order.split(','); QRect accessRect(-10,-10,50,50); reportStartWith("paint2"); walker.collectRects(paintLayer2, testRect); verifyResult(walker, orderList, accessRect, true, true); } { QString order("root,paint5,cplx2,group,paint1"); QStringList orderList = order.split(','); QRect accessRect(3,3,24,24); reportStartWith("paint5"); walker.collectRects(paintLayer5, testRect); verifyResult(walker, orderList, accessRect, false, true); } { /** * Test cropping */ QString order("root,paint5,cplx2,group,paint1," "paint4,paint3,cplx1,paint2"); QStringList orderList = order.split(','); QRect accessRect(0,0,40,40); reportStartWith("paint2 (with cropping)"); walker.setCropRect(image->bounds()); walker.collectRects(paintLayer2, testRect); walker.setCropRect(cropRect); verifyResult(walker, orderList, accessRect, true, true); } { /** * Test uncropped values */ QString order("root,paint5,cplx2,group,paint1," "paint4,paint3,cplx1,paint2"); QStringList orderList = order.split(','); QRect cropRect(9,9,12,12); QRect accessRect(cropRect); reportStartWith("paint2 (testing uncropped)"); walker.setCropRect(cropRect); walker.collectRects(paintLayer2, testRect); walker.setCropRect(cropRect); verifyResult(walker, orderList, accessRect, true, false); QCOMPARE(walker.uncroppedChangeRect(), QRect(4,4,22,22)); } } +#include "kis_psd_layer_style.h" + +/* + +----------+ + |root | + | group ls | + | paint 3 | + | paint 2 | + | paint 1 | + +----------+ + */ + +void KisWalkersTest::testComplexGroupVisiting() +{ + const KoColorSpace * colorSpace = KoColorSpaceRegistry::instance()->rgb8(); + KisImageSP image = new KisImage(0, 512, 512, colorSpace, "walker test"); + + + KisPSDLayerStyleSP style(new KisPSDLayerStyle()); + + { + style->context()->keep_original = true; + + style->dropShadow()->setEffectEnabled(true); + style->dropShadow()->setDistance(3); + style->dropShadow()->setSpread(1); + style->dropShadow()->setSize(7); + style->dropShadow()->setNoise(0); + style->dropShadow()->setKnocksOut(false); + style->dropShadow()->setOpacity(50); + style->dropShadow()->setAngle(0); + } + + KisLayerSP paintLayer1 = new KisPaintLayer(image, "paint1", OPACITY_OPAQUE_U8); + KisLayerSP paintLayer2 = new KisPaintLayer(image, "paint2", OPACITY_OPAQUE_U8); + KisLayerSP paintLayer3 = new KisPaintLayer(image, "paint3", OPACITY_OPAQUE_U8); + + KisLayerSP groupLayer = new KisGroupLayer(image, "groupls", OPACITY_OPAQUE_U8); + + image->addNode(paintLayer1, image->rootLayer()); + image->addNode(groupLayer, image->rootLayer()); + image->addNode(paintLayer2, groupLayer); + image->addNode(paintLayer3, groupLayer); + + groupLayer->setLayerStyle(style); + + QRect testRect(10,10,10,10); + // Empty rect to show we don't need any cropping + QRect cropRect; + + KisMergeWalker walker(cropRect); + + { + QString order("root,groupls,paint1," + "paint3,paint2"); + QStringList orderList = order.split(','); + QRect accessRect(-8,-8,46,46); + + reportStartWith("paint3"); + walker.collectRects(paintLayer3, testRect); + verifyResult(walker, orderList, accessRect, true, true); + } +} + + /* +------------+ |root | | layer 5 | | cplx 2 | | group | | paint 4 | | cplxacc 1 | | paint 3 | | cplx 1 | | paint 2 | | paint 1 | +------------+ */ void KisWalkersTest::testComplexAccessVisiting() { const KoColorSpace * colorSpace = KoColorSpaceRegistry::instance()->rgb8(); KisImageSP image = new KisImage(0, 512, 512, colorSpace, "walker test"); KisLayerSP paintLayer1 = new KisPaintLayer(image, "paint1", OPACITY_OPAQUE_U8); KisLayerSP paintLayer2 = new KisPaintLayer(image, "paint2", OPACITY_OPAQUE_U8); KisLayerSP paintLayer3 = new KisPaintLayer(image, "paint3", OPACITY_OPAQUE_U8); KisLayerSP paintLayer4 = new KisPaintLayer(image, "paint4", OPACITY_OPAQUE_U8); KisLayerSP paintLayer5 = new KisPaintLayer(image, "paint5", OPACITY_OPAQUE_U8); KisLayerSP groupLayer = new KisGroupLayer(image, "group", OPACITY_OPAQUE_U8); KisLayerSP complexRectsLayer1 = new ComplexRectsLayer(image, "cplx1", OPACITY_OPAQUE_U8); KisLayerSP complexRectsLayer2 = new ComplexRectsLayer(image, "cplx2", OPACITY_OPAQUE_U8); KisLayerSP complexAccess = new ComplexAccessLayer(image, "cplxacc1", OPACITY_OPAQUE_U8); image->addNode(paintLayer1, image->rootLayer()); image->addNode(groupLayer, image->rootLayer()); image->addNode(complexRectsLayer2, image->rootLayer()); image->addNode(paintLayer5, image->rootLayer()); image->addNode(paintLayer2, groupLayer); image->addNode(complexRectsLayer1, groupLayer); image->addNode(paintLayer3, groupLayer); image->addNode(complexAccess, groupLayer); image->addNode(paintLayer4, groupLayer); QRect testRect(10,10,10,10); // Empty rect to show we don't need any cropping QRect cropRect; KisMergeWalker walker(cropRect); { QString order("root,paint5,cplx2,group,paint1," "paint4,cplxacc1,paint3,cplx1,paint2"); QStringList orderList = order.split(','); QRect accessRect = QRect(-7,-7,44,44) | QRect(0,0,30,30).translated(70,0); reportStartWith("paint3"); walker.collectRects(paintLayer3, testRect); verifyResult(walker, orderList, accessRect, true, true); } } void KisWalkersTest::checkNotification(const KisMergeWalker::CloneNotification ¬ification, const QString &name, const QRect &rect) { QCOMPARE(notification.m_layer->name(), name); QCOMPARE(notification.m_dirtyRect, rect); } /* +--------------+ |root | | paint 3 <--+ | | cplx 2 | | | group <--+ | | | cplx 1 | | | | paint 2 | | | | clone 2 -+ | | | clone 1 ---+ | | paint 1 | +--------------+ */ void KisWalkersTest::testCloneNotificationsVisiting() { const KoColorSpace * colorSpace = KoColorSpaceRegistry::instance()->rgb8(); KisImageSP image = new KisImage(0, 512, 512, colorSpace, "walker test"); KisLayerSP paintLayer1 = new KisPaintLayer(image, "paint1", OPACITY_OPAQUE_U8); KisLayerSP paintLayer2 = new KisPaintLayer(image, "paint2", OPACITY_OPAQUE_U8); KisLayerSP paintLayer3 = new KisPaintLayer(image, "paint3", OPACITY_OPAQUE_U8); KisLayerSP groupLayer = new KisGroupLayer(image, "group", OPACITY_OPAQUE_U8); KisLayerSP complexRectsLayer1 = new ComplexRectsLayer(image, "cplx1", OPACITY_OPAQUE_U8); KisLayerSP complexRectsLayer2 = new ComplexRectsLayer(image, "cplx2", OPACITY_OPAQUE_U8); KisLayerSP cloneLayer1 = new KisCloneLayer(paintLayer3, image, "clone1", OPACITY_OPAQUE_U8); KisLayerSP cloneLayer2 = new KisCloneLayer(groupLayer, image, "clone2", OPACITY_OPAQUE_U8); image->addNode(paintLayer1, image->rootLayer()); image->addNode(cloneLayer1, image->rootLayer()); image->addNode(cloneLayer2, image->rootLayer()); image->addNode(groupLayer, image->rootLayer()); image->addNode(complexRectsLayer2, image->rootLayer()); image->addNode(paintLayer3, image->rootLayer()); image->addNode(paintLayer2, groupLayer); image->addNode(complexRectsLayer1, groupLayer); QRect testRect(10,10,10,10); QRect cropRect(5,5,507,507); KisMergeWalker walker(cropRect); { QString order("root,paint3,cplx2,group,clone2,clone1,paint1," "cplx1,paint2"); QStringList orderList = order.split(','); QRect accessRect = QRect(5,5,35,35); reportStartWith("paint2"); walker.collectRects(paintLayer2, testRect); verifyResult(walker, orderList, accessRect, true, true); const KisMergeWalker::CloneNotificationsVector vector = walker.cloneNotifications(); QCOMPARE(vector.size(), 1); checkNotification(vector[0], "group", QRect(7,7,16,16)); } } class TestingRefreshSubtreeWalker : public KisRefreshSubtreeWalker { public: TestingRefreshSubtreeWalker(QRect cropRect) : KisRefreshSubtreeWalker(cropRect) {} UpdateType type() const override { return FULL_REFRESH; } }; /* +----------+ |root | | layer 5 | | cplx 2 | | group | | paint 4 | | paint 3 | | cplx 1 | | paint 2 | | paint 1 | +----------+ */ void KisWalkersTest::testRefreshSubtreeVisiting() { const KoColorSpace * colorSpace = KoColorSpaceRegistry::instance()->rgb8(); KisImageSP image = new KisImage(0, 512, 512, colorSpace, "walker test"); KisLayerSP paintLayer1 = new KisPaintLayer(image, "paint1", OPACITY_OPAQUE_U8); KisLayerSP paintLayer2 = new KisPaintLayer(image, "paint2", OPACITY_OPAQUE_U8); KisLayerSP paintLayer3 = new KisPaintLayer(image, "paint3", OPACITY_OPAQUE_U8); KisLayerSP paintLayer4 = new KisPaintLayer(image, "paint4", OPACITY_OPAQUE_U8); KisLayerSP paintLayer5 = new KisPaintLayer(image, "paint5", OPACITY_OPAQUE_U8); KisLayerSP groupLayer = new KisGroupLayer(image, "group", OPACITY_OPAQUE_U8); KisLayerSP complexRectsLayer1 = new ComplexRectsLayer(image, "cplx1", OPACITY_OPAQUE_U8); KisLayerSP complexRectsLayer2 = new ComplexRectsLayer(image, "cplx2", OPACITY_OPAQUE_U8); image->addNode(paintLayer1, image->rootLayer()); image->addNode(groupLayer, image->rootLayer()); image->addNode(complexRectsLayer2, image->rootLayer()); image->addNode(paintLayer5, image->rootLayer()); image->addNode(paintLayer2, groupLayer); image->addNode(complexRectsLayer1, groupLayer); image->addNode(paintLayer3, groupLayer); image->addNode(paintLayer4, groupLayer); QRect testRect(10,10,10,10); // Empty rect to show we don't need any cropping QRect cropRect; TestingRefreshSubtreeWalker walker(cropRect); { QString order("root,paint5,cplx2,group,paint1," "paint4,paint3,cplx1,paint2"); QStringList orderList = order.split(','); QRect accessRect(-10,-10,50,50); reportStartWith("root"); walker.collectRects(image->rootLayer(), testRect); verifyResult(walker, orderList, accessRect, true, true); } } /* +----------+ |root | | layer 5 | | cplx 2 | | group | | paint 4 | | paint 3 | | cplx 1 | | paint 2 | | paint 1 | +----------+ */ void KisWalkersTest::testFullRefreshVisiting() { const KoColorSpace * colorSpace = KoColorSpaceRegistry::instance()->rgb8(); KisImageSP image = new KisImage(0, 512, 512, colorSpace, "walker test"); KisLayerSP paintLayer1 = new KisPaintLayer(image, "paint1", OPACITY_OPAQUE_U8); KisLayerSP paintLayer2 = new KisPaintLayer(image, "paint2", OPACITY_OPAQUE_U8); KisLayerSP paintLayer3 = new KisPaintLayer(image, "paint3", OPACITY_OPAQUE_U8); KisLayerSP paintLayer4 = new KisPaintLayer(image, "paint4", OPACITY_OPAQUE_U8); KisLayerSP paintLayer5 = new KisPaintLayer(image, "paint5", OPACITY_OPAQUE_U8); KisLayerSP groupLayer = new KisGroupLayer(image, "group", OPACITY_OPAQUE_U8); KisLayerSP complexRectsLayer1 = new ComplexRectsLayer(image, "cplx1", OPACITY_OPAQUE_U8); KisLayerSP complexRectsLayer2 = new ComplexRectsLayer(image, "cplx2", OPACITY_OPAQUE_U8); image->addNode(paintLayer1, image->rootLayer()); image->addNode(groupLayer, image->rootLayer()); image->addNode(complexRectsLayer2, image->rootLayer()); image->addNode(paintLayer5, image->rootLayer()); image->addNode(paintLayer2, groupLayer); image->addNode(complexRectsLayer1, groupLayer); image->addNode(paintLayer3, groupLayer); image->addNode(paintLayer4, groupLayer); QRect testRect(10,10,10,10); // Empty rect to show we don't need any cropping QRect cropRect; KisFullRefreshWalker walker(cropRect); { QString order("root,paint5,cplx2,group,paint1," "group,paint4,paint3,cplx1,paint2"); QStringList orderList = order.split(','); QRect accessRect(-10,-10,50,50); reportStartWith("root"); walker.collectRects(groupLayer, testRect); verifyResult(walker, orderList, accessRect, true, true); } } /* +----------+ |root | | layer 5 | | cache1 | | group | | paint 4 | | paint 3 | | paint 2 | | paint 1 | +----------+ */ void KisWalkersTest::testCachedVisiting() { const KoColorSpace * colorSpace = KoColorSpaceRegistry::instance()->rgb8(); KisImageSP image = new KisImage(0, 512, 512, colorSpace, "walker test"); KisLayerSP paintLayer1 = new KisPaintLayer(image, "paint1", OPACITY_OPAQUE_U8); KisLayerSP paintLayer2 = new KisPaintLayer(image, "paint2", OPACITY_OPAQUE_U8); KisLayerSP paintLayer3 = new KisPaintLayer(image, "paint3", OPACITY_OPAQUE_U8); KisLayerSP paintLayer4 = new KisPaintLayer(image, "paint4", OPACITY_OPAQUE_U8); KisLayerSP paintLayer5 = new KisPaintLayer(image, "paint5", OPACITY_OPAQUE_U8); KisLayerSP groupLayer = new KisGroupLayer(image, "group", OPACITY_OPAQUE_U8); KisLayerSP cacheLayer1 = new CacheLayer(image, "cache1", OPACITY_OPAQUE_U8); image->addNode(paintLayer1, image->rootLayer()); image->addNode(groupLayer, image->rootLayer()); image->addNode(cacheLayer1, image->rootLayer()); image->addNode(paintLayer5, image->rootLayer()); image->addNode(paintLayer2, groupLayer); image->addNode(paintLayer3, groupLayer); image->addNode(paintLayer4, groupLayer); QRect testRect(10,10,10,10); // Empty rect to show we don't need any cropping QRect cropRect; KisMergeWalker walker(cropRect); { QString order("root,paint5,cache1,group,paint1," "paint4,paint3,paint2"); QStringList orderList = order.split(','); QRect accessRect(0,0,30,30); reportStartWith("paint3"); walker.collectRects(paintLayer3, testRect); verifyResult(walker, orderList, accessRect, true, true); } { QString order("root,paint5,cache1"); QStringList orderList = order.split(','); QRect accessRect(10,10,10,10); reportStartWith("paint5"); walker.collectRects(paintLayer5, testRect); verifyResult(walker, orderList, accessRect, false, true); } } /* +----------+ |root | | paint 2 | | paint 1 | | fmask2 | | tmask | | fmask1 | +----------+ */ void KisWalkersTest::testMasksVisiting() { const KoColorSpace * colorSpace = KoColorSpaceRegistry::instance()->rgb8(); KisImageSP image = new KisImage(0, 512, 512, colorSpace, "walker test"); KisLayerSP paintLayer1 = new KisPaintLayer(image, "paint1", OPACITY_OPAQUE_U8); KisLayerSP paintLayer2 = new KisPaintLayer(image, "paint2", OPACITY_OPAQUE_U8); image->addNode(paintLayer1, image->rootLayer()); image->addNode(paintLayer2, image->rootLayer()); KisFilterMaskSP filterMask1 = new KisFilterMask(); KisFilterMaskSP filterMask2 = new KisFilterMask(); KisTransparencyMaskSP transparencyMask = new KisTransparencyMask(); KisFilterSP filter = KisFilterRegistry::instance()->value("blur"); Q_ASSERT(filter); KisFilterConfigurationSP configuration1 = filter->defaultConfiguration(); KisFilterConfigurationSP configuration2 = filter->defaultConfiguration(); filterMask1->setFilter(configuration1); filterMask2->setFilter(configuration2); QRect selection1(10, 10, 20, 10); QRect selection2(30, 15, 10, 10); QRect selection3(20, 10, 20, 10); filterMask1->testingInitSelection(selection1, paintLayer1); transparencyMask->testingInitSelection(selection2, paintLayer1); filterMask2->testingInitSelection(selection3, paintLayer1); image->addNode(filterMask1, paintLayer1); image->addNode(transparencyMask, paintLayer1); image->addNode(filterMask2, paintLayer1); QRect testRect(5,5,30,30); // Empty rect to show we don't need any cropping QRect cropRect; KisMergeWalker walker(cropRect); { QString order("root,paint2,paint1"); QStringList orderList = order.split(','); QRect accessRect(0,0,40,40); reportStartWith("tmask"); walker.collectRects(transparencyMask, testRect); verifyResult(walker, orderList, accessRect, true, false); } KisTestWalker twalker; { QString order("paint2,root," "root_TF,paint2_TA,paint1_BP"); QStringList orderList = order.split(','); reportStartWith("tmask"); twalker.startTrip(transparencyMask->projectionLeaf()); QCOMPARE(twalker.popResult(), orderList); } } /* +----------+ |root | | paint 2 | | paint 1 | | fmask2 | | tmask | | fmask1 | +----------+ */ void KisWalkersTest::testMasksVisitingNoFilthy() { const KoColorSpace * colorSpace = KoColorSpaceRegistry::instance()->rgb8(); KisImageSP image = new KisImage(0, 512, 512, colorSpace, "walker test"); KisLayerSP paintLayer1 = new KisPaintLayer(image, "paint1", OPACITY_OPAQUE_U8); KisLayerSP paintLayer2 = new KisPaintLayer(image, "paint2", OPACITY_OPAQUE_U8); image->addNode(paintLayer1, image->rootLayer()); image->addNode(paintLayer2, image->rootLayer()); KisFilterMaskSP filterMask1 = new KisFilterMask(); KisFilterMaskSP filterMask2 = new KisFilterMask(); KisTransparencyMaskSP transparencyMask = new KisTransparencyMask(); KisFilterSP filter = KisFilterRegistry::instance()->value("blur"); Q_ASSERT(filter); KisFilterConfigurationSP configuration1 = filter->defaultConfiguration(); KisFilterConfigurationSP configuration2 = filter->defaultConfiguration(); filterMask1->setFilter(configuration1); filterMask2->setFilter(configuration2); QRect selection1(10, 10, 20, 10); QRect selection2(30, 15, 10, 10); QRect selection3(20, 10, 20, 10); filterMask1->testingInitSelection(selection1, paintLayer1); transparencyMask->testingInitSelection(selection2, paintLayer1); filterMask2->testingInitSelection(selection3, paintLayer1); image->addNode(filterMask1, paintLayer1); image->addNode(transparencyMask, paintLayer1); image->addNode(filterMask2, paintLayer1); QRect testRect(5,5,30,30); // Empty rect to show we don't need any cropping QRect cropRect; { KisMergeWalker walker(cropRect, KisMergeWalker::NO_FILTHY); QString order("root,paint2,paint1"); QStringList orderList = order.split(','); QRect accessRect(0,0,40,40); reportStartWith("tmask"); walker.collectRects(transparencyMask, testRect); verifyResult(walker, orderList, accessRect, true, false); } { KisMergeWalker walker(cropRect, KisMergeWalker::NO_FILTHY); QString order("root,paint2,paint1"); QStringList orderList = order.split(','); QRect accessRect(5,5,30,30); reportStartWith("paint1"); walker.collectRects(paintLayer1, testRect); verifyResult(walker, orderList, accessRect, false, false); } } /* +----------+ |root | | paint 2 | | paint 1 | | fmask2 | | tmask | | fmask1 | +----------+ */ void KisWalkersTest::testMasksOverlapping() { const KoColorSpace * colorSpace = KoColorSpaceRegistry::instance()->rgb8(); KisImageSP image = new KisImage(0, 512, 512, colorSpace, "walker test"); KisLayerSP paintLayer1 = new KisPaintLayer(image, "paint1", OPACITY_OPAQUE_U8); KisLayerSP paintLayer2 = new KisPaintLayer(image, "paint2", OPACITY_OPAQUE_U8); image->addNode(paintLayer1, image->rootLayer()); image->addNode(paintLayer2, image->rootLayer()); KisFilterMaskSP filterMask1 = new KisFilterMask(); KisFilterMaskSP filterMask2 = new KisFilterMask(); KisTransparencyMaskSP transparencyMask = new KisTransparencyMask(); KisFilterSP blurFilter = KisFilterRegistry::instance()->value("blur"); KisFilterSP invertFilter = KisFilterRegistry::instance()->value("invert"); Q_ASSERT(blurFilter); Q_ASSERT(invertFilter); KisFilterConfigurationSP blurConfiguration = blurFilter->defaultConfiguration(); KisFilterConfigurationSP invertConfiguration = invertFilter->defaultConfiguration(); filterMask1->setFilter(invertConfiguration); filterMask2->setFilter(blurConfiguration); QRect selection1(0, 0, 128, 128); QRect selection2(128, 0, 128, 128); QRect selection3(0, 64, 256, 128); filterMask1->testingInitSelection(selection1, paintLayer1); transparencyMask->testingInitSelection(selection2, paintLayer1); filterMask2->testingInitSelection(selection3, paintLayer1); image->addNode(filterMask1, paintLayer1); image->addNode(transparencyMask, paintLayer1); image->addNode(filterMask2, paintLayer1); // Empty rect to show we don't need any cropping QRect cropRect; QRect IMRect(10,10,50,50); QRect TMRect(135,10,40,40); QRect IMTMRect(10,10,256,40); QList updateList; { // FIXME: now we do not crop change rect if COMPOSITE_OVER is used! UpdateTestJob job = {"IM", paintLayer1, IMRect, "", QRect(0,0,0,0), true, false}; updateList.append(job); } { UpdateTestJob job = {"IM", filterMask1, IMRect, "", QRect(0,0,0,0), true, false}; updateList.append(job); } { UpdateTestJob job = {"IM", transparencyMask, IMRect, "root,paint2,paint1", QRect(5,10,60,55), true, false}; updateList.append(job); } { UpdateTestJob job = {"IM", filterMask2, IMRect, "root,paint2,paint1", IMRect, false, false}; updateList.append(job); } /******* Dirty rect: transparency mask *********/ { UpdateTestJob job = {"TM", paintLayer1, TMRect, "root,paint2,paint1", TMRect, false, false}; updateList.append(job); } { UpdateTestJob job = {"TM", filterMask1, TMRect, "root,paint2,paint1", TMRect, false, false}; updateList.append(job); } { UpdateTestJob job = {"TM", transparencyMask, TMRect, "root,paint2,paint1", TMRect, false, false}; updateList.append(job); } { UpdateTestJob job = {"TM", filterMask2, TMRect, "root,paint2,paint1", TMRect, false, false}; updateList.append(job); } /******* Dirty rect: invert + transparency mask *********/ { UpdateTestJob job = {"IMTM", paintLayer1, IMTMRect, "root,paint2,paint1", IMTMRect & selection2, true, false}; updateList.append(job); } { UpdateTestJob job = {"IMTM", filterMask1, IMTMRect, "root,paint2,paint1", IMTMRect & selection2, true, false}; updateList.append(job); } { UpdateTestJob job = {"IMTM", transparencyMask, IMTMRect, "root,paint2,paint1", IMTMRect, false, false}; updateList.append(job); } { UpdateTestJob job = {"IMTM", filterMask2, IMTMRect, "root,paint2,paint1", IMTMRect, false, false}; updateList.append(job); } Q_FOREACH (UpdateTestJob job, updateList) { KisMergeWalker walker(cropRect); reportStartWith(job.startNode->name(), job.updateRect); - dbgKrita << "Area:" << job.updateAreaName; + qDebug() << "Area:" << job.updateAreaName; walker.collectRects(job.startNode, job.updateRect); verifyResult(walker, job); } } /* +----------+ |root | | adj | | paint 1 | +----------+ */ void KisWalkersTest::testRectsChecksum() { QRect imageRect(0,0,512,512); QRect dirtyRect(100,100,100,100); const KoColorSpace * colorSpace = KoColorSpaceRegistry::instance()->rgb8(); KisImageSP image = new KisImage(0, imageRect.width(), imageRect.height(), colorSpace, "walker test"); KisLayerSP paintLayer1 = new KisPaintLayer(image, "paint1", OPACITY_OPAQUE_U8); KisAdjustmentLayerSP adjustmentLayer = new KisAdjustmentLayer(image, "adj", 0, 0); image->lock(); image->addNode(paintLayer1, image->rootLayer()); image->addNode(adjustmentLayer, image->rootLayer()); image->unlock(); KisFilterSP filter = KisFilterRegistry::instance()->value("blur"); Q_ASSERT(filter); KisFilterConfigurationSP configuration; KisMergeWalker walker(imageRect); walker.collectRects(adjustmentLayer, dirtyRect); QCOMPARE(walker.checksumValid(), true); configuration = filter->defaultConfiguration(); adjustmentLayer->setFilter(configuration); QCOMPARE(walker.checksumValid(), false); walker.recalculate(dirtyRect); QCOMPARE(walker.checksumValid(), true); configuration = filter->defaultConfiguration(); configuration->setProperty("halfWidth", 20); configuration->setProperty("halfHeight", 20); adjustmentLayer->setFilter(configuration); QCOMPARE(walker.checksumValid(), false); walker.recalculate(dirtyRect); QCOMPARE(walker.checksumValid(), true); configuration = filter->defaultConfiguration(); configuration->setProperty("halfWidth", 21); configuration->setProperty("halfHeight", 21); adjustmentLayer->setFilter(configuration); QCOMPARE(walker.checksumValid(), false); walker.recalculate(dirtyRect); QCOMPARE(walker.checksumValid(), true); } void KisWalkersTest::testGraphStructureChecksum() { QRect imageRect(0,0,512,512); QRect dirtyRect(100,100,100,100); const KoColorSpace * colorSpace = KoColorSpaceRegistry::instance()->rgb8(); KisImageSP image = new KisImage(0, imageRect.width(), imageRect.height(), colorSpace, "walker test"); KisLayerSP paintLayer1 = new KisPaintLayer(image, "paint1", OPACITY_OPAQUE_U8); KisLayerSP paintLayer2 = new KisPaintLayer(image, "paint2", OPACITY_OPAQUE_U8); image->lock(); image->addNode(paintLayer1, image->rootLayer()); image->unlock(); KisMergeWalker walker(imageRect); walker.collectRects(paintLayer1, dirtyRect); QCOMPARE(walker.checksumValid(), true); image->lock(); image->addNode(paintLayer2, image->rootLayer()); image->unlock(); QCOMPARE(walker.checksumValid(), false); walker.recalculate(dirtyRect); QCOMPARE(walker.checksumValid(), true); image->lock(); image->moveNode(paintLayer1, image->rootLayer(), paintLayer2); image->unlock(); QCOMPARE(walker.checksumValid(), false); walker.recalculate(dirtyRect); QCOMPARE(walker.checksumValid(), true); image->lock(); image->removeNode(paintLayer1); image->unlock(); QCOMPARE(walker.checksumValid(), false); walker.recalculate(dirtyRect); QCOMPARE(walker.checksumValid(), true); } QTEST_MAIN(KisWalkersTest) diff --git a/libs/image/tests/kis_walkers_test.h b/libs/image/tests/kis_walkers_test.h index 5832b315bd..e39f36b84c 100644 --- a/libs/image/tests/kis_walkers_test.h +++ b/libs/image/tests/kis_walkers_test.h @@ -1,268 +1,269 @@ /* * Copyright (c) 2009 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_WALKERS_TEST_H #define KIS_WALKERS_TEST_H #include #include "kis_image.h" #include "kis_layer.h" #include "kis_types.h" #include "kis_node_visitor.h" #include "kis_default_bounds.h" #include "kis_paint_device.h" #include "kis_merge_walker.h" #include "kis_image.h" class TestLayer : public KisLayer { Q_OBJECT public: TestLayer(KisImageWSP image, const QString & name, quint8 opacity) : KisLayer(image, name, opacity) { } KisNodeSP clone() { return new TestLayer(*this); } bool allowAsChild(KisNodeSP) const override { return true; } virtual QString nodeType() { return "TEST"; } KisPaintDeviceSP original() const override { // This test doesn't use updateProjection so just return 0 return 0; } KisPaintDeviceSP paintDevice() const override { return 0; } QIcon icon() const override { return QIcon(); } KisNodeSP clone() const override { return new TestLayer(image(), name(), opacity()); } qint32 x() const override { return 0; } void setX(qint32) override { } qint32 y() const override { return 0; } void setY(qint32) override { } QRect extent() const override { return QRect(); } QRect exactBounds() const override { return QRect(); } using KisLayer::accept; bool accept(KisNodeVisitor& v) override { return v.visit(this); } }; class ComplexRectsLayer : public KisLayer { Q_OBJECT public: ComplexRectsLayer(KisImageWSP image, const QString & name, quint8 opacity) : KisLayer(image, name, opacity) { m_device = new KisPaintDevice(this, image->colorSpace(), new KisDefaultBounds(image), "test device"); } KisNodeSP clone() { return new ComplexRectsLayer(*this); } bool allowAsChild(KisNodeSP) const override { return true; } virtual QString nodeType() { return "TEST"; } KisPaintDeviceSP original() const override { return m_device; } KisPaintDeviceSP paintDevice() const override { return m_device; } QIcon icon() const override { return QIcon(); } KisNodeSP clone() const override { return new ComplexRectsLayer(image(), name(), opacity()); } qint32 x() const override { return 0; } void setX(qint32) override { } qint32 y() const override { return 0; } void setY(qint32) override { } QRect extent() const override { return QRect(); } QRect exactBounds() const override { return QRect(); } using KisLayer::accept; bool accept(KisNodeVisitor& v) override { return v.visit(this); } QRect changeRect(const QRect &rect, PositionToFilthy pos = N_FILTHY) const override { Q_UNUSED(pos); const qint32 delta = 3; return rect.adjusted(-delta, -delta, delta, delta); } QRect needRect(const QRect &rect, PositionToFilthy pos = N_FILTHY) const override { Q_UNUSED(pos); const qint32 delta = 7; return rect.adjusted(-delta, -delta, delta, delta); } private: KisPaintDeviceSP m_device; }; class CacheLayer : public ComplexRectsLayer { Q_OBJECT public: CacheLayer(KisImageWSP image, const QString & name, quint8 opacity) : ComplexRectsLayer(image, name, opacity) { } QRect needRect(const QRect &rect, PositionToFilthy pos = N_FILTHY) const override { QRect retval; if(pos != KisNode::N_BELOW_FILTHY && pos != N_FILTHY_PROJECTION) { const qint32 delta = 7; retval = rect.adjusted(-delta, -delta, delta, delta); } return retval; } }; class ComplexAccessLayer : public ComplexRectsLayer { Q_OBJECT public: ComplexAccessLayer(KisImageWSP image, const QString & name, quint8 opacity) : ComplexRectsLayer(image, name, opacity) { } QRect accessRect(const QRect &rect, PositionToFilthy pos = N_FILTHY) const override { Q_UNUSED(pos); const qint32 delta = 70; return rect.translated(delta, 0); } QRect needRect(const QRect &rect, PositionToFilthy pos = N_FILTHY) const override { Q_UNUSED(pos); return rect; } QRect changeRect(const QRect &rect, PositionToFilthy pos = N_FILTHY) const override { Q_UNUSED(pos); return rect; } }; class KisBaseRectsWalker; class KisWalkersTest : public QObject { Q_OBJECT private Q_SLOTS: void testUsualVisiting(); void testVisitingWithTopmostMask(); void testMergeVisiting(); + void testComplexGroupVisiting(); void testComplexAccessVisiting(); void testCloneNotificationsVisiting(); void testRefreshSubtreeVisiting(); void testFullRefreshVisiting(); void testCachedVisiting(); void testMasksVisiting(); void testMasksVisitingNoFilthy(); void testMasksOverlapping(); void testRectsChecksum(); void testGraphStructureChecksum(); private: void verifyResult(KisBaseRectsWalker &walker, QStringList reference, QRect accessRect, bool changeRectVaries, bool needRectVaries); void verifyResult(KisBaseRectsWalker &walker, struct UpdateTestJob &job); void checkNotification(const KisMergeWalker::CloneNotification ¬ification, const QString &name, const QRect &rect); }; #endif diff --git a/libs/ui/KisAsyncAnimationCacheRenderer.cpp b/libs/ui/KisAsyncAnimationCacheRenderer.cpp index b349baef23..1e49f3720a 100644 --- a/libs/ui/KisAsyncAnimationCacheRenderer.cpp +++ b/libs/ui/KisAsyncAnimationCacheRenderer.cpp @@ -1,76 +1,84 @@ /* * 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 "KisAsyncAnimationCacheRenderer.h" #include "kis_animation_frame_cache.h" #include "kis_update_info.h" struct KisAsyncAnimationCacheRenderer::Private { KisAnimationFrameCacheSP requestedCache; KisOpenGLUpdateInfoSP requestInfo; }; KisAsyncAnimationCacheRenderer::KisAsyncAnimationCacheRenderer() : m_d(new Private) { connect(this, SIGNAL(sigCompleteRegenerationInternal(int)), SLOT(slotCompleteRegenerationInternal(int))); } KisAsyncAnimationCacheRenderer::~KisAsyncAnimationCacheRenderer() { } void KisAsyncAnimationCacheRenderer::setFrameCache(KisAnimationFrameCacheSP cache) { m_d->requestedCache = cache; } void KisAsyncAnimationCacheRenderer::frameCompletedCallback(int frame) { KisAnimationFrameCacheSP cache = m_d->requestedCache; KisImageSP image = requestedImage(); if (!cache || !image) return; m_d->requestInfo = cache->fetchFrameData(frame, image); emit sigCompleteRegenerationInternal(frame); } void KisAsyncAnimationCacheRenderer::slotCompleteRegenerationInternal(int frame) { if (!isActive()) return; KIS_SAFE_ASSERT_RECOVER(m_d->requestInfo) { frameCancelledCallback(frame); return; } m_d->requestedCache->addConvertedFrameData(m_d->requestInfo, frame); notifyFrameCompleted(frame); } void KisAsyncAnimationCacheRenderer::frameCancelledCallback(int frame) { notifyFrameCancelled(frame); } +void KisAsyncAnimationCacheRenderer::clearFrameRegenerationState(bool isCancelled) +{ + m_d->requestInfo.clear(); + m_d->requestedCache.clear(); + + KisAsyncAnimationRendererBase::clearFrameRegenerationState(isCancelled); +} + diff --git a/libs/ui/KisAsyncAnimationCacheRenderer.h b/libs/ui/KisAsyncAnimationCacheRenderer.h index 4f359b88ff..9701203dfc 100644 --- a/libs/ui/KisAsyncAnimationCacheRenderer.h +++ b/libs/ui/KisAsyncAnimationCacheRenderer.h @@ -1,48 +1,50 @@ /* * 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 KISASYNCANIMATIONCACHERENDERER_H #define KISASYNCANIMATIONCACHERENDERER_H #include class KisAsyncAnimationCacheRenderer : public KisAsyncAnimationRendererBase { Q_OBJECT public: KisAsyncAnimationCacheRenderer(); ~KisAsyncAnimationCacheRenderer(); void setFrameCache(KisAnimationFrameCacheSP cache); protected: void frameCompletedCallback(int frame) override; void frameCancelledCallback(int frame) override; + void clearFrameRegenerationState(bool isCancelled) override; + Q_SIGNALS: void sigCompleteRegenerationInternal(int frame); private Q_SLOTS: void slotCompleteRegenerationInternal(int frame); private: struct Private; const QScopedPointer m_d; }; #endif // KISASYNCANIMATIONCACHERENDERER_H diff --git a/libs/ui/KisAsyncAnimationRendererBase.cpp b/libs/ui/KisAsyncAnimationRendererBase.cpp index d42dbb3c6e..4216a923b7 100644 --- a/libs/ui/KisAsyncAnimationRendererBase.cpp +++ b/libs/ui/KisAsyncAnimationRendererBase.cpp @@ -1,158 +1,160 @@ /* * 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 "KisAsyncAnimationRendererBase.h" #include #include #include "kis_image.h" #include "kis_image_animation_interface.h" #include "kis_signal_auto_connection.h" struct KisAsyncAnimationRendererBase::Private { KisSignalAutoConnectionsStore imageRequestConnections; QTimer regenerationTimeout; KisImageSP requestedImage; int requestedFrame = -1; bool isCancelled = false; static const int WAITING_FOR_FRAME_TIMEOUT = 10000; - - void clearFrameRegenerationState(bool); }; KisAsyncAnimationRendererBase::KisAsyncAnimationRendererBase(QObject *parent) : QObject(parent), m_d(new Private()) { connect(&m_d->regenerationTimeout, SIGNAL(timeout()), SLOT(slotFrameRegenerationCancelled())); m_d->regenerationTimeout.setSingleShot(true); m_d->regenerationTimeout.setInterval(Private::WAITING_FOR_FRAME_TIMEOUT); } KisAsyncAnimationRendererBase::~KisAsyncAnimationRendererBase() { } void KisAsyncAnimationRendererBase::startFrameRegeneration(KisImageSP image, int frame) { KIS_SAFE_ASSERT_RECOVER_NOOP(QThread::currentThread() == this->thread()); m_d->requestedImage = image; m_d->requestedFrame = frame; m_d->isCancelled = false; KisImageAnimationInterface *animation = m_d->requestedImage->animationInterface(); m_d->imageRequestConnections.clear(); m_d->imageRequestConnections.addConnection( animation, SIGNAL(sigFrameReady(int)), this, SLOT(slotFrameRegenerationFinished(int)), Qt::DirectConnection); m_d->imageRequestConnections.addConnection( animation, SIGNAL(sigFrameCancelled()), this, SLOT(slotFrameRegenerationCancelled()), Qt::AutoConnection); m_d->regenerationTimeout.start(); animation->requestFrameRegeneration(m_d->requestedFrame, image->bounds()); } bool KisAsyncAnimationRendererBase::isActive() const { return m_d->requestedImage; } void KisAsyncAnimationRendererBase::cancelCurrentFrameRendering() { KIS_SAFE_ASSERT_RECOVER_RETURN(m_d->requestedImage); frameCancelledCallback(m_d->requestedFrame); } -void KisAsyncAnimationRendererBase::Private::clearFrameRegenerationState(bool /*canceled*/) -{ - imageRequestConnections.clear(); - requestedImage = 0; - requestedFrame = -1; - regenerationTimeout.stop(); - isCancelled = true; -} - void KisAsyncAnimationRendererBase::slotFrameRegenerationCancelled() { // the timeout can arrive in async way if (!m_d->requestedImage) return; frameCancelledCallback(m_d->requestedFrame); } void KisAsyncAnimationRendererBase::slotFrameRegenerationFinished(int frame) { // We might have already cancelled the regeneration. We don't check // isCancelled flag here because this code runs asynchronously. if (!m_d->requestedImage) return; // WARNING: executed in the context of image worker thread! // probably a bit too strict... KIS_SAFE_ASSERT_RECOVER_NOOP(QThread::currentThread() != this->thread()); frameCompletedCallback(frame); } void KisAsyncAnimationRendererBase::notifyFrameCompleted(int frame) { KIS_SAFE_ASSERT_RECOVER_NOOP(QThread::currentThread() == this->thread()); // the image events can come with a delay, even after // the processing was cancelled if (m_d->isCancelled) return; KIS_SAFE_ASSERT_RECOVER_RETURN(m_d->requestedImage); KIS_SAFE_ASSERT_RECOVER_RETURN(m_d->requestedFrame == frame); - m_d->clearFrameRegenerationState(false); + clearFrameRegenerationState(false); emit sigFrameCompleted(frame); } void KisAsyncAnimationRendererBase::notifyFrameCancelled(int frame) { KIS_SAFE_ASSERT_RECOVER_NOOP(QThread::currentThread() == this->thread()); // the image events can come with a delay, even after // the processing was cancelled if (m_d->isCancelled) return; KIS_SAFE_ASSERT_RECOVER_RETURN(m_d->requestedImage); KIS_SAFE_ASSERT_RECOVER_RETURN(m_d->requestedFrame == frame); - m_d->clearFrameRegenerationState(true); + clearFrameRegenerationState(true); emit sigFrameCancelled(frame); } +void KisAsyncAnimationRendererBase::clearFrameRegenerationState(bool isCancelled) +{ + // TODO: for some reason we mark the process as cancelled in any case, and it + // seem to be a correct behavior + Q_UNUSED(isCancelled); + + m_d->imageRequestConnections.clear(); + m_d->requestedImage = 0; + m_d->requestedFrame = -1; + m_d->regenerationTimeout.stop(); + m_d->isCancelled = true; +} + KisImageSP KisAsyncAnimationRendererBase::requestedImage() const { return m_d->requestedImage; } diff --git a/libs/ui/KisAsyncAnimationRendererBase.h b/libs/ui/KisAsyncAnimationRendererBase.h index addd65b894..388d586b61 100644 --- a/libs/ui/KisAsyncAnimationRendererBase.h +++ b/libs/ui/KisAsyncAnimationRendererBase.h @@ -1,127 +1,136 @@ /* * 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 KISASYNCANIMATIONRENDERERBASE_H #define KISASYNCANIMATIONRENDERERBASE_H #include #include "kis_types.h" /** * KisAsyncAnimationRendererBase is a special class represinting a * single worker thread inside KisAsyncAnimationRenderDialogBase. It connects * the specified image using correct Qt::DirectConnection connections and * reacts on them. On sigFrameReady() signal it calls frameCompletedCallback(), * so the derived class can fetch a frame from the image and process it. On * sigFrameCancelled() it calls frameCancelledCallback(). The derived class * should override these two methods to do the actual work. */ class KisAsyncAnimationRendererBase : public QObject { Q_OBJECT public: explicit KisAsyncAnimationRendererBase(QObject *parent = 0); virtual ~KisAsyncAnimationRendererBase(); /** * Initiates the rendering of the frame \p frame on an image \p image */ virtual void startFrameRegeneration(KisImageSP image, int frame); /** * @return true if the regeneration process is in progress */ bool isActive() const; public Q_SLOTS: /** * @brief cancels current rendering operation * * After calling this slot requestedImage() becomes invalid. * @see requestedImage() */ void cancelCurrentFrameRendering(); Q_SIGNALS: void sigFrameCompleted(int frame); void sigFrameCancelled(int frame); private Q_SLOTS: void slotFrameRegenerationCancelled(); void slotFrameRegenerationFinished(int frame); protected Q_SLOTS: /** * Called by a derived class to continue processing of the frames */ void notifyFrameCompleted(int frame); /** * Called by a derived class to cancel processing of the frames. After calling * this method, the dialog will stop processing the frames and close. */ void notifyFrameCancelled(int frame); protected: /** * @brief frameCompletedCallback is called by the renderer when * a new frame becomes ready * * NOTE1: the callback is called from the context of a image * worker thread! So it is asynchronous from the GUI thread. * NOTE2: in case of successful processing of the frame, the callback * must issue some signal, connected to notifyFrameCompleted() * via auto connection, to continue processing. Please do not * call the method directly, because notifyFame*() slots should * be called from the context of the GUI thread. * NOTE3: In case of failure, notifyFrameCancelled(). The same threading * rules apply. */ virtual void frameCompletedCallback(int frame) = 0; /** * @brief frameCancelledCallback is called when the rendering of * the frame was cancelled. * * The rendering of the frame can be either cancelled by the image itself or * by receiving a timeout signal (10 seconds). * * NOTE: the slot is called in the GUI thread. Don't forget to call * notifyFrameCancelled() in he end of your call. */ virtual void frameCancelledCallback(int frame) = 0; + + /** + * Called by KisAsyncAnimationRendererBase when the processing has been completed + * and the internal state of the populator should be cleared + * + * @param isCancelled tells if frame regeneration has failed to be regenerated + */ + virtual void clearFrameRegenerationState(bool isCancelled); + protected: /** * @return the image that for which the rendering was requested using * startFrameRegeneration(). Should be used by the derived classes only. * * Please note that requestedImage() will become null as soon as the user * cancels the processing. That happends in the GUI thread so * frameCompletedCallback() should be extremely careful when requesting the * value (check the shared pointer after fetching). */ KisImageSP requestedImage() const; private: struct Private; const QScopedPointer m_d; }; #endif // KISASYNCANIMATIONRENDERERBASE_H diff --git a/libs/ui/dialogs/kis_dlg_layer_style.cpp b/libs/ui/dialogs/kis_dlg_layer_style.cpp index c7bd978f77..007ebc76cf 100644 --- a/libs/ui/dialogs/kis_dlg_layer_style.cpp +++ b/libs/ui/dialogs/kis_dlg_layer_style.cpp @@ -1,1445 +1,1451 @@ /* * Copyright (c) 2014 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_dlg_layer_style.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kis_config.h" #include "kis_cmb_contour.h" #include "kis_cmb_gradient.h" #include "kis_resource_server_provider.h" #include "kis_psd_layer_style_resource.h" #include "kis_psd_layer_style.h" #include "kis_signals_blocker.h" #include "kis_signal_compressor.h" #include "kis_canvas_resource_provider.h" #include KoAbstractGradient* fetchGradientLazy(KoAbstractGradient *gradient, KisCanvasResourceProvider *resourceProvider) { if (!gradient) { gradient = resourceProvider->currentGradient(); } return gradient; } KisDlgLayerStyle::KisDlgLayerStyle(KisPSDLayerStyleSP layerStyle, KisCanvasResourceProvider *resourceProvider, QWidget *parent) : KoDialog(parent) , m_layerStyle(layerStyle) , m_initialLayerStyle(layerStyle->clone()) , m_isSwitchingPredefinedStyle(false) , m_sanityLayerStyleDirty(false) { setCaption(i18n("Layer Styles")); setButtons(Ok | Cancel); setDefaultButton(Ok); m_configChangedCompressor = new KisSignalCompressor(1000, KisSignalCompressor::POSTPONE, this); connect(m_configChangedCompressor, SIGNAL(timeout()), SIGNAL(configChanged())); QWidget *page = new QWidget(this); wdgLayerStyles.setupUi(page); setMainWidget(page); connect(wdgLayerStyles.lstStyleSelector, SIGNAL(itemChanged(QListWidgetItem*)), SLOT(notifyGuiConfigChanged())); m_stylesSelector = new StylesSelector(this); connect(m_stylesSelector, SIGNAL(styleSelected(KisPSDLayerStyleSP)), SLOT(notifyPredefinedStyleSelected(KisPSDLayerStyleSP))); wdgLayerStyles.stylesStack->addWidget(m_stylesSelector); m_blendingOptions = new BlendingOptions(this); wdgLayerStyles.stylesStack->addWidget(m_blendingOptions); m_dropShadow = new DropShadow(DropShadow::DropShadowMode, this); wdgLayerStyles.stylesStack->addWidget(m_dropShadow); connect(m_dropShadow, SIGNAL(configChanged()), SLOT(notifyGuiConfigChanged())); m_innerShadow = new DropShadow(DropShadow::InnerShadowMode, this); wdgLayerStyles.stylesStack->addWidget(m_innerShadow); connect(m_innerShadow, SIGNAL(configChanged()), SLOT(notifyGuiConfigChanged())); m_outerGlow = new InnerGlow(InnerGlow::OuterGlowMode, resourceProvider, this); wdgLayerStyles.stylesStack->addWidget(m_outerGlow); connect(m_outerGlow, SIGNAL(configChanged()), SLOT(notifyGuiConfigChanged())); m_innerGlow = new InnerGlow(InnerGlow::InnerGlowMode, resourceProvider, this); wdgLayerStyles.stylesStack->addWidget(m_innerGlow); connect(m_innerGlow, SIGNAL(configChanged()), SLOT(notifyGuiConfigChanged())); m_contour = new Contour(this); m_texture = new Texture(this); m_bevelAndEmboss = new BevelAndEmboss(m_contour, m_texture, this); wdgLayerStyles.stylesStack->addWidget(m_bevelAndEmboss); wdgLayerStyles.stylesStack->addWidget(m_contour); wdgLayerStyles.stylesStack->addWidget(m_texture); connect(m_bevelAndEmboss, SIGNAL(configChanged()), SLOT(notifyGuiConfigChanged())); m_satin = new Satin(this); wdgLayerStyles.stylesStack->addWidget(m_satin); connect(m_satin, SIGNAL(configChanged()), SLOT(notifyGuiConfigChanged())); m_colorOverlay = new ColorOverlay(this); wdgLayerStyles.stylesStack->addWidget(m_colorOverlay); connect(m_colorOverlay, SIGNAL(configChanged()), SLOT(notifyGuiConfigChanged())); m_gradientOverlay = new GradientOverlay(resourceProvider, this); wdgLayerStyles.stylesStack->addWidget(m_gradientOverlay); connect(m_gradientOverlay, SIGNAL(configChanged()), SLOT(notifyGuiConfigChanged())); m_patternOverlay = new PatternOverlay(this); wdgLayerStyles.stylesStack->addWidget(m_patternOverlay); connect(m_patternOverlay, SIGNAL(configChanged()), SLOT(notifyGuiConfigChanged())); m_stroke = new Stroke(resourceProvider, this); wdgLayerStyles.stylesStack->addWidget(m_stroke); connect(m_stroke, SIGNAL(configChanged()), SLOT(notifyGuiConfigChanged())); KisConfig cfg; wdgLayerStyles.stylesStack->setCurrentIndex(cfg.readEntry("KisDlgLayerStyle::current", 1)); wdgLayerStyles.lstStyleSelector->setCurrentRow(cfg.readEntry("KisDlgLayerStyle::current", 1)); connect(wdgLayerStyles.lstStyleSelector, SIGNAL(currentItemChanged(QListWidgetItem*,QListWidgetItem*)), this, SLOT(changePage(QListWidgetItem*,QListWidgetItem*))); notifyPredefinedStyleSelected(layerStyle); connect(m_dropShadow, SIGNAL(globalAngleChanged(int)), SLOT(syncGlobalAngle(int))); connect(m_innerShadow, SIGNAL(globalAngleChanged(int)), SLOT(syncGlobalAngle(int))); connect(m_bevelAndEmboss, SIGNAL(globalAngleChanged(int)), SLOT(syncGlobalAngle(int))); connect(wdgLayerStyles.btnNewStyle, SIGNAL(clicked()), SLOT(slotNewStyle())); connect(wdgLayerStyles.btnLoadStyle, SIGNAL(clicked()), SLOT(slotLoadStyle())); connect(wdgLayerStyles.btnSaveStyle, SIGNAL(clicked()), SLOT(slotSaveStyle())); connect(wdgLayerStyles.chkMasterFxSwitch, SIGNAL(toggled(bool)), SLOT(slotMasterFxSwitchChanged(bool))); connect(this, SIGNAL(accepted()), SLOT(slotNotifyOnAccept())); connect(this, SIGNAL(rejected()), SLOT(slotNotifyOnReject())); } KisDlgLayerStyle::~KisDlgLayerStyle() { } void KisDlgLayerStyle::slotMasterFxSwitchChanged(bool value) { wdgLayerStyles.lstStyleSelector->setEnabled(value); wdgLayerStyles.stylesStack->setEnabled(value); wdgLayerStyles.btnNewStyle->setEnabled(value); wdgLayerStyles.btnLoadStyle->setEnabled(value); wdgLayerStyles.btnSaveStyle->setEnabled(value); notifyGuiConfigChanged(); } void KisDlgLayerStyle::notifyGuiConfigChanged() { if (m_isSwitchingPredefinedStyle) return; m_configChangedCompressor->start(); m_layerStyle->setUuid(QUuid::createUuid()); m_sanityLayerStyleDirty = true; m_stylesSelector->notifyExternalStyleChanged(m_layerStyle->name(), m_layerStyle->uuid()); } void KisDlgLayerStyle::notifyPredefinedStyleSelected(KisPSDLayerStyleSP style) { m_isSwitchingPredefinedStyle = true; setStyle(style); m_isSwitchingPredefinedStyle = false; m_configChangedCompressor->start(); } void KisDlgLayerStyle::slotNotifyOnAccept() { if (m_configChangedCompressor->isActive()) { m_configChangedCompressor->stop(); emit configChanged(); } } void KisDlgLayerStyle::slotNotifyOnReject() { notifyPredefinedStyleSelected(m_initialLayerStyle); m_configChangedCompressor->stop(); emit configChanged(); } bool checkCustomNameAvailable(const QString &name) { const QString customName = "CustomStyles.asl"; KoResourceServer *server = KisResourceServerProvider::instance()->layerStyleCollectionServer(); KoResource *resource = server->resourceByName(customName); if (!resource) return true; KisPSDLayerStyleCollectionResource *collection = dynamic_cast(resource); Q_FOREACH (KisPSDLayerStyleSP style, collection->layerStyles()) { if (style->name() == name) { return false; } } return true; } QString selectAvailableStyleName(const QString &name) { QString finalName = name; if (checkCustomNameAvailable(finalName)) { return finalName; } int i = 0; do { finalName = QString("%1%2").arg(name).arg(i++); } while (!checkCustomNameAvailable(finalName)); return finalName; } void KisDlgLayerStyle::slotNewStyle() { QString styleName = QInputDialog::getText(this, i18nc("@title:window", "Enter new style name"), i18nc("@label:textbox", "Name:"), QLineEdit::Normal, i18nc("Default name for a new style", "New Style")); KisPSDLayerStyleSP style = this->style(); style->setName(selectAvailableStyleName(styleName)); m_stylesSelector->addNewStyle(style->clone()); } void KisDlgLayerStyle::slotLoadStyle() { QString filename; // default value? KoFileDialog dialog(this, KoFileDialog::OpenFile, "layerstyle"); dialog.setCaption(i18n("Select ASL file")); dialog.setMimeTypeFilters(QStringList() << "application/x-photoshop-style-library", "application/x-photoshop-style-library"); filename = dialog.filename(); m_stylesSelector->loadCollection(filename); wdgLayerStyles.lstStyleSelector->setCurrentRow(0); } void KisDlgLayerStyle::slotSaveStyle() { QString filename; // default value? KoFileDialog dialog(this, KoFileDialog::SaveFile, "layerstyle"); dialog.setCaption(i18n("Select ASL file")); dialog.setMimeTypeFilters(QStringList() << "application/x-photoshop-style-library", "application/x-photoshop-style-library"); filename = dialog.filename(); QScopedPointer collection( new KisPSDLayerStyleCollectionResource(filename)); KisPSDLayerStyleSP newStyle = style()->clone(); newStyle->setName(QFileInfo(filename).baseName()); KisPSDLayerStyleCollectionResource::StylesVector vector = collection->layerStyles(); vector << newStyle; collection->setLayerStyles(vector); collection->save(); } void KisDlgLayerStyle::changePage(QListWidgetItem *current, QListWidgetItem *previous) { if (!current) { current = previous; } wdgLayerStyles.stylesStack->setCurrentIndex(wdgLayerStyles.lstStyleSelector->row(current)); } void KisDlgLayerStyle::setStyle(KisPSDLayerStyleSP style) { - *m_layerStyle = *style; + // we may self-assign style is some cases + if (style != m_layerStyle) { + *m_layerStyle = *style; + } m_sanityLayerStyleDirty = false; - m_stylesSelector->notifyExternalStyleChanged(m_layerStyle->name(), m_layerStyle->uuid()); + { + KisSignalsBlocker b(m_stylesSelector); + m_stylesSelector->notifyExternalStyleChanged(m_layerStyle->name(), m_layerStyle->uuid()); + } QListWidgetItem *item; item = wdgLayerStyles.lstStyleSelector->item(2); item->setCheckState(m_layerStyle->dropShadow()->effectEnabled() ? Qt::Checked : Qt::Unchecked); item = wdgLayerStyles.lstStyleSelector->item(3); item->setCheckState(m_layerStyle->innerShadow()->effectEnabled() ? Qt::Checked : Qt::Unchecked); item = wdgLayerStyles.lstStyleSelector->item(4); item->setCheckState(m_layerStyle->outerGlow()->effectEnabled() ? Qt::Checked : Qt::Unchecked); item = wdgLayerStyles.lstStyleSelector->item(5); item->setCheckState(m_layerStyle->innerGlow()->effectEnabled() ? Qt::Checked : Qt::Unchecked); item = wdgLayerStyles.lstStyleSelector->item(6); item->setCheckState(m_layerStyle->bevelAndEmboss()->effectEnabled() ? Qt::Checked : Qt::Unchecked); item = wdgLayerStyles.lstStyleSelector->item(7); item->setCheckState(m_layerStyle->bevelAndEmboss()->contourEnabled() ? Qt::Checked : Qt::Unchecked); item = wdgLayerStyles.lstStyleSelector->item(8); item->setCheckState(m_layerStyle->bevelAndEmboss()->textureEnabled() ? Qt::Checked : Qt::Unchecked); item = wdgLayerStyles.lstStyleSelector->item(9); item->setCheckState(m_layerStyle->satin()->effectEnabled() ? Qt::Checked : Qt::Unchecked); item = wdgLayerStyles.lstStyleSelector->item(10); item->setCheckState(m_layerStyle->colorOverlay()->effectEnabled() ? Qt::Checked : Qt::Unchecked); item = wdgLayerStyles.lstStyleSelector->item(11); item->setCheckState(m_layerStyle->gradientOverlay()->effectEnabled() ? Qt::Checked : Qt::Unchecked); item = wdgLayerStyles.lstStyleSelector->item(12); item->setCheckState(m_layerStyle->patternOverlay()->effectEnabled() ? Qt::Checked : Qt::Unchecked); item = wdgLayerStyles.lstStyleSelector->item(13); item->setCheckState(m_layerStyle->stroke()->effectEnabled() ? Qt::Checked : Qt::Unchecked); m_dropShadow->setShadow(m_layerStyle->dropShadow()); m_innerShadow->setShadow(m_layerStyle->innerShadow()); m_outerGlow->setConfig(m_layerStyle->outerGlow()); m_innerGlow->setConfig(m_layerStyle->innerGlow()); m_bevelAndEmboss->setBevelAndEmboss(m_layerStyle->bevelAndEmboss()); m_satin->setSatin(m_layerStyle->satin()); m_colorOverlay->setColorOverlay(m_layerStyle->colorOverlay()); m_gradientOverlay->setGradientOverlay(m_layerStyle->gradientOverlay()); m_patternOverlay->setPatternOverlay(m_layerStyle->patternOverlay()); m_stroke->setStroke(m_layerStyle->stroke()); wdgLayerStyles.chkMasterFxSwitch->setChecked(m_layerStyle->isEnabled()); slotMasterFxSwitchChanged(m_layerStyle->isEnabled()); } KisPSDLayerStyleSP KisDlgLayerStyle::style() const { m_layerStyle->setEnabled(wdgLayerStyles.chkMasterFxSwitch->isChecked()); m_layerStyle->dropShadow()->setEffectEnabled(wdgLayerStyles.lstStyleSelector->item(2)->checkState() == Qt::Checked); m_layerStyle->innerShadow()->setEffectEnabled(wdgLayerStyles.lstStyleSelector->item(3)->checkState() == Qt::Checked); m_layerStyle->outerGlow()->setEffectEnabled(wdgLayerStyles.lstStyleSelector->item(4)->checkState() == Qt::Checked); m_layerStyle->innerGlow()->setEffectEnabled(wdgLayerStyles.lstStyleSelector->item(5)->checkState() == Qt::Checked); m_layerStyle->bevelAndEmboss()->setEffectEnabled(wdgLayerStyles.lstStyleSelector->item(6)->checkState() == Qt::Checked); m_layerStyle->bevelAndEmboss()->setContourEnabled(wdgLayerStyles.lstStyleSelector->item(7)->checkState() == Qt::Checked); m_layerStyle->bevelAndEmboss()->setTextureEnabled(wdgLayerStyles.lstStyleSelector->item(8)->checkState() == Qt::Checked); m_layerStyle->satin()->setEffectEnabled(wdgLayerStyles.lstStyleSelector->item(9)->checkState() == Qt::Checked); m_layerStyle->colorOverlay()->setEffectEnabled(wdgLayerStyles.lstStyleSelector->item(10)->checkState() == Qt::Checked); m_layerStyle->gradientOverlay()->setEffectEnabled(wdgLayerStyles.lstStyleSelector->item(11)->checkState() == Qt::Checked); m_layerStyle->patternOverlay()->setEffectEnabled(wdgLayerStyles.lstStyleSelector->item(12)->checkState() == Qt::Checked); m_layerStyle->stroke()->setEffectEnabled(wdgLayerStyles.lstStyleSelector->item(13)->checkState() == Qt::Checked); m_dropShadow->fetchShadow(m_layerStyle->dropShadow()); m_innerShadow->fetchShadow(m_layerStyle->innerShadow()); m_outerGlow->fetchConfig(m_layerStyle->outerGlow()); m_innerGlow->fetchConfig(m_layerStyle->innerGlow()); m_bevelAndEmboss->fetchBevelAndEmboss(m_layerStyle->bevelAndEmboss()); m_satin->fetchSatin(m_layerStyle->satin()); m_colorOverlay->fetchColorOverlay(m_layerStyle->colorOverlay()); m_gradientOverlay->fetchGradientOverlay(m_layerStyle->gradientOverlay()); m_patternOverlay->fetchPatternOverlay(m_layerStyle->patternOverlay()); m_stroke->fetchStroke(m_layerStyle->stroke()); m_sanityLayerStyleDirty = false; m_stylesSelector->notifyExternalStyleChanged(m_layerStyle->name(), m_layerStyle->uuid()); return m_layerStyle; } void KisDlgLayerStyle::syncGlobalAngle(int angle) { KisPSDLayerStyleSP style = this->style(); if (style->dropShadow()->useGlobalLight()) { style->dropShadow()->setAngle(angle); } if (style->innerShadow()->useGlobalLight()) { style->innerShadow()->setAngle(angle); } if (style->bevelAndEmboss()->useGlobalLight()) { style->bevelAndEmboss()->setAngle(angle); } setStyle(style); } /********************************************************************/ /***** Styles Selector **********************************************/ /********************************************************************/ class StyleItem : public QListWidgetItem { public: StyleItem(KisPSDLayerStyleSP style) : QListWidgetItem(style->name()) , m_style(style) { } public: KisPSDLayerStyleSP m_style; }; StylesSelector::StylesSelector(QWidget *parent) : QWidget(parent) { ui.setupUi(this); connect(ui.cmbStyleCollections, SIGNAL(activated(QString)), this, SLOT(loadStyles(QString))); connect(ui.listStyles, SIGNAL(currentItemChanged(QListWidgetItem*,QListWidgetItem*)), this, SLOT(selectStyle(QListWidgetItem*,QListWidgetItem*))); refillCollections(); if (ui.cmbStyleCollections->count()) { ui.cmbStyleCollections->setCurrentIndex(0); loadStyles(ui.cmbStyleCollections->currentText()); } } void StylesSelector::refillCollections() { QString previousCollection = ui.cmbStyleCollections->currentText(); ui.cmbStyleCollections->clear(); Q_FOREACH (KoResource *res, KisResourceServerProvider::instance()->layerStyleCollectionServer()->resources()) { ui.cmbStyleCollections->addItem(res->name()); } if (!previousCollection.isEmpty()) { KisSignalsBlocker blocker(this); int index = ui.cmbStyleCollections->findText(previousCollection); ui.cmbStyleCollections->setCurrentIndex(index); } } void StylesSelector::notifyExternalStyleChanged(const QString &name, const QUuid &uuid) { int currentIndex = -1; for (int i = 0; i < ui.listStyles->count(); i++ ) { StyleItem *item = dynamic_cast(ui.listStyles->item(i)); QString itemName = item->m_style->name(); if (itemName == name) { bool isDirty = item->m_style->uuid() != uuid; if (isDirty) { itemName += "*"; } currentIndex = i; } item->setText(itemName); } ui.listStyles->setCurrentRow(currentIndex); } void StylesSelector::loadStyles(const QString &name) { ui.listStyles->clear(); KoResource *res = KisResourceServerProvider::instance()->layerStyleCollectionServer()->resourceByName(name); KisPSDLayerStyleCollectionResource *collection = dynamic_cast(res); if (collection) { Q_FOREACH (KisPSDLayerStyleSP style, collection->layerStyles()) { // XXX: also use the preview image, when we have one ui.listStyles->addItem(new StyleItem(style)); } } } void StylesSelector::selectStyle(QListWidgetItem *current, QListWidgetItem* /*previous*/) { StyleItem *item = dynamic_cast(current); if (item) { emit styleSelected(item->m_style); } } void StylesSelector::loadCollection(const QString &fileName) { if (!QFileInfo(fileName).exists()) { warnKrita << "Loaded style collection doesn't exist!"; return; } KisPSDLayerStyleCollectionResource *collection = new KisPSDLayerStyleCollectionResource(fileName); collection->load(); KoResourceServer *server = KisResourceServerProvider::instance()->layerStyleCollectionServer(); collection->setFilename(server->saveLocation() + QDir::separator() + collection->name()); server->addResource(collection); refillCollections(); int index = ui.cmbStyleCollections->findText(collection->name()); ui.cmbStyleCollections->setCurrentIndex(index); loadStyles(collection->name()); } void StylesSelector::addNewStyle(KisPSDLayerStyleSP style) { KoResourceServer *server = KisResourceServerProvider::instance()->layerStyleCollectionServer(); // NOTE: not translatable, since it is a key! const QString customName = "CustomStyles.asl"; const QString saveLocation = server->saveLocation(); const QString fullFilename = saveLocation + customName; KoResource *resource = server->resourceByName(customName); KisPSDLayerStyleCollectionResource *collection = 0; if (!resource) { collection = new KisPSDLayerStyleCollectionResource(""); collection->setName(customName); collection->setFilename(fullFilename); KisPSDLayerStyleCollectionResource::StylesVector vector; vector << style; collection->setLayerStyles(vector); server->addResource(collection); } else { collection = dynamic_cast(resource); KisPSDLayerStyleCollectionResource::StylesVector vector; vector = collection->layerStyles(); vector << style; collection->setLayerStyles(vector); collection->save(); } refillCollections(); // select in gui int index = ui.cmbStyleCollections->findText(customName); KIS_ASSERT_RECOVER_RETURN(index >= 0); ui.cmbStyleCollections->setCurrentIndex(index); loadStyles(customName); notifyExternalStyleChanged(style->name(), style->uuid()); } /********************************************************************/ /***** Bevel and Emboss *********************************************/ /********************************************************************/ BevelAndEmboss::BevelAndEmboss(Contour *contour, Texture *texture, QWidget *parent) : QWidget(parent) , m_contour(contour) , m_texture(texture) { ui.setupUi(this); // Structure ui.intDepth->setRange(0, 100); ui.intDepth->setSuffix(i18n(" %")); ui.intSize->setRange(0, 250); ui.intSize->setSuffix(i18n(" px")); ui.intSoften->setRange(0, 18); ui.intSoften->setSuffix(i18n(" px")); connect(ui.cmbStyle, SIGNAL(currentIndexChanged(int)), SIGNAL(configChanged())); connect(ui.cmbTechnique, SIGNAL(currentIndexChanged(int)), SIGNAL(configChanged())); connect(ui.intDepth, SIGNAL(valueChanged(int)), SIGNAL(configChanged())); connect(ui.cmbDirection, SIGNAL(currentIndexChanged(int)), SIGNAL(configChanged())); connect(ui.intSize, SIGNAL(valueChanged(int)), SIGNAL(configChanged())); connect(ui.intSoften, SIGNAL(valueChanged(int)), SIGNAL(configChanged())); // Shading ui.intOpacity->setRange(0, 100); ui.intOpacity->setSuffix(i18n(" %")); ui.intOpacity2->setRange(0, 100); ui.intOpacity2->setSuffix(i18n(" %")); connect(ui.dialAngle, SIGNAL(valueChanged(int)), SLOT(slotDialAngleChanged(int))); connect(ui.intAngle, SIGNAL(valueChanged(int)), SLOT(slotIntAngleChanged(int))); connect(ui.chkUseGlobalLight, SIGNAL(toggled(bool)), SLOT(slotGlobalLightToggled())); connect(ui.dialAngle, SIGNAL(valueChanged(int)), SIGNAL(configChanged())); connect(ui.intAngle, SIGNAL(valueChanged(int)), SIGNAL(configChanged())); connect(ui.chkUseGlobalLight, SIGNAL(toggled(bool)), SIGNAL(configChanged())); connect(ui.intAltitude, SIGNAL(valueChanged(int)), SIGNAL(configChanged())); connect(ui.cmbContour, SIGNAL(currentIndexChanged(int)), SIGNAL(configChanged())); connect(ui.chkAntiAliased, SIGNAL(toggled(bool)), SIGNAL(configChanged())); connect(ui.cmbHighlightMode, SIGNAL(currentIndexChanged(int)), SIGNAL(configChanged())); connect(ui.bnHighlightColor, SIGNAL(changed(KoColor)), SIGNAL(configChanged())); connect(ui.intOpacity, SIGNAL(valueChanged(int)), SIGNAL(configChanged())); connect(ui.cmbShadowMode, SIGNAL(currentIndexChanged(int)), SIGNAL(configChanged())); connect(ui.bnShadowColor, SIGNAL(changed(KoColor)), SIGNAL(configChanged())); connect(ui.intOpacity2, SIGNAL(valueChanged(int)), SIGNAL(configChanged()));; // Contour m_contour->ui.intRange->setRange(1, 100); m_contour->ui.intRange->setSuffix(i18n(" %")); connect(m_contour->ui.cmbContour, SIGNAL(currentIndexChanged(int)), SIGNAL(configChanged())); connect(m_contour->ui.chkAntiAliased, SIGNAL(toggled(bool)), SIGNAL(configChanged())); connect(m_contour->ui.intRange, SIGNAL(valueChanged(int)), SIGNAL(configChanged())); // Texture m_texture->ui.intScale->setRange(0, 100); m_texture->ui.intScale->setSuffix(i18n(" %")); m_texture->ui.intDepth->setRange(-1000, 1000); m_texture->ui.intDepth->setSuffix(i18n(" %")); connect(m_texture->ui.patternChooser, SIGNAL(resourceSelected(KoResource*)), SIGNAL(configChanged())); connect(m_texture->ui.intScale, SIGNAL(valueChanged(int)), SIGNAL(configChanged())); connect(m_texture->ui.intDepth, SIGNAL(valueChanged(int)), SIGNAL(configChanged())); connect(m_texture->ui.chkInvert, SIGNAL(toggled(bool)), SIGNAL(configChanged())); connect(m_texture->ui.chkLinkWithLayer, SIGNAL(toggled(bool)), SIGNAL(configChanged())); } void BevelAndEmboss::setBevelAndEmboss(const psd_layer_effects_bevel_emboss *bevelAndEmboss) { ui.cmbStyle->setCurrentIndex((int)bevelAndEmboss->style()); ui.cmbTechnique->setCurrentIndex((int)bevelAndEmboss->technique()); ui.intDepth->setValue(bevelAndEmboss->depth()); ui.cmbDirection->setCurrentIndex((int)bevelAndEmboss->direction()); ui.intSize->setValue(bevelAndEmboss->size()); ui.intSoften->setValue(bevelAndEmboss->soften()); ui.dialAngle->setValue(bevelAndEmboss->angle()); ui.intAngle->setValue(bevelAndEmboss->angle()); ui.chkUseGlobalLight->setChecked(bevelAndEmboss->useGlobalLight()); ui.intAltitude->setValue(bevelAndEmboss->altitude()); // FIXME: curve editing // ui.cmbContour; ui.chkAntiAliased->setChecked(bevelAndEmboss->glossAntiAliased()); ui.cmbHighlightMode->selectCompositeOp(KoID(bevelAndEmboss->highlightBlendMode())); KoColor highlightshadow(KoColorSpaceRegistry::instance()->rgb8()); highlightshadow.fromQColor(bevelAndEmboss->highlightColor()); ui.bnHighlightColor->setColor(highlightshadow); ui.intOpacity->setValue(bevelAndEmboss->highlightOpacity()); ui.cmbShadowMode->selectCompositeOp(KoID(bevelAndEmboss->shadowBlendMode())); highlightshadow.fromQColor(bevelAndEmboss->shadowColor()); ui.bnShadowColor->setColor(highlightshadow); ui.intOpacity2->setValue(bevelAndEmboss->shadowOpacity()); // FIXME: curve editing // m_contour->ui.cmbContour; m_contour->ui.chkAntiAliased->setChecked(bevelAndEmboss->antiAliased()); m_contour->ui.intRange->setValue(bevelAndEmboss->contourRange()); m_texture->ui.patternChooser->setCurrentPattern(bevelAndEmboss->texturePattern()); m_texture->ui.intScale->setValue(bevelAndEmboss->textureScale()); m_texture->ui.intDepth->setValue(bevelAndEmboss->textureDepth()); m_texture->ui.chkInvert->setChecked(bevelAndEmboss->textureInvert()); m_texture->ui.chkLinkWithLayer->setChecked(bevelAndEmboss->textureAlignWithLayer()); } void BevelAndEmboss::fetchBevelAndEmboss(psd_layer_effects_bevel_emboss *bevelAndEmboss) const { bevelAndEmboss->setStyle((psd_bevel_style)ui.cmbStyle->currentIndex()); bevelAndEmboss->setTechnique((psd_technique_type)ui.cmbTechnique->currentIndex()); bevelAndEmboss->setDepth(ui.intDepth->value()); bevelAndEmboss->setDirection((psd_direction)ui.cmbDirection->currentIndex()); bevelAndEmboss->setSize(ui.intSize->value()); bevelAndEmboss->setSoften(ui.intSoften->value()); bevelAndEmboss->setAngle(ui.dialAngle->value()); bevelAndEmboss->setUseGlobalLight(ui.chkUseGlobalLight->isChecked()); bevelAndEmboss->setAltitude(ui.intAltitude->value()); bevelAndEmboss->setGlossAntiAliased(ui.chkAntiAliased->isChecked()); bevelAndEmboss->setHighlightBlendMode(ui.cmbHighlightMode->selectedCompositeOp().id()); bevelAndEmboss->setHighlightColor(ui.bnHighlightColor->color().toQColor()); bevelAndEmboss->setHighlightOpacity(ui.intOpacity->value()); bevelAndEmboss->setShadowBlendMode(ui.cmbShadowMode->selectedCompositeOp().id()); bevelAndEmboss->setShadowColor(ui.bnShadowColor->color().toQColor()); bevelAndEmboss->setShadowOpacity(ui.intOpacity2->value()); // FIXME: curve editing bevelAndEmboss->setAntiAliased(m_contour->ui.chkAntiAliased->isChecked()); bevelAndEmboss->setContourRange(m_contour->ui.intRange->value()); bevelAndEmboss->setTexturePattern(static_cast(m_texture->ui.patternChooser->currentResource())); bevelAndEmboss->setTextureScale(m_texture->ui.intScale->value()); bevelAndEmboss->setTextureDepth(m_texture->ui.intDepth->value()); bevelAndEmboss->setTextureInvert(m_texture->ui.chkInvert->isChecked()); bevelAndEmboss->setTextureAlignWithLayer(m_texture->ui.chkLinkWithLayer->isChecked()); } void BevelAndEmboss::slotDialAngleChanged(int value) { KisSignalsBlocker b(ui.intAngle); ui.intAngle->setValue(value); if (ui.chkUseGlobalLight->isChecked()) { emit globalAngleChanged(value); } } void BevelAndEmboss::slotIntAngleChanged(int value) { KisSignalsBlocker b(ui.dialAngle); ui.dialAngle->setValue(value); if (ui.chkUseGlobalLight->isChecked()) { emit globalAngleChanged(value); } } void BevelAndEmboss::slotGlobalLightToggled() { if (ui.chkUseGlobalLight->isChecked()) { emit globalAngleChanged(ui.intAngle->value()); } } /********************************************************************/ /***** Texture *********************************************/ /********************************************************************/ Texture::Texture(QWidget *parent) : QWidget(parent) { ui.setupUi(this); } /********************************************************************/ /***** Contour *********************************************/ /********************************************************************/ Contour::Contour(QWidget *parent) : QWidget(parent) { ui.setupUi(this); } /********************************************************************/ /***** Blending Options *********************************************/ /********************************************************************/ BlendingOptions::BlendingOptions(QWidget *parent) : QWidget(parent) { ui.setupUi(this); // FIXME: Blend options are not implemented yet ui.grpBlendingOptions->setTitle(QString("%1 (%2)").arg(ui.grpBlendingOptions->title()).arg(i18n("Not Implemented Yet"))); ui.grpBlendingOptions->setEnabled(false); } /********************************************************************/ /***** Color Overlay *********************************************/ /********************************************************************/ ColorOverlay::ColorOverlay(QWidget *parent) : QWidget(parent) { ui.setupUi(this); ui.intOpacity->setRange(0, 100); ui.intOpacity->setSuffix(i18n(" %")); connect(ui.cmbCompositeOp, SIGNAL(currentIndexChanged(int)), SIGNAL(configChanged())); connect(ui.intOpacity, SIGNAL(valueChanged(int)), SIGNAL(configChanged())); connect(ui.bnColor, SIGNAL(changed(KoColor)), SIGNAL(configChanged())); } void ColorOverlay::setColorOverlay(const psd_layer_effects_color_overlay *colorOverlay) { ui.cmbCompositeOp->selectCompositeOp(KoID(colorOverlay->blendMode())); ui.intOpacity->setValue(colorOverlay->opacity()); KoColor color(KoColorSpaceRegistry::instance()->rgb8()); color.fromQColor(colorOverlay->color()); ui.bnColor->setColor(color); } void ColorOverlay::fetchColorOverlay(psd_layer_effects_color_overlay *colorOverlay) const { colorOverlay->setBlendMode(ui.cmbCompositeOp->selectedCompositeOp().id()); colorOverlay->setOpacity(ui.intOpacity->value()); colorOverlay->setColor(ui.bnColor->color().toQColor()); } /********************************************************************/ /***** Drop Shadow **************************************************/ /********************************************************************/ DropShadow::DropShadow(Mode mode, QWidget *parent) : QWidget(parent), m_mode(mode) { ui.setupUi(this); ui.intOpacity->setRange(0, 100); ui.intOpacity->setSuffix(i18n(" %")); ui.intDistance->setRange(0, 500); ui.intDistance->setSuffix(i18n(" px")); ui.intDistance->setExponentRatio(3.0); ui.intSpread->setRange(0, 100); ui.intSpread->setSuffix(i18n(" %")); ui.intSize->setRange(0, 250); ui.intSize->setSuffix(i18n(" px")); ui.intNoise->setRange(0, 100); ui.intNoise->setSuffix(i18n(" %")); connect(ui.dialAngle, SIGNAL(valueChanged(int)), SLOT(slotDialAngleChanged(int))); connect(ui.intAngle, SIGNAL(valueChanged(int)), SLOT(slotIntAngleChanged(int))); connect(ui.chkUseGlobalLight, SIGNAL(toggled(bool)), SLOT(slotGlobalLightToggled())); // connect everything to configChanged() signal connect(ui.cmbCompositeOp, SIGNAL(currentIndexChanged(int)), SIGNAL(configChanged())); connect(ui.intOpacity, SIGNAL(valueChanged(int)), SIGNAL(configChanged())); connect(ui.bnColor, SIGNAL(changed(KoColor)), SIGNAL(configChanged())); connect(ui.dialAngle, SIGNAL(valueChanged(int)), SIGNAL(configChanged())); connect(ui.intAngle, SIGNAL(valueChanged(int)), SIGNAL(configChanged())); connect(ui.chkUseGlobalLight, SIGNAL(toggled(bool)), SIGNAL(configChanged())); connect(ui.intDistance, SIGNAL(valueChanged(int)), SIGNAL(configChanged())); connect(ui.intSpread, SIGNAL(valueChanged(int)), SIGNAL(configChanged())); connect(ui.intSize, SIGNAL(valueChanged(int)), SIGNAL(configChanged())); connect(ui.cmbContour, SIGNAL(currentIndexChanged(int)), SIGNAL(configChanged())); connect(ui.chkAntiAliased, SIGNAL(toggled(bool)), SIGNAL(configChanged())); connect(ui.intNoise, SIGNAL(valueChanged(int)), SIGNAL(configChanged())); connect(ui.chkLayerKnocksOutDropShadow, SIGNAL(toggled(bool)), SIGNAL(configChanged())); if (m_mode == InnerShadowMode) { ui.chkLayerKnocksOutDropShadow->setVisible(false); ui.grpMain->setTitle(i18n("Inner Shadow")); ui.lblSpread->setText(i18n("Choke:")); } } void DropShadow::slotDialAngleChanged(int value) { KisSignalsBlocker b(ui.intAngle); ui.intAngle->setValue(value); if (ui.chkUseGlobalLight->isChecked()) { emit globalAngleChanged(value); } } void DropShadow::slotIntAngleChanged(int value) { KisSignalsBlocker b(ui.dialAngle); ui.dialAngle->setValue(value); if (ui.chkUseGlobalLight->isChecked()) { emit globalAngleChanged(value); } } void DropShadow::slotGlobalLightToggled() { if (ui.chkUseGlobalLight->isChecked()) { emit globalAngleChanged(ui.intAngle->value()); } } void DropShadow::setShadow(const psd_layer_effects_shadow_common *shadow) { ui.cmbCompositeOp->selectCompositeOp(KoID(shadow->blendMode())); ui.intOpacity->setValue(shadow->opacity()); KoColor color(KoColorSpaceRegistry::instance()->rgb8()); color.fromQColor(shadow->color()); ui.bnColor->setColor(color); ui.dialAngle->setValue(shadow->angle()); ui.intAngle->setValue(shadow->angle()); ui.chkUseGlobalLight->setChecked(shadow->useGlobalLight()); ui.intDistance->setValue(shadow->distance()); ui.intSpread->setValue(shadow->spread()); ui.intSize->setValue(shadow->size()); // FIXME: curve editing // ui.cmbContour; ui.chkAntiAliased->setChecked(shadow->antiAliased()); ui.intNoise->setValue(shadow->noise()); if (m_mode == DropShadowMode) { const psd_layer_effects_drop_shadow *realDropShadow = dynamic_cast(shadow); KIS_ASSERT_RECOVER_NOOP(realDropShadow); ui.chkLayerKnocksOutDropShadow->setChecked(shadow->knocksOut()); } } void DropShadow::fetchShadow(psd_layer_effects_shadow_common *shadow) const { shadow->setBlendMode(ui.cmbCompositeOp->selectedCompositeOp().id()); shadow->setOpacity(ui.intOpacity->value()); shadow->setColor(ui.bnColor->color().toQColor()); shadow->setAngle(ui.dialAngle->value()); shadow->setUseGlobalLight(ui.chkUseGlobalLight->isChecked()); shadow->setDistance(ui.intDistance->value()); shadow->setSpread(ui.intSpread->value()); shadow->setSize(ui.intSize->value()); // FIXME: curve editing // ui.cmbContour; shadow->setAntiAliased(ui.chkAntiAliased->isChecked()); shadow->setNoise(ui.intNoise->value()); if (m_mode == DropShadowMode) { psd_layer_effects_drop_shadow *realDropShadow = dynamic_cast(shadow); KIS_ASSERT_RECOVER_NOOP(realDropShadow); realDropShadow->setKnocksOut(ui.chkLayerKnocksOutDropShadow->isChecked()); } } class GradientPointerConverter { public: static KoAbstractGradientSP resourceToStyle(KoAbstractGradient *gradient) { return gradient ? KoAbstractGradientSP(gradient->clone()) : KoAbstractGradientSP(); } static KoAbstractGradient* styleToResource(KoAbstractGradientSP gradient) { if (!gradient) return 0; KoResourceServer *server = KoResourceServerProvider::instance()->gradientServer(); KoAbstractGradient *resource = server->resourceByMD5(gradient->md5()); if (!resource) { KoAbstractGradient *clone = gradient->clone(); clone->setName(findAvailableName(gradient->name())); server->addResource(clone, false); resource = clone; } return resource; } private: static QString findAvailableName(const QString &name) { KoResourceServer *server = KoResourceServerProvider::instance()->gradientServer(); QString newName = name; int i = 0; while (server->resourceByName(newName)) { newName = QString("%1%2").arg(name).arg(i++); } return newName; } }; /********************************************************************/ /***** Gradient Overlay *********************************************/ /********************************************************************/ GradientOverlay::GradientOverlay(KisCanvasResourceProvider *resourceProvider, QWidget *parent) : QWidget(parent), m_resourceProvider(resourceProvider) { ui.setupUi(this); ui.intOpacity->setRange(0, 100); ui.intOpacity->setSuffix(i18n(" %")); ui.intScale->setRange(0, 100); ui.intScale->setSuffix(i18n(" %")); connect(ui.dialAngle, SIGNAL(valueChanged(int)), SLOT(slotDialAngleChanged(int))); connect(ui.intAngle, SIGNAL(valueChanged(int)), SLOT(slotIntAngleChanged(int))); connect(ui.cmbCompositeOp, SIGNAL(currentIndexChanged(int)), SIGNAL(configChanged())); connect(ui.intOpacity, SIGNAL(valueChanged(int)), SIGNAL(configChanged())); connect(ui.cmbGradient, SIGNAL(gradientChanged(KoAbstractGradient*)), SIGNAL(configChanged())); connect(ui.chkReverse, SIGNAL(toggled(bool)), SIGNAL(configChanged())); connect(ui.cmbStyle, SIGNAL(currentIndexChanged(int)), SIGNAL(configChanged())); connect(ui.chkAlignWithLayer, SIGNAL(toggled(bool)), SIGNAL(configChanged())); connect(ui.dialAngle, SIGNAL(valueChanged(int)), SIGNAL(configChanged())); connect(ui.intAngle, SIGNAL(valueChanged(int)), SIGNAL(configChanged())); connect(ui.intScale, SIGNAL(valueChanged(int)), SIGNAL(configChanged())); } void GradientOverlay::setGradientOverlay(const psd_layer_effects_gradient_overlay *config) { ui.cmbCompositeOp->selectCompositeOp(KoID(config->blendMode())); ui.intOpacity->setValue(config->opacity()); KoAbstractGradient *gradient = fetchGradientLazy( GradientPointerConverter::styleToResource(config->gradient()), m_resourceProvider); if (gradient) { ui.cmbGradient->setGradient(gradient); } - ui.chkReverse->setChecked(config->antiAliased()); + ui.chkReverse->setChecked(config->reverse()); ui.cmbStyle->setCurrentIndex((int)config->style()); ui.chkAlignWithLayer->setCheckable(config->alignWithLayer()); ui.dialAngle->setValue(config->angle()); ui.intAngle->setValue(config->angle()); ui.intScale->setValue(config->scale()); } void GradientOverlay::fetchGradientOverlay(psd_layer_effects_gradient_overlay *config) const { config->setBlendMode(ui.cmbCompositeOp->selectedCompositeOp().id()); config->setOpacity(ui.intOpacity->value()); config->setGradient(GradientPointerConverter::resourceToStyle(ui.cmbGradient->gradient())); config->setReverse(ui.chkReverse->isChecked()); config->setStyle((psd_gradient_style)ui.cmbStyle->currentIndex()); config->setAlignWithLayer(ui.chkAlignWithLayer->isChecked()); config->setAngle(ui.dialAngle->value()); config->setScale(ui.intScale->value()); } void GradientOverlay::slotDialAngleChanged(int value) { KisSignalsBlocker b(ui.intAngle); ui.intAngle->setValue(value); } void GradientOverlay::slotIntAngleChanged(int value) { KisSignalsBlocker b(ui.dialAngle); ui.dialAngle->setValue(value); } /********************************************************************/ /***** Innner Glow *********************************************/ /********************************************************************/ InnerGlow::InnerGlow(Mode mode, KisCanvasResourceProvider *resourceProvider, QWidget *parent) : QWidget(parent), m_mode(mode), m_resourceProvider(resourceProvider) { ui.setupUi(this); ui.intOpacity->setRange(0, 100); ui.intOpacity->setSuffix(i18n(" %")); ui.intNoise->setRange(0, 100); ui.intNoise->setSuffix(i18n(" %")); ui.intChoke->setRange(0, 100); ui.intChoke->setSuffix(i18n(" %")); ui.intSize->setRange(0, 250); ui.intSize->setSuffix(i18n(" px")); ui.intRange->setRange(1, 100); ui.intRange->setSuffix(i18n(" %")); ui.intJitter->setRange(0, 100); ui.intJitter->setSuffix(i18n(" %")); connect(ui.cmbCompositeOp, SIGNAL(currentIndexChanged(int)), SIGNAL(configChanged())); connect(ui.intOpacity, SIGNAL(valueChanged(int)), SIGNAL(configChanged())); connect(ui.intNoise, SIGNAL(valueChanged(int)), SIGNAL(configChanged())); connect(ui.radioColor, SIGNAL(toggled(bool)), SIGNAL(configChanged())); connect(ui.bnColor, SIGNAL(changed(KoColor)), SIGNAL(configChanged())); connect(ui.radioGradient, SIGNAL(toggled(bool)), SIGNAL(configChanged())); connect(ui.cmbGradient, SIGNAL(gradientChanged(KoAbstractGradient*)), SIGNAL(configChanged())); connect(ui.cmbTechnique, SIGNAL(currentIndexChanged(int)), SIGNAL(configChanged())); connect(ui.cmbSource, SIGNAL(currentIndexChanged(int)), SIGNAL(configChanged())); connect(ui.intChoke, SIGNAL(valueChanged(int)), SIGNAL(configChanged())); connect(ui.intSize, SIGNAL(valueChanged(int)), SIGNAL(configChanged())); connect(ui.cmbContour, SIGNAL(currentIndexChanged(int)), SIGNAL(configChanged())); connect(ui.chkAntiAliased, SIGNAL(toggled(bool)), SIGNAL(configChanged())); connect(ui.intRange, SIGNAL(valueChanged(int)), SIGNAL(configChanged())); connect(ui.intJitter, SIGNAL(valueChanged(int)), SIGNAL(configChanged())); if (m_mode == OuterGlowMode) { ui.cmbSource->hide(); ui.lblSource->hide(); ui.lblChoke->setText(i18nc("layer styles parameter", "Spread:")); } } void InnerGlow::setConfig(const psd_layer_effects_glow_common *config) { ui.cmbCompositeOp->selectCompositeOp(KoID(config->blendMode())); ui.intOpacity->setValue(config->opacity()); ui.intNoise->setValue(config->noise()); ui.radioColor->setChecked(config->fillType() == psd_fill_solid_color); KoColor color(KoColorSpaceRegistry::instance()->rgb8()); color.fromQColor(config->color()); ui.bnColor->setColor(color); ui.radioGradient->setChecked(config->fillType() == psd_fill_gradient); KoAbstractGradient *gradient = fetchGradientLazy( GradientPointerConverter::styleToResource(config->gradient()), m_resourceProvider); if (gradient) { ui.cmbGradient->setGradient(gradient); } ui.cmbTechnique->setCurrentIndex((int)config->technique()); ui.intChoke->setValue(config->spread()); ui.intSize->setValue(config->size()); if (m_mode == InnerGlowMode) { const psd_layer_effects_inner_glow *iglow = dynamic_cast(config); KIS_ASSERT_RECOVER_RETURN(iglow); ui.cmbSource->setCurrentIndex(iglow->source() == psd_glow_edge); } // FIXME: Curve editing //ui.cmbContour; ui.chkAntiAliased->setChecked(config->antiAliased()); ui.intRange->setValue(config->range()); ui.intJitter->setValue(config->jitter()); } void InnerGlow::fetchConfig(psd_layer_effects_glow_common *config) const { config->setBlendMode(ui.cmbCompositeOp->selectedCompositeOp().id()); config->setOpacity(ui.intOpacity->value()); config->setNoise(ui.intNoise->value()); if (ui.radioColor->isChecked()) { config->setFillType(psd_fill_solid_color); } else { config->setFillType(psd_fill_gradient); } config->setColor(ui.bnColor->color().toQColor()); config->setGradient(GradientPointerConverter::resourceToStyle(ui.cmbGradient->gradient())); config->setTechnique((psd_technique_type)ui.cmbTechnique->currentIndex()); config->setSpread(ui.intChoke->value()); config->setSize(ui.intSize->value()); if (m_mode == InnerGlowMode) { psd_layer_effects_inner_glow *iglow = dynamic_cast(config); KIS_ASSERT_RECOVER_RETURN(iglow); iglow->setSource((psd_glow_source)ui.cmbSource->currentIndex()); } // FIXME: Curve editing //ui.cmbContour; config->setAntiAliased(ui.chkAntiAliased->isChecked()); config->setRange(ui.intRange->value()); config->setJitter(ui.intJitter->value()); } /********************************************************************/ /***** Pattern Overlay *********************************************/ /********************************************************************/ PatternOverlay::PatternOverlay(QWidget *parent) : QWidget(parent) { ui.setupUi(this); ui.intOpacity->setRange(0, 100); ui.intOpacity->setSuffix(i18n(" %")); ui.intScale->setRange(0, 100); ui.intScale->setSuffix(i18n(" %")); connect(ui.cmbCompositeOp, SIGNAL(currentIndexChanged(int)), SIGNAL(configChanged())); connect(ui.intOpacity, SIGNAL(valueChanged(int)), SIGNAL(configChanged())); connect(ui.patternChooser, SIGNAL(resourceSelected(KoResource*)), SIGNAL(configChanged())); connect(ui.chkLinkWithLayer, SIGNAL(toggled(bool)), SIGNAL(configChanged())); connect(ui.intScale, SIGNAL(valueChanged(int)), SIGNAL(configChanged())); } void PatternOverlay::setPatternOverlay(const psd_layer_effects_pattern_overlay *pattern) { ui.cmbCompositeOp->selectCompositeOp(KoID(pattern->blendMode())); ui.intOpacity->setValue(pattern->opacity()); ui.patternChooser->setCurrentPattern(pattern->pattern()); ui.chkLinkWithLayer->setChecked(pattern->alignWithLayer()); ui.intScale->setValue(pattern->scale()); } void PatternOverlay::fetchPatternOverlay(psd_layer_effects_pattern_overlay *pattern) const { pattern->setBlendMode(ui.cmbCompositeOp->selectedCompositeOp().id()); pattern->setOpacity(ui.intOpacity->value()); pattern->setPattern(static_cast(ui.patternChooser->currentResource())); pattern->setAlignWithLayer(ui.chkLinkWithLayer->isChecked()); pattern->setScale(ui.intScale->value()); } /********************************************************************/ /***** Satin *********************************************/ /********************************************************************/ Satin::Satin(QWidget *parent) : QWidget(parent) { ui.setupUi(this); ui.intOpacity->setRange(0, 100); ui.intOpacity->setSuffix(i18n(" %")); ui.intDistance->setRange(0, 250); ui.intDistance->setSuffix(i18n(" px")); ui.intSize->setRange(0, 250); ui.intSize->setSuffix(i18n(" px")); connect(ui.dialAngle, SIGNAL(valueChanged(int)), SLOT(slotDialAngleChanged(int))); connect(ui.intAngle, SIGNAL(valueChanged(int)), SLOT(slotIntAngleChanged(int))); connect(ui.cmbCompositeOp, SIGNAL(currentIndexChanged(int)), SIGNAL(configChanged())); connect(ui.bnColor, SIGNAL(changed(KoColor)), SIGNAL(configChanged())); connect(ui.intOpacity, SIGNAL(valueChanged(int)), SIGNAL(configChanged())); connect(ui.dialAngle, SIGNAL(valueChanged(int)), SIGNAL(configChanged())); connect(ui.intAngle, SIGNAL(valueChanged(int)), SIGNAL(configChanged())); connect(ui.intDistance, SIGNAL(valueChanged(int)), SIGNAL(configChanged())); connect(ui.intSize, SIGNAL(valueChanged(int)), SIGNAL(configChanged())); connect(ui.cmbContour, SIGNAL(currentIndexChanged(int)), SIGNAL(configChanged())); connect(ui.chkAntiAliased, SIGNAL(toggled(bool)), SIGNAL(configChanged())); connect(ui.chkInvert, SIGNAL(toggled(bool)), SIGNAL(configChanged())); } void Satin::slotDialAngleChanged(int value) { KisSignalsBlocker b(ui.intAngle); ui.intAngle->setValue(value); } void Satin::slotIntAngleChanged(int value) { KisSignalsBlocker b(ui.dialAngle); ui.dialAngle->setValue(value); } void Satin::setSatin(const psd_layer_effects_satin *satin) { ui.cmbCompositeOp->selectCompositeOp(KoID(satin->blendMode())); KoColor color(KoColorSpaceRegistry::instance()->rgb8()); color.fromQColor(satin->color()); ui.bnColor->setColor(color); ui.intOpacity->setValue(satin->opacity()); ui.dialAngle->setValue(satin->angle()); ui.intAngle->setValue(satin->angle()); ui.intDistance->setValue(satin->distance()); ui.intSize->setValue(satin->size()); // FIXME: Curve editing //ui.cmbContour; ui.chkAntiAliased->setChecked(satin->antiAliased()); ui.chkInvert->setChecked(satin->invert()); } void Satin::fetchSatin(psd_layer_effects_satin *satin) const { satin->setBlendMode(ui.cmbCompositeOp->selectedCompositeOp().id()); satin->setOpacity(ui.intOpacity->value()); satin->setColor(ui.bnColor->color().toQColor()); satin->setAngle(ui.dialAngle->value()); satin->setDistance(ui.intDistance->value()); satin->setSize(ui.intSize->value()); // FIXME: curve editing // ui.cmbContour; satin->setAntiAliased(ui.chkAntiAliased->isChecked()); satin->setInvert(ui.chkInvert->isChecked()); } /********************************************************************/ /***** Stroke *********************************************/ /********************************************************************/ Stroke::Stroke(KisCanvasResourceProvider *resourceProvider, QWidget *parent) : QWidget(parent), m_resourceProvider(resourceProvider) { ui.setupUi(this); ui.intSize->setRange(0, 250); ui.intSize->setSuffix(i18n(" px")); ui.intOpacity->setRange(0, 100); ui.intOpacity->setSuffix(i18n(" %")); ui.intScale->setRange(0, 100); ui.intScale->setSuffix(i18n(" %")); ui.intScale_2->setRange(0, 100); ui.intScale_2->setSuffix(i18n(" %")); connect(ui.cmbFillType, SIGNAL(currentIndexChanged(int)), ui.fillStack, SLOT(setCurrentIndex(int))); connect(ui.intSize, SIGNAL(valueChanged(int)), SIGNAL(configChanged())); connect(ui.cmbPosition, SIGNAL(currentIndexChanged(int)), SIGNAL(configChanged())); connect(ui.cmbCompositeOp, SIGNAL(currentIndexChanged(int)), SIGNAL(configChanged())); connect(ui.intOpacity, SIGNAL(valueChanged(int)), SIGNAL(configChanged())); connect(ui.cmbFillType, SIGNAL(currentIndexChanged(int)), SIGNAL(configChanged())); connect(ui.bnColor, SIGNAL(changed(KoColor)), SIGNAL(configChanged())); connect(ui.cmbGradient, SIGNAL(gradientChanged(KoAbstractGradient*)), SIGNAL(configChanged())); connect(ui.chkReverse, SIGNAL(toggled(bool)), SIGNAL(configChanged())); connect(ui.cmbStyle, SIGNAL(currentIndexChanged(int)), SIGNAL(configChanged())); connect(ui.chkAlignWithLayer, SIGNAL(toggled(bool)), SIGNAL(configChanged())); connect(ui.dialAngle, SIGNAL(valueChanged(int)), SLOT(slotDialAngleChanged(int))); connect(ui.intAngle, SIGNAL(valueChanged(int)), SLOT(slotIntAngleChanged(int))); connect(ui.intScale, SIGNAL(valueChanged(int)), SIGNAL(configChanged())); connect(ui.patternChooser, SIGNAL(resourceSelected(KoResource*)), SIGNAL(configChanged())); connect(ui.chkLinkWithLayer, SIGNAL(toggled(bool)), SIGNAL(configChanged())); connect(ui.intScale_2, SIGNAL(valueChanged(int)), SIGNAL(configChanged())); // cold initialization ui.fillStack->setCurrentIndex(ui.cmbFillType->currentIndex()); } void Stroke::slotDialAngleChanged(int value) { KisSignalsBlocker b(ui.intAngle); ui.intAngle->setValue(value); } void Stroke::slotIntAngleChanged(int value) { KisSignalsBlocker b(ui.dialAngle); ui.dialAngle->setValue(value); } void Stroke::setStroke(const psd_layer_effects_stroke *stroke) { ui.intSize->setValue(stroke->size()); ui.cmbPosition->setCurrentIndex((int)stroke->position()); ui.cmbCompositeOp->selectCompositeOp(KoID(stroke->blendMode())); ui.intOpacity->setValue(stroke->opacity()); ui.cmbFillType->setCurrentIndex((int)stroke->fillType()); KoColor color(KoColorSpaceRegistry::instance()->rgb8()); color.fromQColor(stroke->color()); ui.bnColor->setColor(color); KoAbstractGradient *gradient = fetchGradientLazy(GradientPointerConverter::styleToResource(stroke->gradient()), m_resourceProvider); if (gradient) { ui.cmbGradient->setGradient(gradient); } ui.chkReverse->setChecked(stroke->antiAliased()); ui.cmbStyle->setCurrentIndex((int)stroke->style()); ui.chkAlignWithLayer->setCheckable(stroke->alignWithLayer()); ui.dialAngle->setValue(stroke->angle()); ui.intAngle->setValue(stroke->angle()); ui.intScale->setValue(stroke->scale()); ui.patternChooser->setCurrentPattern(stroke->pattern()); ui.chkLinkWithLayer->setChecked(stroke->alignWithLayer()); ui.intScale_2->setValue(stroke->scale()); } void Stroke::fetchStroke(psd_layer_effects_stroke *stroke) const { stroke->setSize(ui.intSize->value()); stroke->setPosition((psd_stroke_position)ui.cmbPosition->currentIndex()); stroke->setBlendMode(ui.cmbCompositeOp->selectedCompositeOp().id()); stroke->setOpacity(ui.intOpacity->value()); stroke->setFillType((psd_fill_type)ui.cmbFillType->currentIndex()); stroke->setColor(ui.bnColor->color().toQColor()); stroke->setGradient(GradientPointerConverter::resourceToStyle(ui.cmbGradient->gradient())); stroke->setReverse(ui.chkReverse->isChecked()); stroke->setStyle((psd_gradient_style)ui.cmbStyle->currentIndex()); stroke->setAlignWithLayer(ui.chkAlignWithLayer->isChecked()); stroke->setAngle(ui.dialAngle->value()); stroke->setScale(ui.intScale->value()); stroke->setPattern(static_cast(ui.patternChooser->currentResource())); stroke->setAlignWithLayer(ui.chkLinkWithLayer->isChecked()); stroke->setScale(ui.intScale->value()); } diff --git a/libs/widgets/KoResourceServer.h b/libs/widgets/KoResourceServer.h index 6714686ea6..385ef9a74a 100644 --- a/libs/widgets/KoResourceServer.h +++ b/libs/widgets/KoResourceServer.h @@ -1,713 +1,727 @@ /* This file is part of the KDE project Copyright (c) 1999 Matthias Elter Copyright (c) 2003 Patrick Julien Copyright (c) 2005 Sven Langkamp Copyright (c) 2007 Jan Hambrecht Copyright (C) 2011 Srikanth Tiyyagura Copyright (c) 2013 Sascha Suelzer 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; either version 2.1 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 library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #ifndef KORESOURCESERVER_H #define KORESOURCESERVER_H #include #include #include #include #include #include #include #include #include "resources/KoResource.h" #include "KoResourceServerPolicies.h" #include "KoResourceServerObserver.h" #include "KoResourceTagStore.h" #include "KoResourcePaths.h" #include "kritawidgets_export.h" #include "WidgetsDebug.h" class KoResource; /** * KoResourceServerBase is the base class of all resource servers */ class KRITAWIDGETS_EXPORT KoResourceServerBase { public: /** * Constructs a KoResourceServerBase * @param resource type, has to be the same as used by KoResourcePaths * @param extensions the file extensions separate by ':', e.g. "*.kgr:*.svg:*.ggr" */ KoResourceServerBase(const QString& type, const QString& extensions) : m_type(type) , m_extensions(extensions) { } virtual ~KoResourceServerBase() {} virtual int resourceCount() const = 0; virtual void loadResources(QStringList filenames) = 0; virtual QStringList blackListedFiles() const = 0; virtual QStringList queryResources(const QString &query) const = 0; QString type() const { return m_type; } /** * File extensions for resources of the server * @returns the file extensions separated by ':', e.g. "*.kgr:*.svg:*.ggr" */ QString extensions() const { return m_extensions; } QStringList fileNames() const { QStringList extensionList = m_extensions.split(':'); QStringList fileNames; foreach (const QString &extension, extensionList) { fileNames += KoResourcePaths::findAllResources(type().toLatin1(), extension, KoResourcePaths::Recursive); } return fileNames; } protected: friend class KoResourceTagStore; virtual KoResource *byMd5(const QByteArray &md5) const = 0; virtual KoResource *byFileName(const QString &fileName) const = 0; private: QString m_type; QString m_extensions; protected: QMutex m_loadLock; }; /** * KoResourceServer manages the resources of one type. It stores, * loads and saves the resources. To keep track of changes the server * can be observed with a KoResourceServerObserver * * The \p Policy template parameter defines the way how the lifetime * of a resource is handled. There are to predefined policies: * * o PointerStoragePolicy --- usual pointers with ownership over * the resource. * o SharedPointerStoragePolicy --- shared pointers. The server does no * extra handling for the lifetime of * the resource. * * Use the former for usual resources and the latter for shared pointer based * ones. */ template > class KoResourceServer : public KoResourceServerBase { public: typedef typename Policy::PointerType PointerType; typedef KoResourceServerObserver ObserverType; KoResourceServer(const QString& type, const QString& extensions) : KoResourceServerBase(type, extensions) { m_blackListFile = KoResourcePaths::locateLocal("data", type + ".blacklist"); m_blackListFileNames = readBlackListFile(); m_tagStore = new KoResourceTagStore(this); m_tagStore->loadTags(); } ~KoResourceServer() override { if (m_tagStore) { delete m_tagStore; } Q_FOREACH (ObserverType* observer, m_observers) { observer->unsetResourceServer(); } Q_FOREACH (PointerType res, m_resources) { Policy::deleteResource(res); } m_resources.clear(); } int resourceCount() const override { return m_resources.size(); } /** * Loads a set of resources and adds them to the resource server. * If a filename appears twice the resource will only be added once. Resources that can't * be loaded or and invalid aren't added to the server. * @param filenames list of filenames to be loaded */ void loadResources(QStringList filenames) override { QStringList uniqueFiles; while (!filenames.empty()) { QString front = filenames.first(); filenames.pop_front(); // In the save location, people can use sub-folders... And then they probably want // to load both versions! See https://bugs.kde.org/show_bug.cgi?id=321361. QString fname; if (front.contains(saveLocation())) { fname = front.split(saveLocation())[1]; } else { fname = QFileInfo(front).fileName(); } // XXX: Don't load resources with the same filename. Actually, we should look inside // the resource to find out whether they are really the same, but for now this // will prevent the same brush etc. showing up twice. if (!uniqueFiles.contains(fname)) { m_loadLock.lock(); uniqueFiles.append(fname); QList resources = createResources(front); Q_FOREACH (PointerType resource, resources) { Q_CHECK_PTR(resource); if (resource->load() && resource->valid() && !resource->md5().isEmpty()) { - QByteArray md5 = resource->md5(); - m_resourcesByMd5[md5] = resource; + addResourceToMd5Registry(resource); m_resourcesByFilename[resource->shortFilename()] = resource; if (resource->name().isEmpty()) { resource->setName(fname); } if (m_resourcesByName.contains(resource->name())) { resource->setName(resource->name() + "(" + resource->shortFilename() + ")"); } m_resourcesByName[resource->name()] = resource; notifyResourceAdded(resource); } else { warnWidgets << "Loading resource " << front << "failed"; Policy::deleteResource(resource); } } m_loadLock.unlock(); } } m_resources = sortedResources(); Q_FOREACH (ObserverType* observer, m_observers) { observer->syncTaggedResourceView(); } debugWidgets << "done loading resources for type " << type(); } /// Adds an already loaded resource to the server bool addResource(PointerType resource, bool save = true, bool infront = false) { if (!resource->valid()) { warnWidgets << "Tried to add an invalid resource!"; return false; } if (save) { QFileInfo fileInfo(resource->filename()); QDir d(fileInfo.path()); if (!d.exists()) { d.mkdir(fileInfo.path()); } if (fileInfo.exists()) { QString filename = fileInfo.path() + "/" + fileInfo.baseName() + "XXXXXX" + "." + fileInfo.suffix(); debugWidgets << "fileName is " << filename; QTemporaryFile file(filename); if (file.open()) { debugWidgets << "now " << file.fileName(); resource->setFilename(file.fileName()); } } if (!resource->save()) { warnWidgets << "Could not save resource!"; return false; } } Q_ASSERT(!resource->filename().isEmpty() || !resource->name().isEmpty()); if (resource->filename().isEmpty()) { resource->setFilename(resource->name()); } else if (resource->name().isEmpty()) { resource->setName(resource->filename()); } m_resourcesByFilename[resource->shortFilename()] = resource; - m_resourcesByMd5[resource->md5()] = resource; + addResourceToMd5Registry(resource); m_resourcesByName[resource->name()] = resource; if (infront) { m_resources.insert(0, resource); } else { m_resources.append(resource); } notifyResourceAdded(resource); return true; } /*Removes a given resource from the blacklist. */ bool removeFromBlacklist(PointerType resource) { if (m_blackListFileNames.contains(resource->filename())) { m_blackListFileNames.removeAll(resource->filename()); writeBlackListFile(); } else{ warnWidgets<<"Doesn't contain filename"; return false; } //then return true// return true; } /// Remove a resource from Resource Server but not from a file bool removeResourceFromServer(PointerType resource){ if ( !m_resourcesByFilename.contains( resource->shortFilename() ) ) { return false; } - m_resourcesByMd5.remove(resource->md5()); + removeResourceFromMd5Registry(resource); m_resourcesByName.remove(resource->name()); m_resourcesByFilename.remove(resource->shortFilename()); m_resources.removeAt(m_resources.indexOf(resource)); m_tagStore->removeResource(resource); notifyRemovingResource(resource); Policy::deleteResource(resource); return true; } /// Remove a resource from the resourceserver and blacklist it bool removeResourceAndBlacklist(PointerType resource) { if ( !m_resourcesByFilename.contains( resource->shortFilename() ) ) { return false; } - m_resourcesByMd5.remove(resource->md5()); + removeResourceFromMd5Registry(resource); m_resourcesByName.remove(resource->name()); m_resourcesByFilename.remove(resource->shortFilename()); m_resources.removeAt(m_resources.indexOf(resource)); m_tagStore->removeResource(resource); notifyRemovingResource(resource); m_blackListFileNames.append(resource->filename()); writeBlackListFile(); Policy::deleteResource(resource); return true; } QList resources() { m_loadLock.lock(); QList resourceList = m_resources; Q_FOREACH (PointerType r, m_resourceBlackList) { resourceList.removeOne(r); } m_loadLock.unlock(); return resourceList; } /// Returns path where to save user defined and imported resources to virtual QString saveLocation() { return KoResourcePaths::saveLocation(type().toLatin1()); } /** * Creates a new resource from a given file and adds them to the resource server * The base implementation does only load one resource per file, override to implement collections * @param filename file name of the resource file to be imported * @param fileCreation decides whether to create the file in the saveLocation() directory */ virtual bool importResourceFile(const QString & filename , bool fileCreation=true) { QFileInfo fi(filename); if (!fi.exists()) return false; if ( fi.size() == 0) return false; PointerType resource = createResource( filename ); resource->load(); if (!resource->valid()) { warnWidgets << "Import failed! Resource is not valid"; Policy::deleteResource(resource); return false; } if (fileCreation) { Q_ASSERT(!resource->defaultFileExtension().isEmpty()); Q_ASSERT(!saveLocation().isEmpty()); QString newFilename = saveLocation() + fi.baseName() + resource->defaultFileExtension(); QFileInfo fileInfo(newFilename); int i = 1; while (fileInfo.exists()) { fileInfo.setFile(saveLocation() + fi.baseName() + QString("%1").arg(i) + resource->defaultFileExtension()); i++; } resource->setFilename(fileInfo.filePath()); } if(!addResource(resource)) { Policy::deleteResource(resource); } return true; } /// Removes the resource file from the resource server virtual void removeResourceFile(const QString & filename) { QFileInfo fi(filename); PointerType resource = resourceByFilename(fi.fileName()); if (!resource) { warnWidgets << "Resource file do not exist "; return; } if (!removeResourceFromServer(resource)) return; } /** * Addes an observer to the server * @param observer the observer to be added * @param notifyLoadedResources determines if the observer should be notified about the already loaded resources */ void addObserver(ObserverType* observer, bool notifyLoadedResources = true) { m_loadLock.lock(); if(observer && !m_observers.contains(observer)) { m_observers.append(observer); if(notifyLoadedResources) { Q_FOREACH (PointerType resource, m_resourcesByFilename) { observer->resourceAdded(resource); } } } m_loadLock.unlock(); } /** * Removes an observer from the server * @param observer the observer to be removed */ void removeObserver(ObserverType* observer) { int index = m_observers.indexOf( observer ); if( index < 0 ) return; m_observers.removeAt( index ); } PointerType resourceByFilename(const QString& filename) const { if (m_resourcesByFilename.contains(filename)) { return m_resourcesByFilename[filename]; } return 0; } PointerType resourceByName( const QString& name ) const { if (m_resourcesByName.contains(name)) { return m_resourcesByName[name]; } return 0; } PointerType resourceByMD5(const QByteArray& md5) const { return m_resourcesByMd5.value(md5); } /** * Call after changing the content of a resource; * Notifies the connected views. */ void updateResource( PointerType resource ) { notifyResourceChanged(resource); } QStringList blackListedFiles() const override { return m_blackListFileNames; } void removeBlackListedFiles() { QStringList remainingFiles; // Files that can't be removed e.g. no rights will stay blacklisted Q_FOREACH (const QString &filename, m_blackListFileNames) { QFile file( filename ); if( ! file.remove() ) { remainingFiles.append(filename); } } m_blackListFileNames = remainingFiles; writeBlackListFile(); } QStringList tagNamesList() const { return m_tagStore->tagNamesList(); } // don't use these method directly since it doesn't update views! void addTag( KoResource* resource,const QString& tag) { m_tagStore->addTag(resource,tag); } // don't use these method directly since it doesn't update views! void delTag( KoResource* resource,const QString& tag) { m_tagStore->delTag(resource, tag); } QStringList searchTag(const QString& lineEditText) { return m_tagStore->searchTag(lineEditText); } void tagCategoryAdded(const QString& tag) { m_tagStore->serializeTags(); Q_FOREACH (ObserverType* observer, m_observers) { observer->syncTagAddition(tag); } } void tagCategoryRemoved(const QString& tag) { m_tagStore->delTag(tag); m_tagStore->serializeTags(); Q_FOREACH (ObserverType* observer, m_observers) { observer->syncTagRemoval(tag); } } void tagCategoryMembersChanged() { m_tagStore->serializeTags(); Q_FOREACH (ObserverType* observer, m_observers) { observer->syncTaggedResourceView(); } } QStringList queryResources(const QString &query) const override { return m_tagStore->searchTag(query); } QStringList assignedTagsList(KoResource* resource) const { return m_tagStore->assignedTagsList(resource); } /** * Create one or more resources from a single file. By default one resource is created. * Override to create more resources from the file. * @param filename the filename of the resource or resource collection */ virtual QList createResources( const QString & filename ) { QList createdResources; createdResources.append(createResource(filename)); return createdResources; } virtual PointerType createResource( const QString & filename ) = 0; /// Return the currently stored resources in alphabetical order, overwrite for customized sorting virtual QList sortedResources() { QMap sortedNames; Q_FOREACH (const QString &name, m_resourcesByName.keys()) { sortedNames.insert(name.toLower(), m_resourcesByName[name]); } return sortedNames.values(); } protected: void notifyResourceAdded(PointerType resource) { Q_FOREACH (ObserverType* observer, m_observers) { observer->resourceAdded(resource); } } void notifyRemovingResource(PointerType resource) { Q_FOREACH (ObserverType* observer, m_observers) { observer->removingResource(resource); } } void notifyResourceChanged(PointerType resource) { Q_FOREACH (ObserverType* observer, m_observers) { observer->resourceChanged(resource); } } /// Reads the xml file and returns the filenames as a list QStringList readBlackListFile() { QStringList filenameList; QFile f(m_blackListFile); if (!f.open(QIODevice::ReadOnly)) { return filenameList; } QDomDocument doc; if (!doc.setContent(&f)) { warnWidgets << "The file could not be parsed."; return filenameList; } QDomElement root = doc.documentElement(); if (root.tagName() != "resourceFilesList") { warnWidgets << "The file doesn't seem to be of interest."; return filenameList; } QDomElement file = root.firstChildElement("file"); while (!file.isNull()) { QDomNode n = file.firstChild(); QDomElement e = n.toElement(); if (e.tagName() == "name") { filenameList.append((e.text()).replace(QString("~"),QDir::homePath())); } file = file.nextSiblingElement("file"); } return filenameList; } /// write the blacklist file entries to an xml file void writeBlackListFile() { QFile f(m_blackListFile); if (!f.open(QIODevice::WriteOnly | QIODevice::Text)) { warnWidgets << "Cannot write meta information to '" << m_blackListFile << "'." << endl; return; } QDomDocument doc; QDomElement root; QDomDocument docTemp("m_blackListFile"); doc = docTemp; doc.appendChild(doc.createProcessingInstruction("xml", "version=\"1.0\" encoding=\"UTF-8\"")); root = doc.createElement("resourceFilesList"); doc.appendChild(root); Q_FOREACH (QString filename, m_blackListFileNames) { QDomElement fileEl = doc.createElement("file"); QDomElement nameEl = doc.createElement("name"); QDomText nameText = doc.createTextNode(filename.replace(QDir::homePath(),QString("~"))); nameEl.appendChild(nameText); fileEl.appendChild(nameEl); root.appendChild(fileEl); } QTextStream metastream(&f); metastream << doc.toString(); f.close(); } protected: KoResource* byMd5(const QByteArray &md5) const override { return Policy::toResourcePointer(resourceByMD5(md5)); } KoResource* byFileName(const QString &fileName) const override { return Policy::toResourcePointer(resourceByFilename(fileName)); } +private: + void addResourceToMd5Registry(PointerType resource) { + const QByteArray md5 = resource->md5(); + if (!md5.isEmpty()) { + m_resourcesByMd5.insert(md5, resource); + } + } + + void removeResourceFromMd5Registry(PointerType resource) { + const QByteArray md5 = resource->md5(); + if (!md5.isEmpty()) { + m_resourcesByMd5.remove(md5); + } + } + private: QHash m_resourcesByName; QHash m_resourcesByFilename; QHash m_resourcesByMd5; QList m_resourceBlackList; QList m_resources; ///< list of resources in order of addition QList m_observers; QString m_blackListFile; QStringList m_blackListFileNames; KoResourceTagStore* m_tagStore; }; template > class KoResourceServerSimpleConstruction : public KoResourceServer { public: KoResourceServerSimpleConstruction(const QString& type, const QString& extensions) : KoResourceServer(type, extensions) { } typename KoResourceServer::PointerType createResource( const QString & filename ) override { return new T(filename); } }; #endif // KORESOURCESERVER_H diff --git a/plugins/python/comics_project_management_tools/comics_export_dialog.py b/plugins/python/comics_project_management_tools/comics_export_dialog.py index ce6f2dbd90..7b12e54daf 100644 --- a/plugins/python/comics_project_management_tools/comics_export_dialog.py +++ b/plugins/python/comics_project_management_tools/comics_export_dialog.py @@ -1,401 +1,418 @@ """ -Part of the comics project management tools (CPMT). +Copyright (c) 2017 Wolthera van Hövell tot Westerflier +This file is part of the Comics Project Management Tools(CPMT). + +CPMT 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 3 of the License, or +(at your option) any later version. + +CPMT 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 the CPMT. If not, see . +""" + +""" A dialog for editing the exporter settings. """ from PyQt5.QtGui import QStandardItem, QStandardItemModel, QColor from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QGroupBox, QFormLayout, QCheckBox, QComboBox, QSpinBox, QWidget, QVBoxLayout, QTabWidget, QPushButton, QLineEdit, QLabel, QListView from PyQt5.QtCore import Qt, QUuid from krita import * """ A generic widget to make selecting size easier. It works by initialising with a config name(like "scale"), and then optionally setting the config with a dictionary. Then, afterwards, you get the config with a dictionary, with the config name being the entry the values are under. """ class comic_export_resize_widget(QGroupBox): configName = "" def __init__(self, configName, batch=False, fileType=True): super().__init__() self.configName = configName self.setTitle("Adjust Workingfile") formLayout = QFormLayout() self.setLayout(formLayout) self.crop = QCheckBox(i18n("Crop files before resize.")) self.cmbFile = QComboBox() self.cmbFile.addItems(["png", "jpg", "webp"]) self.resizeMethod = QComboBox() self.resizeMethod.addItems([i18n("Percentage"), i18n("DPI"), i18n("Maximum Width"), i18n("Maximum Height")]) self.resizeMethod.currentIndexChanged.connect(self.slot_set_enabled) self.spn_DPI = QSpinBox() self.spn_DPI.setMaximum(1200) self.spn_DPI.setSuffix(i18n(" DPI")) self.spn_DPI.setValue(72) self.spn_PER = QSpinBox() if batch is True: self.spn_PER.setMaximum(1000) else: self.spn_PER.setMaximum(100) self.spn_PER.setSuffix(" %") self.spn_PER.setValue(100) self.spn_width = QSpinBox() self.spn_width.setMaximum(99999) self.spn_width.setSuffix(" px") self.spn_width.setValue(800) self.spn_height = QSpinBox() self.spn_height.setMaximum(99999) self.spn_height.setSuffix(" px") self.spn_height.setValue(800) if batch is False: formLayout.addRow("", self.crop) if fileType is True and configName != "TIFF": formLayout.addRow(i18n("File Type"), self.cmbFile) formLayout.addRow(i18n("Method:"), self.resizeMethod) formLayout.addRow(i18n("DPI:"), self.spn_DPI) formLayout.addRow(i18n("Percentage:"), self.spn_PER) formLayout.addRow(i18n("Width:"), self.spn_width) formLayout.addRow(i18n("Height:"), self.spn_height) self.slot_set_enabled() def slot_set_enabled(self): method = self.resizeMethod.currentIndex() self.spn_DPI.setEnabled(False) self.spn_PER.setEnabled(False) self.spn_width.setEnabled(False) self.spn_height.setEnabled(False) if method is 0: self.spn_PER.setEnabled(True) if method is 1: self.spn_DPI.setEnabled(True) if method is 2: self.spn_width.setEnabled(True) if method is 3: self.spn_height.setEnabled(True) def set_config(self, config): if self.configName in config.keys(): mConfig = config[self.configName] if "Method" in mConfig.keys(): self.resizeMethod.setCurrentIndex(mConfig["Method"]) if "FileType" in mConfig.keys(): self.cmbFile.setCurrentText(mConfig["FileType"]) if "Crop" in mConfig.keys(): self.crop.setChecked(mConfig["Crop"]) if "DPI" in mConfig.keys(): self.spn_DPI.setValue(mConfig["DPI"]) if "Percentage" in mConfig.keys(): self.spn_PER.setValue(mConfig["Percentage"]) if "Width" in mConfig.keys(): self.spn_width.setValue(mConfig["Width"]) if "Height" in mConfig.keys(): self.spn_height.setValue(mConfig["Height"]) self.slot_set_enabled() def get_config(self, config): mConfig = {} mConfig["Method"] = self.resizeMethod.currentIndex() if self.configName == "TIFF": mConfig["FileType"] = "tiff" else: mConfig["FileType"] = self.cmbFile.currentText() mConfig["Crop"] = self.crop.isChecked() mConfig["DPI"] = self.spn_DPI.value() mConfig["Percentage"] = self.spn_PER.value() mConfig["Width"] = self.spn_width.value() mConfig["Height"] = self.spn_height.value() config[self.configName] = mConfig return config """ Quick combobox for selecting the color label. """ class labelSelector(QComboBox): def __init__(self): super(labelSelector, self).__init__() lisOfColors = [] lisOfColors.append(Qt.transparent) lisOfColors.append(QColor(91, 173, 220)) lisOfColors.append(QColor(151, 202, 63)) lisOfColors.append(QColor(247, 229, 61)) lisOfColors.append(QColor(255, 170, 63)) lisOfColors.append(QColor(177, 102, 63)) lisOfColors.append(QColor(238, 50, 51)) lisOfColors.append(QColor(191, 106, 209)) lisOfColors.append(QColor(118, 119, 114)) self.itemModel = QStandardItemModel() for color in lisOfColors: item = QStandardItem() item.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled) item.setCheckState(Qt.Unchecked) item.setText(" ") item.setData(color, Qt.BackgroundColorRole) self.itemModel.appendRow(item) self.setModel(self.itemModel) def getLabels(self): listOfIndexes = [] for i in range(self.itemModel.rowCount()): index = self.itemModel.index(i, 0) item = self.itemModel.itemFromIndex(index) if item.checkState(): listOfIndexes.append(i) return listOfIndexes def setLabels(self, listOfIndexes): for i in listOfIndexes: index = self.itemModel.index(i, 0) item = self.itemModel.itemFromIndex(index) item.setCheckState(True) """ The comic export settings dialog will allow configuring the export. This config consists of... * Crop settings. for removing bleeds. * Selecting layer labels to remove. * Choosing which formats to export to. * Choosing how to resize these * Whether to crop. * Which file type to use. And for ACBF, it gives the ability to edit acbf document info. """ class comic_export_setting_dialog(QDialog): def __init__(self): super().__init__() self.setLayout(QVBoxLayout()) self.setWindowTitle(i18n("Export settings")) buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) buttons.accepted.connect(self.accept) buttons.rejected.connect(self.reject) mainWidget = QTabWidget() self.layout().addWidget(mainWidget) self.layout().addWidget(buttons) # Set basic crop settings # Set which layers to remove before export. mainExportSettings = QWidget() mainExportSettings.setLayout(QVBoxLayout()) groupExportCrop = QGroupBox(i18n("Crop settings")) formCrop = QFormLayout() groupExportCrop.setLayout(formCrop) self.chk_toOutmostGuides = QCheckBox(i18n("Crop to outmost guides")) self.chk_toOutmostGuides.setChecked(True) self.chk_toOutmostGuides.setToolTip(i18n("This will crop to the outmost guides if possible and otherwise use the underlying crop settings.")) formCrop.addRow("", self.chk_toOutmostGuides) btn_fromSelection = QPushButton(i18n("Set margins from active selection")) btn_fromSelection.clicked.connect(self.slot_set_margin_from_selection) # This doesn't work. formCrop.addRow("", btn_fromSelection) self.spn_marginLeft = QSpinBox() self.spn_marginLeft.setMaximum(99999) self.spn_marginLeft.setSuffix(" px") formCrop.addRow(i18n("Left:"), self.spn_marginLeft) self.spn_marginTop = QSpinBox() self.spn_marginTop.setMaximum(99999) self.spn_marginTop.setSuffix(" px") formCrop.addRow(i18n("Top:"), self.spn_marginTop) self.spn_marginRight = QSpinBox() self.spn_marginRight.setMaximum(99999) self.spn_marginRight.setSuffix(" px") formCrop.addRow(i18n("Right:"), self.spn_marginRight) self.spn_marginBottom = QSpinBox() self.spn_marginBottom.setMaximum(99999) self.spn_marginBottom.setSuffix(" px") formCrop.addRow(i18n("Bottom:"), self.spn_marginBottom) groupExportLayers = QGroupBox(i18n("Layers")) formLayers = QFormLayout() groupExportLayers.setLayout(formLayers) self.cmbLabelsRemove = labelSelector() formLayers.addRow(i18n("Label for removal:"), self.cmbLabelsRemove) mainExportSettings.layout().addWidget(groupExportCrop) mainExportSettings.layout().addWidget(groupExportLayers) mainWidget.addTab(mainExportSettings, i18n("General")) # CBZ, crop, resize, which metadata to add. CBZexportSettings = QWidget() CBZexportSettings.setLayout(QVBoxLayout()) self.CBZactive = QCheckBox(i18n("Export to CBZ")) CBZexportSettings.layout().addWidget(self.CBZactive) self.CBZgroupResize = comic_export_resize_widget("CBZ") CBZexportSettings.layout().addWidget(self.CBZgroupResize) self.CBZactive.clicked.connect(self.CBZgroupResize.setEnabled) CBZgroupMeta = QGroupBox(i18n("Metadata to add")) # CBZexportSettings.layout().addWidget(CBZgroupMeta) CBZgroupMeta.setLayout(QFormLayout()) mainWidget.addTab(CBZexportSettings, "CBZ") # ACBF, crop, resize, creator name, version history, panel layer, text layers. ACBFExportSettings = QWidget() ACBFform = QFormLayout() ACBFExportSettings.setLayout(QVBoxLayout()) ACBFdocInfo = QGroupBox() ACBFdocInfo.setTitle(i18n("ACBF Document Info")) ACBFdocInfo.setLayout(ACBFform) self.lnACBFAuthor = QLineEdit() self.lnACBFAuthor.setToolTip(i18n("The person responsible for the generation of the CBZ.")) self.lnACBFSource = QLineEdit() self.lnACBFSource.setToolTip(i18n("Whether the acbf file is an adaption of an existing source, and if so, how to find information about that source. So for example, for an adapted webcomic, the official website url should go here.")) self.lnACBFID = QLabel() self.lnACBFID.setToolTip(i18n("By default this will be filled with a generated universal unique identifier. The ID by itself is merely so that comic book library management programs can figure out if this particular comic is already in their database and whether it has been rated. Of course, the UUID can be changed into something else by manually changing the json, but this is advanced usage.")) self.spnACBFVersion = QSpinBox() self.ACBFhistoryModel = QStandardItemModel() acbfHistoryList = QListView() acbfHistoryList.setModel(self.ACBFhistoryModel) btn_add_history = QPushButton(i18n("Add history entry")) btn_add_history.clicked.connect(self.slot_add_history_item) ACBFform.addRow(i18n("Author-name:"), self.lnACBFAuthor) ACBFform.addRow(i18n("Source:"), self.lnACBFSource) ACBFform.addRow(i18n("ACBF UID:"), self.lnACBFID) ACBFform.addRow(i18n("Version:"), self.spnACBFVersion) ACBFform.addRow(i18n("Version History:"), acbfHistoryList) ACBFform.addRow("", btn_add_history) ACBFExportSettings.layout().addWidget(ACBFdocInfo) mainWidget.addTab(ACBFExportSettings, "ACBF") # Epub export, crop, resize, other questions. EPUBexportSettings = QWidget() EPUBexportSettings.setLayout(QVBoxLayout()) self.EPUBactive = QCheckBox(i18n("Export to EPUB")) EPUBexportSettings.layout().addWidget(self.EPUBactive) self.EPUBgroupResize = comic_export_resize_widget("EPUB") EPUBexportSettings.layout().addWidget(self.EPUBgroupResize) self.EPUBactive.clicked.connect(self.EPUBgroupResize.setEnabled) mainWidget.addTab(EPUBexportSettings, "EPUB") # For Print. Crop, no resize. TIFFExportSettings = QWidget() TIFFExportSettings.setLayout(QVBoxLayout()) self.TIFFactive = QCheckBox(i18n("Export to TIFF")) TIFFExportSettings.layout().addWidget(self.TIFFactive) self.TIFFgroupResize = comic_export_resize_widget("TIFF") TIFFExportSettings.layout().addWidget(self.TIFFgroupResize) self.TIFFactive.clicked.connect(self.TIFFgroupResize.setEnabled) mainWidget.addTab(TIFFExportSettings, "TIFF") # SVG, crop, resize, embed vs link. #SVGExportSettings = QWidget() #mainWidget.addTab(SVGExportSettings, "SVG") """ Add a history item to the acbf version history list. """ def slot_add_history_item(self): newItem = QStandardItem() newItem.setText("v" + str(self.spnACBFVersion.value()) + "-" + i18n("in this version...")) self.ACBFhistoryModel.appendRow(newItem) """ Get the margins by treating the active selection in a document as the trim area. This allows people to snap selections to a vector or something, and then get the margins. """ def slot_set_margin_from_selection(self): doc = Application.activeDocument() if doc is not None: if doc.selection() is not None: self.spn_marginLeft.setValue(doc.selection().x()) self.spn_marginTop.setValue(doc.selection().y()) self.spn_marginRight.setValue(doc.width() - (doc.selection().x() + doc.selection().width())) self.spn_marginBottom.setValue(doc.height() - (doc.selection().y() + doc.selection().height())) """ Load the UI values from the config dictionary given. """ def setConfig(self, config): if "cropToGuides" in config.keys(): self.chk_toOutmostGuides.setChecked(config["cropToGuides"]) if "cropLeft" in config.keys(): self.spn_marginLeft.setValue(config["cropLeft"]) if "cropTop" in config.keys(): self.spn_marginTop.setValue(config["cropTop"]) if "cropRight" in config.keys(): self.spn_marginRight.setValue(config["cropRight"]) if "cropBottom" in config.keys(): self.spn_marginBottom.setValue(config["cropBottom"]) if "labelsToRemove" in config.keys(): self.cmbLabelsRemove.setLabels(config["labelsToRemove"]) self.CBZgroupResize.set_config(config) if "CBZactive" in config.keys(): self.CBZactive.setChecked(config["CBZactive"]) self.EPUBgroupResize.set_config(config) if "EPUBactive" in config.keys(): self.EPUBactive.setChecked(config["EPUBactive"]) self.TIFFgroupResize.set_config(config) if "TIFFactive" in config.keys(): self.TIFFactive.setChecked(config["TIFFactive"]) if "acbfAuthor" in config.keys(): self.lnACBFAuthor.setText(config["acbfAuthor"]) if "acbfSource" in config.keys(): self.lnACBFSource.setText(config["acbfSource"]) if "acbfID" in config.keys(): self.lnACBFID.setText(config["acbfID"]) else: self.lnACBFID.setText(QUuid.createUuid().toString()) if "acbfVersion" in config.keys(): self.spnACBFVersion.setValue(config["acbfVersion"]) if "acbfHistory" in config.keys(): for h in config["acbfHistory"]: item = QStandardItem() item.setText(h) self.ACBFhistoryModel.appendRow(item) self.CBZgroupResize.setEnabled(self.CBZactive.isChecked()) """ Store the GUI values into the config dictionary given. @return the config diactionary filled with new values. """ def getConfig(self, config): config["cropToGuides"] = self.chk_toOutmostGuides.isChecked() config["cropLeft"] = self.spn_marginLeft.value() config["cropTop"] = self.spn_marginTop.value() config["cropBottom"] = self.spn_marginRight.value() config["cropRight"] = self.spn_marginBottom.value() config["labelsToRemove"] = self.cmbLabelsRemove.getLabels() config["CBZactive"] = self.CBZactive.isChecked() config = self.CBZgroupResize.get_config(config) config["EPUBactive"] = self.EPUBactive.isChecked() config = self.EPUBgroupResize.get_config(config) config["TIFFactive"] = self.TIFFactive.isChecked() config = self.TIFFgroupResize.get_config(config) config["acbfAuthor"] = self.lnACBFAuthor.text() config["acbfSource"] = self.lnACBFSource.text() config["acbfID"] = self.lnACBFID.text() config["acbfVersion"] = self.spnACBFVersion.value() versionList = [] for r in range(self.ACBFhistoryModel.rowCount()): index = self.ACBFhistoryModel.index(r, 0) versionList.append(self.ACBFhistoryModel.data(index, Qt.DisplayRole)) config["acbfHistory"] = versionList # Turn this into something that retreives from a line-edit when string freeze is over. config["textLayerNames"] = ["text"] config["panelLayerNames"] = ["panels"] return config diff --git a/plugins/python/comics_project_management_tools/comics_exporter.py b/plugins/python/comics_project_management_tools/comics_exporter.py index 479985c96b..483ab9d1b5 100644 --- a/plugins/python/comics_project_management_tools/comics_exporter.py +++ b/plugins/python/comics_project_management_tools/comics_exporter.py @@ -1,1598 +1,1615 @@ """ -Part of the comics project management tools (CPMT). +Copyright (c) 2017 Wolthera van Hövell tot Westerflier +This file is part of the Comics Project Management Tools(CPMT). + +CPMT 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 3 of the License, or +(at your option) any later version. + +CPMT 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 the CPMT. If not, see . +""" + +""" An exporter that take the comicsConfig and uses it to generate several files. """ import sys import os from pathlib import Path import json import zipfile import xml.etree.ElementTree as ET from xml.dom import minidom import types import shutil import html import re from PyQt5.QtWidgets import QLabel, QProgressDialog, qApp # For the progress dialog. from PyQt5.QtCore import QElapsedTimer, QDate, QLocale, Qt, QRectF, QPointF, QByteArray, QBuffer from PyQt5.QtGui import QImage, QTransform, QPainterPath from krita import * """ The sizesCalculator is a convenience class for interpretting the resize configuration from the export settings dialog. It is also used for batch resize. """ class sizesCalculator(): def __init__(self): pass def get_scale_from_resize_config(self, config, listSizes): listScaleTo = listSizes oldWidth = listSizes[0] oldHeight = listSizes[1] oldXDPI = listSizes[2] oldYDPI = listSizes[3] if "Method" in config.keys(): method = config["Method"] if method is 0: # percentage percentage = config["Percentage"] / 100 listScaleTo[0] = round(oldWidth * percentage) listScaleTo[1] = round(oldHeight * percentage) if method is 1: # dpi DPI = config["DPI"] listScaleTo[0] = round((oldWidth / oldXDPI) * DPI) listScaleTo[1] = round((oldHeight / oldYDPI) * DPI) listScaleTo[2] = DPI listScaleTo[3] = DPI if method is 2: # maximum width width = config["Width"] listScaleTo[0] = width listScaleTo[1] = round((oldHeight / oldWidth) * width) if method is 3: # maximum height height = config["Height"] listScaleTo[1] = height listScaleTo[0] = round((oldWidth / oldHeight) * height) return listScaleTo """ The comicsExporter is a class that batch exports to all the requested formats. Make it, set_config with the right data, and then call up "export". The majority of the functions are meta-data encoding functions. """ class comicsExporter(): acbfLocation = str() acbfPageData = [] cometLocation = str() comicRackInfo = str() comic_book_info_json_dump = str() pagesLocationList = {} # set of keys used to define specific export behaviour for this page. pageKeys = ["acbf_title", "acbf_none", "acbf_fade", "acbf_blend", "acbf_horizontal", "acbf_vertical"] def __init__(self): pass """ The the configuration of the exporter. @param config: A dictionary containing all the config. @param projectUrl: the main location of the project folder. """ def set_config(self, config, projectURL): self.configDictionary = config self.projectURL = projectURL self.pagesLocationList = {} self.acbfLocation = str() self.acbfPageData = [] self.cometLocation = str() self.comicRackInfo = str() self.comic_book_info_json_dump = str() """ Export everything according to config and get yourself a coffee. This won't work if the config hasn't been set. """ def export(self): export_success = False path = Path(self.projectURL) if path.exists(): # Make a meta-data folder so we keep the export folder nice and clean. exportPath = path / self.configDictionary["exportLocation"] if Path(exportPath / "metadata").exists() is False: Path(exportPath / "metadata").mkdir() # Get to which formats to export, and set the sizeslist. sizesList = {} if "CBZ" in self.configDictionary.keys(): if self.configDictionary["CBZactive"]: sizesList["CBZ"] = self.configDictionary["CBZ"] if "EPUB" in self.configDictionary.keys(): if self.configDictionary["EPUBactive"]: sizesList["EPUB"] = self.configDictionary["EPUB"] if "TIFF" in self.configDictionary.keys(): if self.configDictionary["TIFFactive"]: sizesList["TIFF"] = self.configDictionary["TIFF"] # Export the pngs according to the sizeslist. export_success = self.save_out_pngs(sizesList) # Export acbf metadata. if export_success: export_success = self.export_to_acbf() # Export and package CBZ and Epub. if export_success: if "CBZ" in sizesList.keys(): export_success = self.export_to_cbz() print("CPMT: Exported to CBZ", export_success) if "EPUB" in sizesList.keys(): export_success = self.export_to_epub() print("CPMT: Exported to EPUB", export_success) else: print("CPMT: Nothing to export, url not set.") return export_success """ This calls up all the functions necessary for making a acbf. """ def export_to_acbf(self): self.write_acbf_meta_data() return True """ This calls up all the functions necessary for making a cbz. """ def export_to_cbz(self): export_success = self.write_comet_meta_data() export_success = self.write_comic_rack_info() export_success = self.write_comic_book_info_json() self.package_cbz() return export_success """ Create an epub folder, finally, package to a epubzip. """ def export_to_epub(self): path = Path(os.path.join(self.projectURL, self.configDictionary["exportLocation"])) exportPath = path / "EPUB-files" metaInf = exportPath / "META-INF" oebps = exportPath / "OEBPS" imagePath = oebps / "Images" stylesPath = oebps / "Styles" textPath = oebps / "Text" if exportPath.exists() is False: exportPath.mkdir() metaInf.mkdir() oebps.mkdir() imagePath.mkdir() stylesPath.mkdir() textPath.mkdir() mimetype = open(str(Path(exportPath / "mimetype")), mode="w") mimetype.write("application/epub+zip") mimetype.close() container = ET.ElementTree() cRoot = ET.Element("container") cRoot.set("version", "1.0") cRoot.set("xmlns", "urn:oasis:names:tc:opendocument:xmlns:container") container._setroot(cRoot) rootFiles = ET.Element("rootfiles") rootfile = ET.Element("rootfile") rootfile.set("full-path", "OEBPS/content.opf") rootfile.set("media-type", "application/oebps-package+xml") rootFiles.append(rootfile) cRoot.append(rootFiles) container.write(str(Path(metaInf / "container.xml")), encoding="utf-8", xml_declaration=True) # copyimages to images pagesList = [] if "EPUB" in self.pagesLocationList.keys(): coverNumber = self.configDictionary["pages"].index(self.configDictionary["cover"]) for p in self.pagesLocationList["EPUB"]: if os.path.exists(p): shutil.copy2(p, str(imagePath)) pagesList.append(str(Path(imagePath / os.path.basename(p)))) if len(self.pagesLocationList["EPUB"]) >= coverNumber: coverpageurl = pagesList[coverNumber] else: print("CPMT: Couldn't find the location for the epub files.") return False # for each image, make an xml file htmlFiles = [] for i in range(len(pagesList)): pageName = "Page" + str(i) + ".xhtml" doc = ET.ElementTree() html = ET.Element("html") doc._setroot(html) html.set("xmlns", "http://www.w3.org/1999/xhtml") html.set("xmlns:epub", "http://www.idpf.org/2007/ops") head = ET.Element("head") html.append(head) body = ET.Element("body") img = ET.Element("img") img.set("src", os.path.relpath(pagesList[i], str(textPath))) body.append(img) if pagesList[i] != coverpageurl: pagenumber = ET.Element("p") pagenumber.text = "Page " + str(i) body.append(pagenumber) html.append(body) filename = str(Path(textPath / pageName)) doc.write(filename, encoding="utf-8", xml_declaration=True) if pagesList[i] == coverpageurl: coverpagehtml = os.path.relpath(filename, str(oebps)) htmlFiles.append(filename) # opf file opfFile = ET.ElementTree() opfRoot = ET.Element("package") opfRoot.set("version", "3.0") opfRoot.set("unique-identifier", "BookId") opfRoot.set("xmlns", "http://www.idpf.org/2007/opf") opfFile._setroot(opfRoot) # metadata opfMeta = ET.Element("metadata") opfMeta.set("xmlns:dc", "http://purl.org/dc/elements/1.1/") if "language" in self.configDictionary.keys(): bookLang = ET.Element("dc:language") bookLang.text = self.configDictionary["language"] opfMeta.append(bookLang) bookTitle = ET.Element("dc:title") if "title" in self.configDictionary.keys(): bookTitle.text = str(self.configDictionary["title"]) else: bookTitle.text = "Comic with no Name" opfMeta.append(bookTitle) if "authorList" in self.configDictionary.keys(): for authorE in range(len(self.configDictionary["authorList"])): authorDict = self.configDictionary["authorList"][authorE] authorType = "dc:creator" if "role" in authorDict.keys(): if str(authorDict["role"]).lower() in ["editor", "assistant editor", "proofreader", "beta"]: authorType = "dc:contributor" author = ET.Element(authorType) authorName = [] if "last-name" in authorDict.keys(): authorName.append(authorDict["last-name"]) if "first-name" in authorDict.keys(): authorName.append(authorDict["first-name"]) if "initials" in authorDict.keys(): authorName.append(authorDict["initials"]) if "nickname" in authorDict.keys(): authorName.append("(" + authorDict["nickname"] + ")") author.text = ", ".join(authorName) opfMeta.append(author) if "role" in authorDict.keys(): author.set("id", "cre" + str(authorE)) role = ET.Element("meta") role.set("refines", "cre" + str(authorE)) role.set("scheme", "marc:relators") role.set("property", "role") role.text = str(authorDict["role"]) opfMeta.append(role) if "publishingDate" in self.configDictionary.keys(): date = ET.Element("dc:date") date.text = self.configDictionary["publishingDate"] opfMeta.append(date) description = ET.Element("dc:description") if "summary" in self.configDictionary.keys(): description.text = self.configDictionary["summary"] else: description.text = "There was no summary upon generation of this file." opfMeta.append(description) type = ET.Element("dc:type") type.text = "Comic" opfMeta.append(type) if "publisherName" in self.configDictionary.keys(): publisher = ET.Element("dc:publisher") publisher.text = self.configDictionary["publisherName"] opfMeta.append(publisher) if "isbn-number" in self.configDictionary.keys(): publishISBN = ET.Element("dc:identifier") publishISBN.text = str("urn:isbn:") + self.configDictionary["isbn-number"] opfMeta.append(publishISBN) if "license" in self.configDictionary.keys(): rights = ET.Element("dc:rights") rights.text = self.configDictionary["license"] opfMeta.append(rights) if "genre" in self.configDictionary.keys(): for g in self.configDictionary["genre"]: subject = ET.Element("dc:subject") subject.text = g opfMeta.append(subject) if "characters" in self.configDictionary.keys(): for name in self.configDictionary["characters"]: char = ET.Element("dc:subject") char.text = name opfMeta.append(char) if "format" in self.configDictionary.keys(): for format in self.configDictionary["format"]: f = ET.Element("dc:subject") f.text = format opfMeta.append(f) if "otherKeywords" in self.configDictionary.keys(): for key in self.configDictionary["otherKeywords"]: word = ET.Element("dc:subject") word.text = key opfMeta.append(word) opfRoot.append(opfMeta) opfManifest = ET.Element("manifest") toc = ET.Element("item") toc.set("id", "ncx") toc.set("href", "toc.ncx") toc.set("media-type", "application/x-dtbncx+xml") opfManifest.append(toc) for p in htmlFiles: item = ET.Element("item") item.set("id", os.path.basename(p)) item.set("href", os.path.relpath(p, str(oebps))) item.set("media-type", "application/xhtml+xml") opfManifest.append(item) for p in pagesList: item = ET.Element("item") item.set("id", os.path.basename(p)) item.set("href", os.path.relpath(p, str(oebps))) item.set("media-type", "image/png") if os.path.basename(p) == os.path.basename(coverpageurl): item.set("properties", "cover-image") opfManifest.append(item) opfRoot.append(opfManifest) opfSpine = ET.Element("spine") opfSpine.set("toc", "ncx") for p in htmlFiles: item = ET.Element("itemref") item.set("idref", os.path.basename(p)) opfSpine.append(item) opfRoot.append(opfSpine) opfGuide = ET.Element("guide") if coverpagehtml is not None and coverpagehtml.isspace() is False and len(coverpagehtml) > 0: item = ET.Element("reference") item.set("type", "cover") item.set("title", "Cover") item.set("href", coverpagehtml) opfRoot.append(opfGuide) opfFile.write(str(Path(oebps / "content.opf")), encoding="utf-8", xml_declaration=True) # toc tocDoc = ET.ElementTree() ncx = ET.Element("ncx") ncx.set("version", "2005-1") ncx.set("xmlns", "http://www.daisy.org/z3986/2005/ncx/") tocDoc._setroot(ncx) tocHead = ET.Element("head") metaID = ET.Element("meta") metaID.set("content", "ID_UNKNOWN") metaID.set("name", "dtb:uid") tocHead.append(metaID) metaDepth = ET.Element("meta") metaDepth.set("content", str(0)) metaDepth.set("name", "dtb:depth") tocHead.append(metaDepth) metaTotal = ET.Element("meta") metaTotal.set("content", str(0)) metaTotal.set("name", "dtb:totalPageCount") tocHead.append(metaTotal) metaMax = ET.Element("meta") metaMax.set("content", str(0)) metaMax.set("name", "dtb:maxPageNumber") tocHead.append(metaDepth) ncx.append(tocHead) docTitle = ET.Element("docTitle") text = ET.Element("text") if "title" in self.configDictionary.keys(): text.text = str(self.configDictionary["title"]) else: text.text = "Comic with no Name" docTitle.append(text) ncx.append(docTitle) navmap = ET.Element("navMap") navPoint = ET.Element("navPoint") navPoint.set("id", "navPoint-1") navPoint.set("playOrder", "1") navLabel = ET.Element("navLabel") navLabelText = ET.Element("text") navLabelText.text = "Start" navLabel.append(navLabelText) navContent = ET.Element("content") navContent.set("src", os.path.relpath(htmlFiles[0], str(oebps))) navPoint.append(navLabel) navPoint.append(navContent) navmap.append(navPoint) ncx.append(navmap) tocDoc.write(str(Path(oebps / "toc.ncx")), encoding="utf-8", xml_declaration=True) self.package_epub() return True def save_out_pngs(self, sizesList): # A small fix to ensure crop to guides is set. if "cropToGuides" not in self.configDictionary.keys(): self.configDictionary["cropToGuides"] = False # Check if we have pages at all... if "pages" in self.configDictionary.keys(): # Check if there's export methods, and if so make sure the appropriate dictionaries are initialised. if len(sizesList.keys()) < 1: print("CPMT: Export failed because there's no export methods set.") return False else: for key in sizesList.keys(): self.pagesLocationList[key] = [] # Get the appropriate paths. path = Path(self.projectURL) exportPath = path / self.configDictionary["exportLocation"] pagesList = self.configDictionary["pages"] fileName = str(exportPath) # Create a progress dialog. progress = QProgressDialog("Preparing export.", str(), 0, len(pagesList)) progress.setWindowTitle("Exporting comic...") progress.setCancelButton(None) timer = QElapsedTimer() timer.start() qApp.processEvents() for p in range(0, len(pagesList)): # Update the label in the progress dialog. progress.setValue(p) timePassed = timer.elapsed() if (p > 0): timeEstimated = (len(pagesList) - p) * (timePassed / p) passedString = str(int(timePassed / 60000)) + ":" + format(int(timePassed / 1000), "02d") + ":" + format(timePassed % 1000, "03d") estimatedString = str(int(timeEstimated / 60000)) + ":" + format(int(timeEstimated / 1000), "02d") + ":" + format(int(timeEstimated % 1000), "03d") progress.setLabelText(str(i18n("{pages} of {pagesTotal} done. \nTime passed: {passedString}:\n Estimated:{estimated}")).format(pages=p, pagesTotal=len(pagesList), passedString=passedString, estimated=estimatedString)) qApp.processEvents() # Get the appropriate url and open the page. url = os.path.join(self.projectURL, pagesList[p]) page = Application.openDocument(url) page.waitForDone() # remove layers and flatten. labelList = self.configDictionary["labelsToRemove"] panelsAndText = [] # These three lines are what is causing the page not to close. root = page.rootNode() self.removeLayers(labelList, root) self.getPanelsAndText(root, panelsAndText) # We'll need the offset and scale for aligning the panels and text correctly. We're getting this from the CBZ pageData = {} pageData["vector"] = panelsAndText tree = ET.fromstring(page.documentInfo()) pageData["title"] = page.name() calligra = "{http://www.calligra.org/DTD/document-info}" about = tree.find(calligra + "about") keywords = about.find(calligra + "keyword") keys = str(keywords.text).split(",") pKeys = [] for key in keys: if key in self.pageKeys: pKeys.append(key) pageData["keys"] = pKeys page.flatten() page.waitForDone() batchsave = Application.batchmode() Application.setBatchmode(True) # Start making the format specific copy. for key in sizesList.keys(): w = sizesList[key] # copy over data projection = page.clone() projection.setBatchmode(True) # Crop. Cropping per guide only happens if said guides have been found. if w["Crop"] is True: listHGuides = [] listHGuides = page.horizontalGuides() listHGuides.sort() for i in range(len(listHGuides) - 1, 0, -1): if listHGuides[i] < 0 or listHGuides[i] > page.height(): listHGuides.pop(i) listVGuides = page.verticalGuides() listVGuides.sort() for i in range(len(listVGuides) - 1, 0, -1): if listVGuides[i] < 0 or listVGuides[i] > page.width(): listVGuides.pop(i) if self.configDictionary["cropToGuides"] and len(listVGuides) > 1: cropx = listVGuides[0] cropw = listVGuides[-1] - cropx else: cropx = self.configDictionary["cropLeft"] cropw = page.width() - self.configDictionary["cropRight"] - cropx if self.configDictionary["cropToGuides"] and len(listHGuides) > 1: cropy = listHGuides[0] croph = listHGuides[-1] - cropy else: cropy = self.configDictionary["cropTop"] croph = page.height() - self.configDictionary["cropBottom"] - cropy projection.crop(cropx, cropy, cropw, croph) projection.waitForDone() qApp.processEvents() # resize appropriately res = page.resolution() listScales = [projection.width(), projection.height(), res, res] projectionOldSize = [projection.width(), projection.height()] sizesCalc = sizesCalculator() listScales = sizesCalc.get_scale_from_resize_config(config=w, listSizes=listScales) projection.unlock() projection.scaleImage(listScales[0], listScales[1], listScales[2], listScales[3], "bicubic") projection.waitForDone() qApp.processEvents() # png, gif and other webformats should probably be in 8bit srgb at maximum. if key != "TIFF": if (projection.colorModel() != "RGBA" and projection.colorModel() != "GRAYA") or projection.colorDepth() != "U8": projection.setColorSpace("RGBA", "U8", "sRGB built-in") else: # Tiff on the other hand can handle all the colormodels, but can only handle integer bit depths. # Tiff is intended for print output, and 16 bit integer will be sufficient. if projection.colorDepth() != "U8" or projection.colorDepth() != "U16": projection.setColorSpace(page.colorModel(), "U16", page.colorProfile()) # save # Make sure the folder name for this export exists. It'll allow us to keep the # export folders nice and clean. folderName = str(key + "-" + w["FileType"]) if Path(exportPath / folderName).exists() is False: Path.mkdir(exportPath / folderName) # Get a nice and descriptive fle name. fn = os.path.join(str(Path(exportPath / folderName)), "page_" + format(p, "03d") + "_" + str(listScales[0]) + "x" + str(listScales[1]) + "." + w["FileType"]) # Finally save and add the page to a list of pages. This will make it easy for the packaging function to # find the pages and store them. projection.exportImage(fn, InfoObject()) projection.waitForDone() qApp.processEvents() if key == "CBZ": transform = {} transform["offsetX"] = cropx transform["offsetY"] = cropy transform["resDiff"] = page.resolution() / 72 transform["scaleWidth"] = projection.width() / projectionOldSize[0] transform["scaleHeight"] = projection.height() / projectionOldSize[1] pageData["transform"] = transform self.pagesLocationList[key].append(fn) projection.close() self.acbfPageData.append(pageData) page.close() progress.setValue(len(pagesList)) Application.setBatchmode(batchsave) # TODO: Check what or whether memory leaks are still caused and otherwise remove the entry below. print("CPMT: Export has finished. If there are memory leaks, they are caused by file layers.") return True print("CPMT: Export not happening because there aren't any pages.") return False """ Function to get the panel and text data. """ def getPanelsAndText(self, node, list): textLayersToSearch = ["text"] panelLayersToSearch = ["panels"] if "textLayerNames" in self.configDictionary.keys(): textLayersToSearch = self.configDictionary["textLayerNames"] if "panelLayerNames" in self.configDictionary.keys(): panelLayersToSearch = self.configDictionary["panelLayerNames"] if node.type() == "vectorlayer": for name in panelLayersToSearch: if str(name).lower() in str(node.name()).lower(): for shape in node.shapes(): if (shape.type() == "groupshape"): self.getPanelsAndTextVector(shape, list) else: self.handleShapeDescription(shape, list) for name in textLayersToSearch: if str(name).lower() in str(node.name()).lower(): for shape in node.shapes(): if (shape.type() == "groupshape"): self.getPanelsAndTextVector(shape, list, True) else: self.handleShapeDescription(shape, list, True) else: if node.childNodes(): for child in node.childNodes(): self.getPanelsAndText(node=child, list=list) """ Function to get the panel and text data from a group shape """ def getPanelsAndTextVector(self, group, list, textOnly=False): for shape in group.shapes(): if (shape.type() == "groupshape"): self.getPanelsAndTextVector(shape, list, textOnly) else: self.handleShapeDescription(shape, list, textOnly) """ Function to get text and panels in a format that acbf will accept TODO: move this to a new file. """ def handleShapeDescription(self, shape, list, textOnly=False): if (shape.type() != "KoSvgTextShapeID" and textOnly is True): return shapeDesc = {} shapeDesc["name"] = shape.name() rect = shape.boundingBox() listOfPoints = [rect.topLeft(), rect.topRight(), rect.bottomRight(), rect.bottomLeft()] shapeDoc = minidom.parseString(shape.toSvg()) docElem = shapeDoc.documentElement svgRegExp = re.compile('[MLCSQHVATmlzcqshva]\d+\.?\d* \d+\.?\d*') transform = docElem.getAttribute("transform") coord = [] adjust = QTransform() # TODO: If we get global transform api, use that instead of parsing manually. if "translate" in transform: transform = transform.replace('translate(', '') for c in transform[:-1].split(" "): coord.append(float(c)) if len(coord) < 2: coord.append(coord[0]) adjust = QTransform(1, 0, 0, 1, coord[0], coord[1]) if "matrix" in transform: transform = transform.replace('matrix(', '') for c in transform[:-1].split(" "): coord.append(float(c)) adjust = QTransform(coord[0], coord[1], coord[2], coord[3], coord[4], coord[5]) path = QPainterPath() if docElem.localName == "path": dVal = docElem.getAttribute("d") listOfSvgStrings = [" "] listOfSvgStrings = svgRegExp.findall(dVal) if listOfSvgStrings: listOfPoints = [] for l in listOfSvgStrings: line = l[1:] coordinates = line.split(" ") if len(coordinates) < 2: coordinates.append(coordinates[0]) x = float(coordinates[-2]) y = float(coordinates[-1]) offset = QPointF() if l.islower(): offset = listOfPoints[0] if l.lower().startswith("m"): path.moveTo(QPointF(x, y) + offset) elif l.lower().startswith("h"): y = listOfPoints[-1].y() path.lineTo(QPointF(x, y) + offset) elif l.lower().startswith("v"): x = listOfPoints[-1].x() path.lineTo(QPointF(x, y) + offset) else: path.lineTo(QPointF(x, y) + offset) path.setFillRule(Qt.WindingFill) for polygon in path.simplified().toSubpathPolygons(adjust): for point in polygon: listOfPoints.append(point) elif docElem.localName == "rect": listOfPoints = [] if (docElem.hasAttribute("x")): x = float(docElem.getAttribute("x")) else: x = 0 if (docElem.hasAttribute("y")): y = float(docElem.getAttribute("y")) else: y = 0 w = float(docElem.getAttribute("width")) h = float(docElem.getAttribute("height")) path.addRect(QRectF(x, y, w, h)) for point in path.toFillPolygon(adjust): listOfPoints.append(point) elif docElem.localName == "ellipse": listOfPoints = [] if (docElem.hasAttribute("cx")): x = float(docElem.getAttribute("cx")) else: x = 0 if (docElem.hasAttribute("cy")): y = float(docElem.getAttribute("cy")) else: y = 0 ry = float(docElem.getAttribute("ry")) rx = float(docElem.getAttribute("rx")) path.addEllipse(QPointF(x, y), rx, ry) for point in path.toFillPolygon(adjust): listOfPoints.append(point) shapeDesc["boundingBox"] = listOfPoints if (shape.type() == "KoSvgTextShapeID" and textOnly is True): textRoot = ET.fromstring(shape.toSvg()) paragraph = ET.Element("p") if (len(textRoot) > 0): self.parseTextChildren(textRoot, paragraph) shapeDesc["text"] = ET.tostring(paragraph, "unicode") list.append(shapeDesc) """ Function to parse svg text to acbf ready text TODO: Move to a new file. """ def parseTextChildren(self, elRead, elWrite): if elRead.text is not None: if len(elWrite) > 0: if (elWrite[-1].tail is None): elWrite[-1].tail = str() elWrite[-1].tail = " ".join([elWrite[-1].tail, elRead.text]) else: if elWrite.text is None: elWrite.text = elRead.text else: elWrite.text = " ".join([elWrite.text, elRead.text]) for childNode in elRead: fontWeight = childNode.get("font-weight") fontItalic = childNode.get("font-style") fontStrikeThrough = childNode.get("text-decoration") fontBaseLine = childNode.get("baseline-shift") newElementMade = False if fontItalic is not None: if (fontItalic == "italic"): newElement = ET.Element("Emphasis") newElementMade = True elif fontWeight is not None: if (fontWeight == "bold" or int(fontWeight) > 400): newElement = ET.Element("Strong") newElementMade = True elif fontStrikeThrough is not None: if (fontStrikeThrough == "line-through"): newElement = ET.Element("Strikethrough") newElementMade = True elif fontBaseLine is not None: if (fontBaseLine == "super"): newElement = ET.Element("Sup") newElementMade = True elif (fontBaseLine == "sub"): newElement = ET.Element("Sub") newElementMade = True if newElementMade is True: if (len(childNode) > 0): self.parseTextChildren(childNode, newElement) else: newElement.text = childNode.text elWrite.append(newElement) else: if (len(childNode) > 0): self.parseTextChildren(childNode, elWrite) else: if len(elWrite) > 0: if (elWrite[-1].tail is None): elWrite[-1].tail = str() elWrite[-1].tail = " ".join([elWrite[-1].tail, childNode.text]) else: if elWrite.text is None: elWrite.text = str() elWrite.text = " ".join([elWrite.text, childNode.text]) """ Function to remove layers when they have the given labels. If not, but the node does have children, check those too. """ def removeLayers(self, labels, node): if node.colorLabel() in labels: node.remove() else: if node.childNodes(): for child in node.childNodes(): self.removeLayers(labels, node=child) """ Write the Advanced Comic Book Data xml file. http://acbf.wikia.com/wiki/ACBF_Specifications """ def write_acbf_meta_data(self): acbfGenreList = ["science_fiction", "fantasy", "adventure", "horror", "mystery", "crime", "military", "real_life", "superhero", "humor", "western", "manga", "politics", "caricature", "sports", "history", "biography", "education", "computer", "religion", "romance", "children", "non-fiction", "adult", "alternative", "other", "artbook"] acbfAuthorRolesList = ["Writer", "Adapter", "Artist", "Penciller", "Inker", "Colorist", "Letterer", "Cover Artist", "Photographer", "Editor", "Assistant Editor", "Translator", "Other", "Designer"] title = self.configDictionary["projectName"] if "title" in self.configDictionary.keys(): title = self.configDictionary["title"] location = str(os.path.join(self.projectURL, self.configDictionary["exportLocation"], "metadata", title + ".acbf")) document = minidom.Document() root = document.createElement("ACBF") root.setAttribute("xmlns", "http://www.fictionbook-lib.org/xml/acbf/1.0") document.appendChild(root) meta = document.createElement("meta-data") bookInfo = document.createElement("book-info") if "authorList" in self.configDictionary.keys(): for authorE in range(len(self.configDictionary["authorList"])): author = document.createElement("author") authorDict = self.configDictionary["authorList"][authorE] if "first-name" in authorDict.keys(): authorN = document.createElement("first-name") authorN.appendChild(document.createTextNode(str(authorDict["first-name"]))) author.appendChild(authorN) if "last-name" in authorDict.keys(): authorN = document.createElement("last-name") authorN.appendChild(document.createTextNode(str(authorDict["last-name"]))) author.appendChild(authorN) if "initials" in authorDict.keys(): authorN = document.createElement("middle-name") authorN.appendChild(document.createTextNode(str(authorDict["initials"]))) author.appendChild(authorN) if "nickname" in authorDict.keys(): authorN = document.createElement("nickname") authorN.appendChild(document.createTextNode(str(authorDict["nickname"]))) author.appendChild(authorN) if "homepage" in authorDict.keys(): authorN = document.createElement("home-page") authorN.appendChild(document.createTextNode(str(authorDict["homepage"]))) author.appendChild(authorN) if "email" in authorDict.keys(): authorN = document.createElement("email") authorN.appendChild(document.createTextNode(str(authorDict["email"]))) author.appendChild(authorN) if "role" in authorDict.keys(): if str(authorDict["role"]).title() in acbfAuthorRolesList: author.setAttribute("activity", str(authorDict["role"])) if "language" in authorDict.keys(): author.setAttribute("lang", str(authorDict["language"])) bookInfo.appendChild(author) bookTitle = document.createElement("book-title") if "title" in self.configDictionary.keys(): bookTitle.appendChild(document.createTextNode(str(self.configDictionary["title"]))) else: bookTitle.appendChild(document.createTextNode(str("Comic with no Name"))) bookInfo.appendChild(bookTitle) extraGenres = [] if "genre" in self.configDictionary.keys(): for genre in self.configDictionary["genre"]: genreModified = str(genre).lower() genreModified.replace(" ", "_") if genreModified in acbfGenreList: bookGenre = document.createElement("genre") bookGenre.appendChild(document.createTextNode(str(genreModified))) bookInfo.appendChild(bookGenre) else: extraGenres.appendChild(genre) annotation = document.createElement("annotation") if "summary" in self.configDictionary.keys(): paragraphList = str(self.configDictionary["summary"]).split("\n") for para in paragraphList: p = document.createElement("p") p.appendChild(document.createTextNode(str(para))) annotation.appendChild(p) else: p = document.createElement("p") p.appendChild(document.createTextNode(str("There was no summary upon generation of this file."))) annotation.appendChild(p) bookInfo.appendChild(annotation) if "characters" in self.configDictionary.keys(): character = document.createElement("characters") for name in self.configDictionary["characters"]: char = document.createElement("name") char.appendChild(document.createTextNode(str(name))) character.appendChild(char) bookInfo.appendChild(character) keywords = document.createElement("keywords") stringKeywordsList = [] for key in extraGenres: stringKeywordsList.append(str(key)) if "otherKeywords" in self.configDictionary.keys(): for key in self.configDictionary["otherKeywords"]: stringKeywordsList.append(str(key)) if "format" in self.configDictionary.keys(): for key in self.configDictionary["format"]: stringKeywordsList.append(str(key)) keywords.appendChild(document.createTextNode(", ".join(stringKeywordsList))) bookInfo.appendChild(keywords) coverpageurl = "" coverpage = document.createElement("coverpage") if "pages" in self.configDictionary.keys(): if "cover" in self.configDictionary.keys(): pageList = [] pageList = self.configDictionary["pages"] coverNumber = max([pageList.index(self.configDictionary["cover"]), 0]) image = document.createElement("image") if len(self.pagesLocationList["CBZ"]) >= coverNumber: coverpageurl = self.pagesLocationList["CBZ"][coverNumber] image.setAttribute("href", os.path.basename(coverpageurl)) coverpage.appendChild(image) bookInfo.appendChild(coverpage) if "language" in self.configDictionary.keys(): language = document.createElement("languages") textlayer = document.createElement("text-layer") textlayer.setAttribute("lang", self.configDictionary["language"]) textlayer.setAttribute("show", "False") textlayerNative = document.createElement("text-layer") textlayerNative.setAttribute("lang", self.configDictionary["language"]) textlayerNative.setAttribute("show", "True") language.appendChild(textlayer) language.appendChild(textlayerNative) bookInfo.appendChild(language) bookTitle.setAttribute("lang", self.configDictionary["language"]) annotation.setAttribute("lang", self.configDictionary["language"]) keywords.setAttribute("lang", self.configDictionary["language"]) #database = document.createElement("databaseref") # bookInfo.appendChild(database) if "seriesName" in self.configDictionary.keys(): sequence = document.createElement("sequence") sequence.setAttribute("title", self.configDictionary["seriesName"]) if "seriesVolume" in self.configDictionary.keys(): sequence.setAttribute("volume", str(self.configDictionary["seriesVolume"])) if "seriesNumber" in self.configDictionary.keys(): sequence.appendChild(document.createTextNode(str(self.configDictionary["seriesNumber"]))) else: sequence.appendChild(document.createTextNode(str(0))) bookInfo.appendChild(sequence) contentrating = document.createElement("content-rating") if "rating" in self.configDictionary.keys(): contentrating.appendChild(document.createTextNode(str(self.configDictionary["rating"]))) else: contentrating.appendChild(document.createTextNode(str("Unrated."))) if "ratingSystem" in self.configDictionary.keys(): contentrating.setAttribute("type", self.configDictionary["ratingSystem"]) bookInfo.appendChild(contentrating) if "readingDirection" in self.configDictionary.keys(): readingDirection = document.createElement("reading-direction") if self.configDictionary["readingDirection"] is "rightToLeft": readingDirection.appendChild(document.createTextNode(str("RTL"))) else: readingDirection.appendChild(document.createTextNode(str("LTR"))) bookInfo.appendChild(readingDirection) meta.appendChild(bookInfo) publisherInfo = document.createElement("publish-info") if "publisherName" in self.configDictionary.keys(): publisherName = document.createElement("publisher") publisherName.appendChild(document.createTextNode(str(self.configDictionary["publisherName"]))) publisherInfo.appendChild(publisherName) if "publishingDate" in self.configDictionary.keys(): publishingDate = document.createElement("publish-date") publishingDate.setAttribute("value", self.configDictionary["publishingDate"]) publishingDate.appendChild(document.createTextNode(QDate.fromString(self.configDictionary["publishingDate"], Qt.ISODate).toString(Qt.SystemLocaleLongDate))) publisherInfo.appendChild(publishingDate) if "publisherCity" in self.configDictionary.keys(): publishCity = document.createElement("city") publishCity.appendChild(document.createTextNode(str(self.configDictionary["publisherCity"]))) publisherInfo.appendChild(publishCity) if "isbn-number" in self.configDictionary.keys(): publishISBN = document.createElement("isbn") publishISBN.appendChild(document.createTextNode(str(self.configDictionary["isbn-number"]))) publisherInfo.appendChild(publishISBN) if "license" in self.configDictionary.keys(): license = self.configDictionary["license"] if license.isspace() is False and len(license) > 0: publishLicense = document.createElement("license") publishLicense.appendChild(document.createTextNode(str(self.configDictionary["license"]))) publisherInfo.appendChild(publishLicense) meta.appendChild(publisherInfo) documentInfo = document.createElement("document-info") # TODO: ACBF apparantly uses first/middle/last/nick/email/homepage for the document auhtor too... # The following code compensates for me not understanding this initially. This still needs # adjustments in the gui. if "acbfAuthor" in self.configDictionary.keys(): if isinstance(self.configDictionary["acbfAuthor"], list): for e in self.configDictionary["acbfAuthor"]: acbfAuthor = document.createElement("author") authorDict = self.configDictionary["acbfAuthor"][e] if "first-name" in authorDict.keys(): authorN = document.createElement("first-name") authorN.appendChild(document.createTextNode(str(authorDict["first-name"]))) acbfAuthor.appendChild(authorN) if "last-name" in authorDict.keys(): authorN = document.createElement("last-name") authorN.appendChild(document.createTextNode(str(authorDict["last-name"]))) acbfAuthor.appendChild(authorN) if "initials" in authorDict.keys(): authorN = document.createElement("middle-name") authorN.appendChild(document.createTextNode(str(authorDict["initials"]))) acbfAuthor.appendChild(authorN) if "nickname" in authorDict.keys(): authorN = document.createElement("nickname") authorN.appendChild(document.createTextNode(str(authorDict["nickname"]))) acbfAuthor.appendChild(authorN) if "homepage" in authorDict.keys(): authorN = document.createElement("homepage") authorN.appendChild(document.createTextNode(str(authorDict["home-page"]))) acbfAuthor.appendChild(authorN) if "email" in authorDict.keys(): authorN = document.createElement("email") authorN.appendChild(document.createTextNode(str(authorDict["email"]))) acbfAuthor.appendChild(authorN) if "language" in authorDict.keys(): acbfAuthor.setAttribute("lang", str(authorDict["language"])) documentInfo.appendChild(acbfAuthor) else: acbfAuthor = document.createElement("author") acbfAuthorNick = document.createElement("nickname") acbfAuthorNick.appendChild(document.createTextNode(str(self.configDictionary["acbfAuthor"]))) acbfAuthor.appendChild(acbfAuthorNick) documentInfo.appendChild(acbfAuthor) else: acbfAuthor = document.createElement("author") acbfAuthorNick = document.createElement("nickname") acbfAuthorNick.appendChild(document.createTextNode(str("Anon"))) acbfAuthor.appendChild(acbfAuthorNick) documentInfo.appendChild(acbfAuthor) acbfDate = document.createElement("creation-date") now = QDate.currentDate() acbfDate.setAttribute("value", now.toString(Qt.ISODate)) acbfDate.appendChild(document.createTextNode(str(now.toString(Qt.SystemLocaleLongDate)))) documentInfo.appendChild(acbfDate) if "acbfSource" in self.configDictionary.keys(): acbfSource = document.createElement("source") acbfSourceP = document.createElement("p") acbfSourceP.appendChild(document.createTextNode(str(self.configDictionary["acbfSource"]))) acbfSource.appendChild(acbfSourceP) documentInfo.appendChild(acbfSource) if "acbfID" in self.configDictionary.keys(): acbfID = document.createElement("id") acbfID.appendChild(document.createTextNode(str(self.configDictionary["acbfID"]))) documentInfo.appendChild(acbfID) if "acbfVersion" in self.configDictionary.keys(): acbfVersion = document.createElement("version") acbfVersion.appendChild(document.createTextNode(str(self.configDictionary["acbfVersion"]))) documentInfo.appendChild(acbfVersion) if "acbfHistory" in self.configDictionary.keys(): acbfHistory = document.createElement("history") for h in self.configDictionary["acbfHistory"]: p = document.createElement("p") p.appendChild(document.createTextNode(str(h))) acbfHistory.appendChild(p) documentInfo.appendChild(acbfHistory) meta.appendChild(documentInfo) root.appendChild(meta) body = document.createElement("body") for p in range(0, len(self.pagesLocationList["CBZ"])): page = self.pagesLocationList["CBZ"][p] language = "en" if "language" in self.configDictionary.keys(): language = self.configDictionary["language"] textLayer = document.createElement("text-layer") textLayer.setAttribute("lang", language) pageData = self.acbfPageData[p] transform = pageData["transform"] frameList = [] for v in pageData["vector"]: boundingBoxText = [] for point in v["boundingBox"]: offset = QPointF(transform["offsetX"], transform["offsetY"]) pixelPoint = QPointF(point.x() * transform["resDiff"], point.y() * transform["resDiff"]) newPoint = pixelPoint - offset x = int(newPoint.x() * transform["scaleWidth"]) y = int(newPoint.y() * transform["scaleHeight"]) pointText = str(x) + "," + str(y) boundingBoxText.append(pointText) if "text" in v.keys(): textArea = document.createElement("text-area") textArea.setAttribute("points", " ".join(boundingBoxText)) # TODO: Rotate will require proper global transform api as transform info is not written intotext. #textArea.setAttribute("text-rotation", str(v["rotate"])) paragraph = minidom.parseString(v["text"]) textArea.appendChild(paragraph.documentElement) textLayer.appendChild(textArea) else: f = {} f["points"] = " ".join(boundingBoxText) frameList.append(f) if page is not coverpageurl: pg = document.createElement("page") image = document.createElement("image") image.setAttribute("href", os.path.basename(page)) pg.appendChild(image) if "acbf_title" in pageData["keys"]: title = document.createElement("title") title.setAttribute("lang", language) title.appendChild(document.createTextNode(str(pageData["title"]))) pg.appendChild(title) if "acbf_none" in pageData["keys"]: pg.setAttribute("transition", "none") if "acbf_blend" in pageData["keys"]: pg.setAttribute("transition", "blend") if "acbf_fade" in pageData["keys"]: pg.setAttribute("transition", "fade") if "acbf_horizontal" in pageData["keys"]: pg.setAttribute("transition", "scroll_right") if "acbf_vertical" in pageData["keys"]: pg.setAttribute("transition", "scroll_down") for f in frameList: frame = document.createElement("frame") frame.setAttribute("points", f["points"]) pg.appendChild(frame) pg.appendChild(textLayer) body.appendChild(pg) else: for f in frameList: frame = document.createElement("frame") frame.setAttribute("points", f["points"]) coverpage.appendChild(frame) coverpage.appendChild(textLayer) root.appendChild(body) f = open(location, 'w', newline="", encoding="utf-8") f.write(document.toprettyxml(indent=" ")) f.close() self.acbfLocation = location success = True success = self.createStandAloneACBF(document) return success def createStandAloneACBF(self, document): title = self.configDictionary["projectName"] if "title" in self.configDictionary.keys(): title = self.configDictionary["title"] location = str(os.path.join(self.projectURL, self.configDictionary["exportLocation"], title + ".acbf")) root = document.getElementsByTagName("ACBF")[0] meta = root.getElementsByTagName("meta-data")[0] bookInfo = meta.getElementsByTagName("book-info")[0] cover = bookInfo.getElementsByTagName("coverpage")[0] body = root.getElementsByTagName("body")[0] pages = body.getElementsByTagName("page") if (cover): pages.append(cover) data = document.createElement("data") # Covert pages to base64 strings. for i in range(0, len(pages)): image = pages[i].getElementsByTagName("image")[0] href = image.getAttribute("href") for p in self.pagesLocationList["CBZ"]: if href in p: binary = document.createElement("binary") binary.setAttribute("id", href) imageFile = QImage() imageFile.load(p) imageData = QByteArray() buffer = QBuffer(imageData) imageFile.save(buffer, "PNG") # For now always embed as png. contentType = "image/png" binary.setAttribute("content-type", contentType) binary.appendChild(document.createTextNode(str(bytearray(imageData.toBase64()).decode("ascii")))) image.setAttribute("href", "#" + href) data.appendChild(binary) root.appendChild(data) f = open(location, 'w', newline="", encoding="utf-8") f.write(document.toprettyxml(indent=" ")) f.close() return True """ Write a CoMet xml file to url """ def write_comet_meta_data(self): title = self.configDictionary["projectName"] if "title" in self.configDictionary.keys(): title = self.configDictionary["title"] location = str(os.path.join(self.projectURL, self.configDictionary["exportLocation"], "metadata", title + " CoMet.xml")) document = minidom.Document() root = document.createElement("comet") root.setAttribute("xmlns:comet", "http://www.denvog.com/comet/") root.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance") root.setAttribute("xsi:schemaLocation", "http://www.denvog.com http://www.denvog.com/comet/comet.xsd") document.appendChild(root) title = document.createElement("title") if "title" in self.configDictionary.keys(): title.appendChild(document.createTextNode(str(self.configDictionary["title"]))) else: title.appendChild(document.createTextNode(str("Untitled Comic"))) root.appendChild(title) description = document.createElement("description") if "summary" in self.configDictionary.keys(): description.appendChild(document.createTextNode(str(self.configDictionary["summary"]))) else: description.appendChild(document.createTextNode(str("There was no summary upon generation of this file."))) root.appendChild(description) if "seriesName" in self.configDictionary.keys(): series = document.createElement("series") series.appendChild(document.createTextNode(str(self.configDictionary["seriesName"]))) root.appendChild(series) if "seriesNumber" in self.configDictionary.keys(): issue = document.createElement("issue") issue.appendChild(document.createTextNode(str(self.configDictionary["seriesNumber"]))) root.appendChild(issue) if "seriesVolume" in self.configDictionary.keys(): volume = document.createElement("volume") volume.appendChild(document.createTextNode(str(self.configDictionary["seriesVolume"]))) root.appendChild(volume) if "publisherName" in self.configDictionary.keys(): publisher = document.createElement("publisher") publisher.appendChild(document.createTextNode(str(self.configDictionary["publisherName"]))) root.appendChild(publisher) if "publishingDate" in self.configDictionary.keys(): date = document.createElement("date") date.appendChild(document.createTextNode(str(self.configDictionary["publishingDate"]))) root.appendChild(date) if "genre" in self.configDictionary.keys(): for genreE in self.configDictionary["genre"]: genre = document.createElement("genre") genre.appendChild(document.createTextNode(str(genreE))) root.appendChild(genre) if "characters" in self.configDictionary.keys(): for char in self.configDictionary["characters"]: character = document.createElement("character") character.appendChild(document.createTextNode(str(char))) root.appendChild(character) if "format" in self.configDictionary.keys(): format = document.createElement("format") format.appendChild(document.createTextNode(str(",".join(self.configDictionary["format"])))) root.appendChild(format) if "language" in self.configDictionary.keys(): language = document.createElement("language") language.appendChild(document.createTextNode(str(self.configDictionary["language"]))) root.appendChild(language) if "rating" in self.configDictionary.keys(): rating = document.createElement("rating") rating.appendChild(document.createTextNode(str(self.configDictionary["rating"]))) root.appendChild(rating) #rights = document.createElement("rights") if "pages" in self.configDictionary.keys(): pages = document.createElement("pages") pages.appendChild(document.createTextNode(str(len(self.configDictionary["pages"])))) root.appendChild(pages) if "isbn-number" in self.configDictionary.keys(): identifier = document.createElement("identifier") identifier.appendChild(document.createTextNode(str(self.configDictionary["isbn-number"]))) root.appendChild(identifier) if "authorList" in self.configDictionary.keys(): for authorE in range(len(self.configDictionary["authorList"])): author = document.createElement("creator") authorDict = self.configDictionary["authorList"][authorE] if "role" in authorDict.keys(): if str(authorDict["role"]).lower() in ["writer", "penciller", "editor", "assistant editor", "cover artist", "letterer", "inker", "colorist"]: if str(authorDict["role"]).lower() is "cover artist": author = document.createElement("coverDesigner") elif str(authorDict["role"]).lower() is "assistant editor": author = document.createElement("editor") else: author = document.createElement(str(authorDict["role"]).lower()) stringName = [] if "last-name" in authorDict.keys(): stringName.append(authorDict["last-name"]) if "first-name" in authorDict.keys(): stringName.append(authorDict["first-name"]) if "nickname" in authorDict.keys(): stringName.append("(" + authorDict["nickname"] + ")") author.appendChild(document.createTextNode(str(",".join(stringName)))) root.appendChild(author) if "pages" in self.configDictionary.keys(): if "cover" in self.configDictionary.keys(): pageList = [] pageList = self.configDictionary["pages"] coverNumber = pageList.index(self.configDictionary["cover"]) if len(self.pagesLocationList["CBZ"]) >= coverNumber: coverImage = document.createElement("coverImage") coverImage.appendChild(document.createTextNode(str(os.path.basename(self.pagesLocationList["CBZ"][coverNumber])))) root.appendChild(coverImage) readingDirection = document.createElement("readingDirection") readingDirection.appendChild(document.createTextNode(str("ltr"))) if "readingDirection" in self.configDictionary.keys(): if self.configDictionary["readingDirection"] is "rightToLeft": readingDirection.appendChild(document.createTextNode(str("rtl"))) root.appendChild(readingDirection) f = open(location, 'w', newline="", encoding="utf-8") f.write(document.toprettyxml(indent=" ")) f.close() self.cometLocation = location return True """ The comicrack information is sorta... incomplete, so no idea if the following is right... I can't check in any case: It is a windows application. """ def write_comic_rack_info(self): location = str(os.path.join(self.projectURL, self.configDictionary["exportLocation"], "metadata", "ComicInfo.xml")) document = minidom.Document() root = document.createElement("ComicInfo") root.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance") root.setAttribute("xmlns:xsd", "http://www.w3.org/2001/XMLSchema") title = document.createElement("Title") if "title" in self.configDictionary.keys(): title.appendChild(document.createTextNode(str(self.configDictionary["title"]))) else: title.appendChild(document.createTextNode(str("Untitled Comic"))) root.appendChild(title) description = document.createElement("Summary") if "summary" in self.configDictionary.keys(): description.appendChild(document.createTextNode(str(self.configDictionary["summary"]))) else: description.appendChild(document.createTextNode(str("There was no summary upon generation of this file."))) root.appendChild(description) if "seriesNumber" in self.configDictionary.keys(): number = document.createElement("Number") number.appendChild(document.createTextNode(str(self.configDictionary["seriesNumber"]))) root.appendChild(number) if "publishingDate" in self.configDictionary.keys(): date = QDate.fromString(self.configDictionary["publishingDate"], Qt.ISODate) publishYear = document.createElement("Year") publishYear.appendChild(document.createTextNode(str(date.year()))) publishMonth = document.createElement("Month") publishMonth.appendChild(document.createTextNode(str(date.month()))) root.appendChild(publishYear) root.appendChild(publishMonth) if "format" in self.configDictionary.keys(): for form in self.configDictionary["format"]: formattag = document.createElement("Format") formattag.appendChild(document.createTextNode(str(form))) root.appendChild(formattag) if "otherKeywords" in self.configDictionary.keys(): tags = document.createElement("Tags") tags.appendChild(document.createTextNode(str(", ".join(self.configDictionary["otherKeywords"])))) root.appendChild(tags) if "authorList" in self.configDictionary.keys(): for authorE in range(len(self.configDictionary["authorList"])): author = document.createElement("Writer") authorDict = self.configDictionary["authorList"][authorE] if "role" in authorDict.keys(): if str(authorDict["role"]).lower() in ["writer", "penciller", "editor", "assistant editor", "cover artist", "letterer", "inker", "colorist"]: if str(authorDict["role"]).lower() is "cover artist": author = document.createElement("CoverArtist") elif str(authorDict["role"]).lower() is "assistant editor": author = document.createElement("Editor") else: author = document.createElement(str(authorDict["role"]).title()) stringName = [] if "last-name" in authorDict.keys(): stringName.append(authorDict["last-name"]) if "first-name" in authorDict.keys(): stringName.append(authorDict["first-name"]) if "nickname" in authorDict.keys(): stringName.append("(" + authorDict["nickname"] + ")") author.appendChild(document.createTextNode(str(",".join(stringName)))) root.appendChild(author) if "publisherName" in self.configDictionary.keys(): publisher = document.createElement("Publisher") publisher.appendChild(document.createTextNode(str(self.configDictionary["publisherName"]))) root.appendChild(publisher) if "genre" in self.configDictionary.keys(): for genreE in self.configDictionary["genre"]: genre = document.createElement("Genre") genre.appendChild(document.createTextNode(str(genreE))) root.appendChild(genre) blackAndWhite = document.createElement("BlackAndWhite") blackAndWhite.appendChild(document.createTextNode(str("No"))) root.appendChild(blackAndWhite) readingDirection = document.createElement("Manga") readingDirection.appendChild(document.createTextNode(str("No"))) if "readingDirection" in self.configDictionary.keys(): if self.configDictionary["readingDirection"] is "rightToLeft": readingDirection.appendChild(document.createTextNode(str("Yes"))) root.appendChild(readingDirection) if "characters" in self.configDictionary.keys(): for char in self.configDictionary["characters"]: character = document.createElement("Character") character.appendChild(document.createTextNode(str(char))) root.appendChild(character) if "pages" in self.configDictionary.keys(): pagecount = document.createElement("PageCount") pagecount.appendChild(document.createTextNode(str(len(self.configDictionary["pages"])))) root.appendChild(pagecount) pages = document.createElement("Pages") covernumber = 0 if "pages" in self.configDictionary.keys() and "cover" in self.configDictionary.keys(): covernumber = self.configDictionary["pages"].index(self.configDictionary["cover"]) for i in range(len(self.pagesLocationList["CBZ"])): page = document.createElement("Page") page.setAttribute("Image", str(i)) if i is covernumber: page.setAttribute("Type", "FrontCover") pages.appendChild(page) root.appendChild(pages) document.appendChild(root) f = open(location, 'w', newline="", encoding="utf-8") f.write(document.toprettyxml(indent=" ")) f.close() self.comicRackInfo = location return True """ Another metadata format but then a json dump stored into the zipfile comment. Doesn't seem to be supported much. :/ https://code.google.com/archive/p/comicbookinfo/wikis/Example.wiki """ def write_comic_book_info_json(self): self.comic_book_info_json_dump = str() basedata = {} metadata = {} authorList = [] taglist = [] if "authorList" in self.configDictionary.keys(): for authorE in range(len(self.configDictionary["authorList"])): author = {} authorDict = self.configDictionary["authorList"][authorE] stringName = [] if "last-name" in authorDict.keys(): stringName.append(authorDict["last-name"]) if "first-name" in authorDict.keys(): stringName.append(authorDict["first-name"]) if "nickname" in authorDict.keys(): stringName.append("(" + authorDict["nickname"] + ")") author["person"] = ",".join(stringName) if "role" in authorDict.keys(): author["role"] = str(authorDict["role"]).title() authorList.append(author) if "characters" in self.configDictionary.keys(): for character in self.configDictionary["characters"]: taglist.append(character) if "format" in self.configDictionary.keys(): for item in self.configDictionary["format"]: taglist.append(item) if "otherKeywords" in self.configDictionary.keys(): for item in self.configDictionary["otherKeywords"]: taglist.append(item) if "seriesName" in self.configDictionary.keys(): metadata["series"] = self.configDictionary["seriesName"] if "title" in self.configDictionary.keys(): metadata["title"] = self.configDictionary["title"] else: metadata["title"] = "Unnamed comic" if "publisherName" in self.configDictionary.keys(): metadata["publisher"] = self.configDictionary["publisherName"] if "publishingDate" in self.configDictionary.keys(): date = QDate.fromString(self.configDictionary["publishingDate"], Qt.ISODate) metadata["publicationMonth"] = date.month() metadata["publicationYear"] = date.year() if "seriesNumber" in self.configDictionary.keys(): metadata["issue"] = self.configDictionary["seriesNumber"] if "seriesVolume" in self.configDictionary.keys(): metadata["volume"] = self.configDictionary["seriesVolume"] if "genre" in self.configDictionary.keys(): metadata["genre"] = self.configDictionary["genre"] if "language" in self.configDictionary.keys(): metadata["language"] = QLocale.languageToString(QLocale(self.configDictionary["language"]).language()) metadata["credits"] = authorList metadata["tags"] = taglist if "summary" in self.configDictionary.keys(): metadata["comments"] = self.configDictionary["summary"] else: metadata["comments"] = "File generated without summary" basedata["appID"] = "Krita" basedata["lastModified"] = QDateTime.currentDateTimeUtc().toString(Qt.ISODate) basedata["ComicBookInfo/1.0"] = metadata self.comic_book_info_json_dump = json.dumps(basedata) return True """ package cbz puts all the meta-data and relevant files into an zip file ending with ".cbz" """ def package_cbz(self): # Use the project name if there's no title to avoid sillyness with unnamed zipfiles. title = self.configDictionary["projectName"] if "title" in self.configDictionary.keys(): title = self.configDictionary["title"] # Get the appropriate path. url = os.path.join(self.projectURL, self.configDictionary["exportLocation"], title + ".cbz") # Create a zip file. cbzArchive = zipfile.ZipFile(url, mode="w", compression=zipfile.ZIP_STORED) # Add all the meta data files. cbzArchive.write(self.acbfLocation, os.path.basename(self.acbfLocation)) cbzArchive.write(self.cometLocation, os.path.basename(self.cometLocation)) cbzArchive.write(self.comicRackInfo, os.path.basename(self.comicRackInfo)) cbzArchive.comment = self.comic_book_info_json_dump.encode("utf-8") # Add the pages. if "CBZ" in self.pagesLocationList.keys(): for page in self.pagesLocationList["CBZ"]: if (os.path.exists(page)): cbzArchive.write(page, os.path.basename(page)) # Close the zip file when done. cbzArchive.close() """ package epub packages the whole epub folder and renames the zip file to .epub. """ def package_epub(self): # Use the project name if there's no title to avoid sillyness with unnamed zipfiles. title = self.configDictionary["projectName"] if "title" in self.configDictionary.keys(): title = self.configDictionary["title"] # Get the appropriate paths. url = os.path.join(self.projectURL, self.configDictionary["exportLocation"], title) epub = os.path.join(self.projectURL, self.configDictionary["exportLocation"], "EPUB-files") # Make the archive. shutil.make_archive(base_name=url, format="zip", root_dir=epub) # Rename the archive to epub. shutil.move(src=str(url + ".zip"), dst=str(url + ".epub")) diff --git a/plugins/python/comics_project_management_tools/comics_metadata_dialog.py b/plugins/python/comics_project_management_tools/comics_metadata_dialog.py index 420ab8b254..b1c1973bc9 100644 --- a/plugins/python/comics_project_management_tools/comics_metadata_dialog.py +++ b/plugins/python/comics_project_management_tools/comics_metadata_dialog.py @@ -1,648 +1,665 @@ """ -Part of the comics project management tools (CPMT). +Copyright (c) 2017 Wolthera van Hövell tot Westerflier +This file is part of the Comics Project Management Tools(CPMT). + +CPMT 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 3 of the License, or +(at your option) any later version. + +CPMT 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 the CPMT. If not, see . +""" + +""" This is a metadata editor that helps out setting the proper metadata """ import sys import os # For finding the script location. import csv from pathlib import Path # For reading all the files in a directory. from PyQt5.QtGui import QStandardItem, QStandardItemModel from PyQt5.QtWidgets import QComboBox, QCompleter, QStyledItemDelegate, QLineEdit, QDialog, QDialogButtonBox, QVBoxLayout, QFormLayout, QTabWidget, QWidget, QPlainTextEdit, QHBoxLayout, QSpinBox, QDateEdit, QPushButton, QLabel, QTableView from PyQt5.QtCore import QDir, QLocale, QStringListModel, Qt, QDate """ multi entry completer cobbled together from the two examples on stackoverflow:3779720 This allows us to let people type in comma-separated lists and get completion for those. """ class multi_entry_completer(QCompleter): punctuation = "," def __init__(self, parent=None): super(QCompleter, self).__init__(parent) def pathFromIndex(self, index): path = QCompleter.pathFromIndex(self, index) string = str(self.widget().text()) split = string.split(self.punctuation) if len(split) > 1: path = "%s, %s" % (",".join(split[:-1]), path) return path def splitPath(self, path): split = str(path.split(self.punctuation)[-1]) if split.startswith(" "): split = split[1:] if split.endswith(" "): split = split[:-1] return [split] """ Language combobox that can take locale codes and get the right language for it and visa-versa. """ class language_combo_box(QComboBox): languageList = [] codesList = [] def __init__(self, parent=None): super(QComboBox, self).__init__(parent) mainP = os.path.dirname(__file__) languageP = os.path.join(mainP, "isoLanguagesList.csv") if (os.path.exists(languageP)): file = open(languageP, "r", newline="", encoding="utf8") languageReader = csv.reader(file) for row in languageReader: self.languageList.append(row[0]) self.codesList.append(row[1]) self.addItem(row[0]) file.close() def codeForCurrentEntry(self): if self.currentText() in self.languageList: return self.codesList[self.languageList.index(self.currentText())] def setEntryToCode(self, code): if (code == "C" and "en" in self.codesList): self.setCurrentIndex(self.codesList.index("en")) if code in self.codesList: self.setCurrentIndex(self.codesList.index(code)) """ A combobox that fills up with licenses from a CSV, and also sets tooltips from that csv. """ class license_combo_box(QComboBox): def __init__(self, parent=None): super(QComboBox, self).__init__(parent) mainP = os.path.dirname(__file__) languageP = os.path.join(mainP, "LicenseList.csv") model = QStandardItemModel() if (os.path.exists(languageP)): file = open(languageP, "r", newline="", encoding="utf8") languageReader = csv.reader(file) for row in languageReader: license = QStandardItem(row[0]) license.setToolTip(row[1]) model.appendRow(license) file.close() self.setModel(model) """ Allows us to set completers on the author roles. """ class author_delegate(QStyledItemDelegate): completerStrings = [] completerColumn = 0 languageColumn = 0 def __init__(self, parent=None): super(QStyledItemDelegate, self).__init__(parent) def setCompleterData(self, completerStrings=[str()], completerColumn=0): self.completerStrings = completerStrings self.completerColumn = completerColumn def setLanguageData(self, languageColumn=0): self.languageColumn = languageColumn def createEditor(self, parent, option, index): if index.column() != self.languageColumn: editor = QLineEdit(parent) else: editor = QComboBox(parent) editor.addItem("") for i in range(2, 356): if QLocale(i, QLocale.AnyScript, QLocale.AnyCountry) is not None: languagecode = QLocale(i, QLocale.AnyScript, QLocale.AnyCountry).name().split("_")[0] if languagecode != "C": editor.addItem(languagecode) editor.model().sort(0) if index.column() == self.completerColumn: editor.setCompleter(QCompleter(self.completerStrings)) editor.completer().setCaseSensitivity(False) return editor """ A comic project metadata editing dialog that can take our config diactionary and set all the relevant information. To help our user, the dialog loads up lists of keywords to populate several autocompletion methods. """ class comic_meta_data_editor(QDialog): configGroup = "ComicsProjectManagementTools" # Translatable genre dictionary that has it's translated entries added to the genrelist and from which the untranslated items are taken. acbfGenreList = {"science_fiction": str(i18n("Science Fiction")), "fantasy": str(i18n("Fantasy")), "adventure": str(i18n("Adventure")), "horror": str(i18n("Horror")), "mystery": str(i18n("Mystery")), "crime": str(i18n("Crime")), "military": str(i18n("Military")), "real_life": str(i18n("Real Life")), "superhero": str(i18n("Superhero")), "humor": str(i18n("Humor")), "western": str(i18n("Western")), "manga": str(i18n("Manga")), "politics": str(i18n("Politics")), "caricature": str(i18n("Caricature")), "sports": str(i18n("Sports")), "history": str(i18n("History")), "biography": str(i18n("Biography")), "education": str(i18n("Education")), "computer": str(i18n("Computer")), "religion": str(i18n("Religion")), "romance": str(i18n("Romance")), "children": str(i18n("Children")), "non-fiction": str(i18n("Non Fiction")), "adult": str(i18n("Adult")), "alternative": str(i18n("Alternative")), "other": str(i18n("Other"))} acbfAuthorRolesList = {"Writer": str(i18n("Writer")), "Adapter": str(i18n("Adapter")), "Artist": str(i18n("Artist")), "Penciller": str(i18n("Penciller")), "Inker": str(i18n("Inker")), "Colorist": str(i18n("Colorist")), "Letterer": str(i18n("Letterer")), "Cover Artist": str(i18n("Cover Artist")), "Photographer": str(i18n("Photographer")), "Editor": str(i18n("Editor")), "Assistant Editor": str(i18n("Assistant Editor")), "Translator": str(i18n("Translator")), "Other": str(i18n("Other"))} def __init__(self): super().__init__() # Get the keys for the autocompletion. self.genreKeysList = [] self.characterKeysList = [] self.ratingKeysList = {} self.formatKeysList = [] self.otherKeysList = [] self.authorRoleList = [] for g in self.acbfGenreList.values(): self.genreKeysList.append(g) for r in self.acbfAuthorRolesList.values(): self.authorRoleList.append(r) mainP = Path(os.path.abspath(__file__)).parent self.get_auto_completion_keys(mainP) extraKeyP = Path(QDir.homePath()) / Application.readSetting(self.configGroup, "extraKeysLocation", str()) self.get_auto_completion_keys(extraKeyP) # Setup the dialog. self.setLayout(QVBoxLayout()) mainWidget = QTabWidget() self.layout().addWidget(mainWidget) self.setWindowTitle(i18n("Comic Metadata")) buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) self.layout().addWidget(buttons) buttons.accepted.connect(self.accept) buttons.rejected.connect(self.reject) # Title, concept, summary, genre, characters, format, rating, language, series, other keywords metadataPage = QWidget() mformLayout = QFormLayout() metadataPage.setLayout(mformLayout) self.lnTitle = QLineEdit() self.lnTitle.setToolTip(i18n("The proper title of the comic.")) self.teSummary = QPlainTextEdit() self.teSummary.setToolTip(i18n("What will you tell others to entice them to read your comic?")) self.lnGenre = QLineEdit() genreCompletion = multi_entry_completer() genreCompletion.setModel(QStringListModel(self.genreKeysList)) self.lnGenre.setCompleter(genreCompletion) genreCompletion.setCaseSensitivity(False) self.lnGenre.setToolTip(i18n("The genre of the work. Prefilled values are from the ACBF, but you can fill in your own. Separate genres with commas. Try to limit the amount to about two or three")) self.lnCharacters = QLineEdit() characterCompletion = multi_entry_completer() characterCompletion.setModel(QStringListModel(self.characterKeysList)) characterCompletion.setCaseSensitivity(False) characterCompletion.setFilterMode(Qt.MatchContains) # So that if there is a list of names with last names, people can type in a last name. self.lnCharacters.setCompleter(characterCompletion) self.lnCharacters.setToolTip(i18n("The names of the characters that this comic revolves around. Comma-separated.")) self.lnFormat = QLineEdit() formatCompletion = multi_entry_completer() formatCompletion.setModel(QStringListModel(self.formatKeysList)) formatCompletion.setCaseSensitivity(False) self.lnFormat.setCompleter(formatCompletion) ratingLayout = QHBoxLayout() self.cmbRatingSystem = QComboBox() self.cmbRatingSystem.addItems(self.ratingKeysList.keys()) self.cmbRatingSystem.setEditable(True) self.cmbRating = QComboBox() self.cmbRating.setEditable(True) self.cmbRatingSystem.currentIndexChanged.connect(self.slot_refill_ratings) ratingLayout.addWidget(self.cmbRatingSystem) ratingLayout.addWidget(self.cmbRating) self.lnSeriesName = QLineEdit() self.lnSeriesName.setToolTip(i18n("If this is part of a series, enter the name of the series and the number.")) self.spnSeriesNumber = QSpinBox() self.spnSeriesNumber.setPrefix("No. ") self.spnSeriesVol = QSpinBox() self.spnSeriesVol.setPrefix("Vol. ") seriesLayout = QHBoxLayout() seriesLayout.addWidget(self.lnSeriesName) seriesLayout.addWidget(self.spnSeriesVol) seriesLayout.addWidget(self.spnSeriesNumber) otherCompletion = multi_entry_completer() otherCompletion.setModel(QStringListModel(self.otherKeysList)) otherCompletion.setCaseSensitivity(False) otherCompletion.setFilterMode(Qt.MatchContains) self.lnOtherKeywords = QLineEdit() self.lnOtherKeywords.setCompleter(otherCompletion) self.lnOtherKeywords.setToolTip(i18n("Other keywords that don't fit in the previously mentioned sets. As always, comma-separated")) self.cmbLanguage = language_combo_box() self.cmbReadingMode = QComboBox() self.cmbReadingMode.addItem(i18n("Left to Right")) self.cmbReadingMode.addItem(i18n("Right to Left")) self.cmbCoverPage = QComboBox() self.cmbCoverPage.setToolTip(i18n("Which page is the cover page? This will be empty if there's no pages.")) mformLayout.addRow(i18n("Title:"), self.lnTitle) mformLayout.addRow(i18n("Cover Page:"), self.cmbCoverPage) mformLayout.addRow(i18n("Summary:"), self.teSummary) mformLayout.addRow(i18n("Language:"), self.cmbLanguage) mformLayout.addRow(i18n("Reading Direction:"), self.cmbReadingMode) mformLayout.addRow(i18n("Genre:"), self.lnGenre) mformLayout.addRow(i18n("Characters:"), self.lnCharacters) mformLayout.addRow(i18n("Format:"), self.lnFormat) mformLayout.addRow(i18n("Rating:"), ratingLayout) mformLayout.addRow(i18n("Series:"), seriesLayout) mformLayout.addRow(i18n("Other:"), self.lnOtherKeywords) mainWidget.addTab(metadataPage, i18n("Work")) # The page for the authors. authorPage = QWidget() authorPage.setLayout(QVBoxLayout()) explanation = QLabel(i18n("The following is a table of the authors that contributed to this comic. You can set their nickname, proper names (first, middle, last), Role (Penciller, Inker, etc), email and homepage.")) explanation.setWordWrap(True) self.authorModel = QStandardItemModel(0, 7) labels = [i18n("Nick Name"), i18n("Given Name"), i18n("Middle Name"), i18n("Family Name"), i18n("Role"), i18n("Email"), i18n("Homepage"), i18n("Language")] self.authorModel.setHorizontalHeaderLabels(labels) self.authorTable = QTableView() self.authorTable.setModel(self.authorModel) self.authorTable.verticalHeader().setDragEnabled(True) self.authorTable.verticalHeader().setDropIndicatorShown(True) self.authorTable.verticalHeader().setSectionsMovable(True) self.authorTable.verticalHeader().sectionMoved.connect(self.slot_reset_author_row_visual) delegate = author_delegate() delegate.setCompleterData(self.authorRoleList, 4) delegate.setLanguageData(len(labels) - 1) self.authorTable.setItemDelegate(delegate) author_button_layout = QWidget() author_button_layout.setLayout(QHBoxLayout()) btn_add_author = QPushButton(i18n("Add Author")) btn_add_author.clicked.connect(self.slot_add_author) btn_remove_author = QPushButton(i18n("Remove Author")) btn_remove_author.clicked.connect(self.slot_remove_author) author_button_layout.layout().addWidget(btn_add_author) author_button_layout.layout().addWidget(btn_remove_author) authorPage.layout().addWidget(explanation) authorPage.layout().addWidget(self.authorTable) authorPage.layout().addWidget(author_button_layout) mainWidget.addTab(authorPage, i18n("Authors")) # The page with publisher information. publisherPage = QWidget() publisherLayout = QFormLayout() publisherPage.setLayout(publisherLayout) self.publisherName = QLineEdit() self.publisherName.setToolTip(i18n("The name of the company, group or person who is responsible for the final version the reader gets.")) publishDateLayout = QHBoxLayout() self.publishDate = QDateEdit() self.publishDate.setDisplayFormat(QLocale().system().dateFormat()) currentDate = QPushButton(i18n("Set Today")) currentDate.setToolTip(i18n("Sets the publish date to the current date.")) currentDate.clicked.connect(self.slot_set_date) publishDateLayout.addWidget(self.publishDate) publishDateLayout.addWidget(currentDate) self.publishCity = QLineEdit() self.publishCity.setToolTip(i18n("Traditional publishers are always mentioned in source with the city they are located.")) self.isbn = QLineEdit() self.license = license_combo_box() # Maybe ought to make this a QLineEdit... self.license.setEditable(True) self.license.completer().setCompletionMode(QCompleter.PopupCompletion) publisherLayout.addRow(i18n("Name:"), self.publisherName) publisherLayout.addRow(i18n("City:"), self.publishCity) publisherLayout.addRow(i18n("Date:"), publishDateLayout) publisherLayout.addRow(i18n("ISBN:"), self.isbn) publisherLayout.addRow(i18n("License:"), self.license) mainWidget.addTab(publisherPage, i18n("Publisher")) """ Ensure that the drag and drop of authors doesn't mess up the labels. """ def slot_reset_author_row_visual(self): headerLabelList = [] for i in range(self.authorTable.verticalHeader().count()): headerLabelList.append(str(i)) for i in range(self.authorTable.verticalHeader().count()): logicalI = self.authorTable.verticalHeader().logicalIndex(i) headerLabelList[logicalI] = str(i + 1) self.authorModel.setVerticalHeaderLabels(headerLabelList) """ Set the publish date to the current date. """ def slot_set_date(self): self.publishDate.setDate(QDate().currentDate()) """ Append keys to autocompletion lists from the directory mainP. """ def get_auto_completion_keys(self, mainP=Path()): genre = Path(mainP / "key_genre") characters = Path(mainP / "key_characters") rating = Path(mainP / "key_rating") format = Path(mainP / "key_format") keywords = Path(mainP / "key_other") authorRole = Path(mainP / "key_author_roles") if genre.exists(): for t in list(genre.glob('**/*.txt')): file = open(str(t), "r", errors="replace") for l in file: if str(l).strip("\n") not in self.genreKeysList: self.genreKeysList.append(str(l).strip("\n")) file.close() if characters.exists(): for t in list(characters.glob('**/*.txt')): file = open(str(t), "r", errors="replace") for l in file: if str(l).strip("\n") not in self.characterKeysList: self.characterKeysList.append(str(l).strip("\n")) file.close() if format.exists(): for t in list(format.glob('**/*.txt')): file = open(str(t), "r", errors="replace") for l in file: if str(l).strip("\n") not in self.formatKeysList: self.formatKeysList.append(str(l).strip("\n")) file.close() if rating.exists(): for t in list(rating.glob('**/*.csv')): file = open(str(t), "r", newline="", encoding="utf-8") ratings = csv.reader(file) title = os.path.basename(str(t)) r = 0 for row in ratings: listItem = [] if r is 0: title = row[1] else: listItem = self.ratingKeysList[title] item = [] item.append(row[0]) item.append(row[1]) listItem.append(item) self.ratingKeysList[title] = listItem r += 1 file.close() if keywords.exists(): for t in list(keywords.glob('**/*.txt')): file = open(str(t), "r", errors="replace") for l in file: if str(l).strip("\n") not in self.otherKeysList: self.otherKeysList.append(str(l).strip("\n")) file.close() if authorRole.exists(): for t in list(authorRole.glob('**/*.txt')): file = open(str(t), "r", errors="replace") for l in file: if str(l).strip("\n") not in self.authorRoleList: self.authorRoleList.append(str(l).strip("\n")) file.close() """ Refill the ratings box. This is called whenever the rating system changes. """ def slot_refill_ratings(self): if self.cmbRatingSystem.currentText() in self.ratingKeysList.keys(): self.cmbRating.clear() model = QStandardItemModel() for i in self.ratingKeysList[self.cmbRatingSystem.currentText()]: item = QStandardItem() item.setText(i[0]) item.setToolTip(i[1]) model.appendRow(item) self.cmbRating.setModel(model) """ Add an author with default values initialised. """ def slot_add_author(self): listItems = [] listItems.append(QStandardItem(i18n("Anon"))) # Nick name listItems.append(QStandardItem(i18n("John"))) # First name listItems.append(QStandardItem()) # Middle name listItems.append(QStandardItem(i18n("Doe"))) # Last name listItems.append(QStandardItem()) # role listItems.append(QStandardItem()) # email listItems.append(QStandardItem()) # homepage language = QLocale.system().name().split("_")[0] if language == "C": language = "en" listItems.append(QStandardItem(language)) # Language self.authorModel.appendRow(listItems) """ Remove the selected author from the author list. """ def slot_remove_author(self): self.authorModel.removeRow(self.authorTable.currentIndex().row()) """ Load the UI values from the config dictionary given. """ def setConfig(self, config): if "title" in config.keys(): self.lnTitle.setText(config["title"]) self.teSummary.clear() if "pages" in config.keys(): self.cmbCoverPage.clear() for page in config["pages"]: self.cmbCoverPage.addItem(page) if "cover" in config.keys(): if config["cover"] in config["pages"]: self.cmbCoverPage.setCurrentText(config["cover"]) if "summary" in config.keys(): self.teSummary.appendPlainText(config["summary"]) if "genre" in config.keys(): genreList = [] for genre in config["genre"]: if genre in self.acbfGenreList.keys(): genreList.append(self.acbfGenreList[genre]) else: genreList.append(genre) self.lnGenre.setText(", ".join(genreList)) if "characters" in config.keys(): self.lnCharacters.setText(", ".join(config["characters"])) if "format" in config.keys(): self.lnFormat.setText(", ".join(config["format"])) if "rating" in config.keys(): self.cmbRating.setCurrentText(config["rating"]) else: self.cmbRating.setCurrentText("") if "ratingSystem" in config.keys(): self.cmbRatingSystem.setCurrentText(config["ratingSystem"]) else: self.cmbRatingSystem.setCurrentText("") if "otherKeywords" in config.keys(): self.lnOtherKeywords.setText(", ".join(config["otherKeywords"])) if "seriesName" in config.keys(): self.lnSeriesName.setText(config["seriesName"]) if "seriesVolume" in config.keys(): self.spnSeriesVol.setValue(config["seriesVolume"]) if "seriesNumber" in config.keys(): self.spnSeriesNumber.setValue(config["seriesNumber"]) if "language" in config.keys(): code = config["language"] if "_" in code: code = code.split("_")[0] self.cmbLanguage.setEntryToCode(code) if "readingDirection" in config.keys(): if config["readingDirection"] is "leftToRight": self.cmbReadingMode.setCurrentIndex(0) else: self.cmbReadingMode.setCurrentIndex(1) else: self.cmbReadingMode.setCurrentIndex(QLocale(self.cmbLanguage.codeForCurrentEntry()).textDirection()) if "publisherName" in config.keys(): self.publisherName.setText(config["publisherName"]) if "publisherCity" in config.keys(): self.publishCity.setText(config["publisherCity"]) if "publishingDate" in config.keys(): self.publishDate.setDate(QDate.fromString(config["publishingDate"], Qt.ISODate)) if "isbn-number" in config.keys(): self.isbn.setText(config["isbn-number"]) if "license" in config.keys(): self.license.setCurrentText(config["license"]) else: self.license.setCurrentText("") # I would like to keep it ambiguous whether the artist has thought about the license or not. if "authorList" in config.keys(): authorList = config["authorList"] for i in range(len(authorList)): author = authorList[i] if len(author.keys()) > 0: listItems = [] authorNickName = QStandardItem() if "nickname" in author.keys(): authorNickName.setText(author["nickname"]) listItems.append(authorNickName) authorFirstName = QStandardItem() if "first-name" in author.keys(): authorFirstName.setText(author["first-name"]) listItems.append(authorFirstName) authorMiddleName = QStandardItem() if "initials" in author.keys(): authorMiddleName.setText(author["initials"]) listItems.append(authorMiddleName) authorLastName = QStandardItem() if "last-name" in author.keys(): authorLastName.setText(author["last-name"]) listItems.append(authorLastName) authorRole = QStandardItem() if "role" in author.keys(): role = author["role"] if author["role"] in self.acbfAuthorRolesList.keys(): role = self.acbfAuthorRolesList[author["role"]] authorRole.setText(role) listItems.append(authorRole) authorEMail = QStandardItem() if "email" in author.keys(): authorEMail.setText(author["email"]) listItems.append(authorEMail) authorHomePage = QStandardItem() if "homepage" in author.keys(): authorHomePage.setText(author["homepage"]) listItems.append(authorHomePage) authorLanguage = QStandardItem() if "language" in author.keys(): authorLanguage.setText(author["language"]) pass listItems.append(authorLanguage) self.authorModel.appendRow(listItems) else: self.slot_add_author() """ Store the GUI values into the config dictionary given. @return the config diactionary filled with new values. """ def getConfig(self, config): text = self.lnTitle.text() if len(text) > 0 and text.isspace() is False: config["title"] = text elif "title" in config.keys(): config.pop("title") config["cover"] = self.cmbCoverPage.currentText() listkeys = self.lnGenre.text() if len(listkeys) > 0 and listkeys.isspace() is False: genreList = [] for genre in self.lnGenre.text().split(", "): if genre in self.acbfGenreList.values(): i = list(self.acbfGenreList.values()).index(genre) genreList.append(list(self.acbfGenreList.keys())[i]) else: genreList.append(genre) config["genre"] = genreList elif "genre" in config.keys(): config.pop("genre") listkeys = self.lnCharacters.text() if len(listkeys) > 0 and listkeys.isspace() is False: config["characters"] = self.lnCharacters.text().split(", ") elif "characters" in config.keys(): config.pop("characters") listkeys = self.lnFormat.text() if len(listkeys) > 0 and listkeys.isspace() is False: config["format"] = self.lnFormat.text().split(", ") elif "format" in config.keys(): config.pop("format") config["ratingSystem"] = self.cmbRatingSystem.currentText() config["rating"] = self.cmbRating.currentText() listkeys = self.lnOtherKeywords.text() if len(listkeys) > 0 and listkeys.isspace() is False: config["otherKeywords"] = self.lnOtherKeywords.text().split(", ") elif "characters" in config.keys(): config.pop("otherKeywords") text = self.teSummary.toPlainText() if len(text) > 0 and text.isspace() is False: config["summary"] = text elif "summary" in config.keys(): config.pop("summary") if len(self.lnSeriesName.text()) > 0: config["seriesName"] = self.lnSeriesName.text() config["seriesNumber"] = self.spnSeriesNumber.value() if self.spnSeriesVol.value() > 0: config["seriesVolume"] = self.spnSeriesVol.value() config["language"] = str(self.cmbLanguage.codeForCurrentEntry()) if self.cmbReadingMode is Qt.LeftToRight: config["readingDirection"] = "leftToRight" else: config["readingDirection"] = "rightToLeft" authorList = [] for row in range(self.authorTable.verticalHeader().count()): logicalIndex = self.authorTable.verticalHeader().logicalIndex(row) listEntries = ["nickname", "first-name", "initials", "last-name", "role", "email", "homepage", "language"] author = {} for i in range(len(listEntries)): entry = self.authorModel.data(self.authorModel.index(logicalIndex, i)) if entry is None: entry = " " if entry.isspace() is False and len(entry) > 0: if listEntries[i] == "role": if entry in self.acbfAuthorRolesList.values(): entryI = list(self.acbfAuthorRolesList.values()).index(entry) entry = list(self.acbfAuthorRolesList.keys())[entryI] author[listEntries[i]] = entry elif listEntries[i] in author.keys(): author.pop(listEntries[i]) authorList.append(author) config["authorList"] = authorList config["publisherName"] = self.publisherName.text() config["publisherCity"] = self.publishCity.text() config["publishingDate"] = self.publishDate.date().toString(Qt.ISODate) config["isbn-number"] = self.isbn.text() config["license"] = self.license.currentText() return config diff --git a/plugins/python/comics_project_management_tools/comics_project_manager_docker.py b/plugins/python/comics_project_management_tools/comics_project_manager_docker.py index 3058d67968..6224f81bd6 100644 --- a/plugins/python/comics_project_management_tools/comics_project_manager_docker.py +++ b/plugins/python/comics_project_management_tools/comics_project_manager_docker.py @@ -1,763 +1,780 @@ """ -Part of the comics project management tools (CPMT). +Copyright (c) 2017 Wolthera van Hövell tot Westerflier +This file is part of the Comics Project Management Tools(CPMT). + +CPMT 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 3 of the License, or +(at your option) any later version. + +CPMT 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 the CPMT. If not, see . +""" + +""" This is a docker that helps you organise your comics project. """ import sys import json import os import zipfile # quick reading of documents import shutil import xml.etree.ElementTree as ET from PyQt5.QtCore import QElapsedTimer, QSize, Qt from PyQt5.QtGui import QStandardItem, QStandardItemModel, QImage, QIcon, QPixmap, QFontMetrics from PyQt5.QtWidgets import QHBoxLayout, QVBoxLayout, QTableView, QToolButton, QMenu, QAction, QPushButton, QSpacerItem, QSizePolicy, QWidget, QAbstractItemView, QProgressDialog, QDialog, QFileDialog, QDialogButtonBox, qApp, QSplitter, QSlider, QLabel import math from krita import * from . import comics_metadata_dialog, comics_exporter, comics_export_dialog, comics_project_setup_wizard, comics_template_dialog, comics_project_settings_dialog, comics_project_page_viewer """ A very simple class so we can have a label that is single line, but doesn't force the widget size to be bigger. This is used by the project name. """ class Elided_Text_Label(QLabel): mainText = str() def __init__(self, parent=None): super(QLabel, self).__init__(parent) self.setMinimumWidth(self.fontMetrics().width("...")) self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum) def setMainText(self, text=str()): self.mainText = text self.elideText() def elideText(self): self.setText(self.fontMetrics().elidedText(self.mainText, Qt.ElideRight, self.width())) def resizeEvent(self, event): self.elideText() """ This is a Krita docker called 'Comics Manager'. It allows people to create comics project files, load those files, add pages, remove pages, move pages, manage the metadata, and finally export the result. The logic behind this docker is that it is very easy to get lost in a comics project due to the massive amount of files. By having a docker that gives the user quick access to the pages and also allows them to do all of the meta-stuff, like meta data, but also reordering the pages, the chaos of managing the project should take up less time, and more time can be focussed on actual writing and drawing. """ class comics_project_manager_docker(DockWidget): setupDictionary = {} stringName = i18n("Comics Manager") projecturl = None def __init__(self): super().__init__() self.setWindowTitle(self.stringName) # Setup layout: base = QHBoxLayout() widget = QWidget() widget.setLayout(base) baseLayout = QSplitter() base.addWidget(baseLayout) self.setWidget(widget) buttonLayout = QVBoxLayout() buttonBox = QWidget() buttonBox.setLayout(buttonLayout) baseLayout.addWidget(buttonBox) # Comic page list and pages model self.comicPageList = QTableView() self.comicPageList.verticalHeader().setSectionsMovable(True) self.comicPageList.verticalHeader().setDragEnabled(True) self.comicPageList.verticalHeader().setDragDropMode(QAbstractItemView.InternalMove) self.comicPageList.setAcceptDrops(True) self.comicPageList.horizontalHeader().setStretchLastSection(True) self.pagesModel = QStandardItemModel() self.comicPageList.doubleClicked.connect(self.slot_open_page) self.comicPageList.setIconSize(QSize(128, 128)) # self.comicPageList.itemDelegate().closeEditor.connect(self.slot_write_description) self.pagesModel.layoutChanged.connect(self.slot_write_config) self.pagesModel.rowsInserted.connect(self.slot_write_config) self.pagesModel.rowsRemoved.connect(self.slot_write_config) self.comicPageList.verticalHeader().sectionMoved.connect(self.slot_write_config) self.comicPageList.setModel(self.pagesModel) pageBox = QWidget() pageBox.setLayout(QVBoxLayout()) zoomSlider = QSlider(Qt.Horizontal, None) zoomSlider.setRange(1, 8) zoomSlider.setValue(4) zoomSlider.setTickInterval(1) zoomSlider.setMinimumWidth(10) zoomSlider.valueChanged.connect(self.slot_scale_thumbnails) self.projectName = Elided_Text_Label() pageBox.layout().addWidget(self.projectName) pageBox.layout().addWidget(zoomSlider) pageBox.layout().addWidget(self.comicPageList) baseLayout.addWidget(pageBox) self.btn_project = QToolButton() self.btn_project.setPopupMode(QToolButton.MenuButtonPopup) self.btn_project.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) menu_project = QMenu() self.action_new_project = QAction(i18n("New Project"), self) self.action_new_project.triggered.connect(self.slot_new_project) self.action_load_project = QAction(i18n("Open Project"), self) self.action_load_project.triggered.connect(self.slot_open_config) menu_project.addAction(self.action_new_project) menu_project.addAction(self.action_load_project) self.btn_project.setMenu(menu_project) self.btn_project.setDefaultAction(self.action_load_project) buttonLayout.addWidget(self.btn_project) # Settings dropdown with actions for the different settings menus. self.btn_settings = QToolButton() self.btn_settings.setPopupMode(QToolButton.MenuButtonPopup) self.btn_settings.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) self.action_edit_project_settings = QAction(i18n("Project Settings"), self) self.action_edit_project_settings.triggered.connect(self.slot_edit_project_settings) self.action_edit_meta_data = QAction(i18n("Meta Data"), self) self.action_edit_meta_data.triggered.connect(self.slot_edit_meta_data) self.action_edit_export_settings = QAction(i18n("Export Settings"), self) self.action_edit_export_settings.triggered.connect(self.slot_edit_export_settings) menu_settings = QMenu() menu_settings.addAction(self.action_edit_project_settings) menu_settings.addAction(self.action_edit_meta_data) menu_settings.addAction(self.action_edit_export_settings) self.btn_settings.setDefaultAction(self.action_edit_project_settings) self.btn_settings.setMenu(menu_settings) buttonLayout.addWidget(self.btn_settings) self.btn_settings.setDisabled(True) # Add page drop down with different page actions. self.btn_add_page = QToolButton() self.btn_add_page.setPopupMode(QToolButton.MenuButtonPopup) self.btn_add_page.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) self.action_add_page = QAction(i18n("Add Page"), self) self.action_add_page.triggered.connect(self.slot_add_new_page_single) self.action_add_template = QAction(i18n("Add Page From Template"), self) self.action_add_template.triggered.connect(self.slot_add_new_page_from_template) self.action_add_existing = QAction(i18n("Add Existing Pages"), self) self.action_add_existing.triggered.connect(self.slot_add_page_from_url) self.action_remove_selected_page = QAction(i18n("Remove Page"), self) self.action_remove_selected_page.triggered.connect(self.slot_remove_selected_page) self.action_resize_all_pages = QAction(i18n("Batch Resize"), self) self.action_resize_all_pages.triggered.connect(self.slot_batch_resize) self.btn_add_page.setDefaultAction(self.action_add_page) self.action_show_page_viewer = QAction(i18n("View Page In Window"), self) self.action_show_page_viewer.triggered.connect(self.slot_show_page_viewer) self.action_scrape_authors = QAction(i18n("Scrape Author Info"), self) self.action_scrape_authors.setToolTip(i18n("Search for author information in documents and add it to the author list. This doesn't check for duplicates.")) self.action_scrape_authors.triggered.connect(self.slot_scrape_author_list) actionList = [] menu_page = QMenu() actionList.append(self.action_add_page) actionList.append(self.action_add_template) actionList.append(self.action_add_existing) actionList.append(self.action_remove_selected_page) actionList.append(self.action_resize_all_pages) actionList.append(self.action_show_page_viewer) actionList.append(self.action_scrape_authors) menu_page.addActions(actionList) self.btn_add_page.setMenu(menu_page) buttonLayout.addWidget(self.btn_add_page) self.btn_add_page.setDisabled(True) self.comicPageList.setContextMenuPolicy(Qt.ActionsContextMenu) self.comicPageList.addActions(actionList) # Export button that... exports. self.btn_export = QPushButton(i18n("Export Comic")) self.btn_export.clicked.connect(self.slot_export) buttonLayout.addWidget(self.btn_export) self.btn_export.setDisabled(True) self.btn_project_url = QPushButton(i18n("Copy Location")) self.btn_project_url.setToolTip(i18n("Copies the path of the project to the clipboard. Useful for quickly copying to a file manager or the like.")) self.btn_project_url.clicked.connect(self.slot_copy_project_url) self.btn_project_url.setDisabled(True) buttonLayout.addWidget(self.btn_project_url) self.page_viewer_dialog = comics_project_page_viewer.comics_project_page_viewer() Application.notifier().imageSaved.connect(self.slot_check_for_page_update) buttonLayout.addItem(QSpacerItem(0, 0, QSizePolicy.Minimum, QSizePolicy.MinimumExpanding)) """ Open the config file and load the json file into a dictionary. """ def slot_open_config(self): self.path_to_config = QFileDialog.getOpenFileName(caption=i18n("Please select the json comic config file."), filter=str(i18n("json files") + "(*.json)"))[0] if os.path.exists(self.path_to_config) is True: configFile = open(self.path_to_config, "r", newline="", encoding="utf-16") self.setupDictionary = json.load(configFile) self.projecturl = os.path.dirname(str(self.path_to_config)) configFile.close() self.load_config() """ Further config loading. """ def load_config(self): self.projectName.setMainText(text=str(self.setupDictionary["projectName"])) self.fill_pages() self.btn_settings.setEnabled(True) self.btn_add_page.setEnabled(True) self.btn_export.setEnabled(True) self.btn_project_url.setEnabled(True) """ Fill the pages model with the pages from the pages list. """ def fill_pages(self): self.loadingPages = True self.pagesModel.clear() self.pagesModel.setHorizontalHeaderLabels([i18n("Page"), i18n("Description")]) pagesList = [] if "pages" in self.setupDictionary.keys(): pagesList = self.setupDictionary["pages"] progress = QProgressDialog() progress.setMinimum(0) progress.setMaximum(len(pagesList)) progress.setWindowTitle(i18n("Loading pages...")) for url in pagesList: absurl = os.path.join(self.projecturl, url) if (os.path.exists(absurl)): #page = Application.openDocument(absurl) page = zipfile.ZipFile(absurl, "r") thumbnail = QImage.fromData(page.read("preview.png")) pageItem = QStandardItem() dataList = self.get_description_and_title(page.read("documentinfo.xml")) if (dataList[0].isspace() or len(dataList[0]) < 1): dataList[0] = os.path.basename(url) pageItem.setText(dataList[0].replace("_", " ")) pageItem.setDragEnabled(True) pageItem.setDropEnabled(False) pageItem.setEditable(False) pageItem.setIcon(QIcon(QPixmap.fromImage(thumbnail))) pageItem.setToolTip(url) page.close() description = QStandardItem() description.setText(dataList[1]) description.setEditable(False) listItem = [] listItem.append(pageItem) listItem.append(description) self.pagesModel.appendRow(listItem) self.comicPageList.resizeRowsToContents() self.comicPageList.setColumnWidth(0, 200) self.comicPageList.setColumnWidth(1, 256) progress.setValue(progress.value() + 1) progress.setValue(len(pagesList)) self.loadingPages = False """ Function that is triggered by the zoomSlider Resizes the thumbnails. """ def slot_scale_thumbnails(self, multiplier=4): self.comicPageList.setIconSize(QSize(multiplier * 32, multiplier * 32)) self.comicPageList.resizeRowsToContents() """ Function that takes the documentinfo.xml and parses it for the title, subject and abstract tags, to get the title and description. @returns a stringlist with the name on 0 and the description on 1. """ def get_description_and_title(self, string): xmlDoc = ET.fromstring(string) calligra = str("{http://www.calligra.org/DTD/document-info}") name = "" if ET.iselement(xmlDoc[0].find(calligra + 'title')): name = xmlDoc[0].find(calligra + 'title').text if name is None: name = " " desc = "" if ET.iselement(xmlDoc[0].find(calligra + 'subject')): desc = xmlDoc[0].find(calligra + 'subject').text if desc is None or desc.isspace() or len(desc) < 1: if ET.iselement(xmlDoc[0].find(calligra + 'abstract')): desc = str(xmlDoc[0].find(calligra + 'abstract').text) if desc.startswith(""): desc = desc[:-len("]]>")] return [name, desc] """ Scrapes authors from the author data in the document info and puts them into the author list. Doesn't check for duplicates. """ def slot_scrape_author_list(self): listOfAuthors = [] if "authorList" in self.setupDictionary.keys(): listOfAuthors = self.setupDictionary["authorList"] if "pages" in self.setupDictionary.keys(): for relurl in self.setupDictionary["pages"]: absurl = os.path.join(self.projecturl, relurl) page = zipfile.ZipFile(absurl, "r") xmlDoc = ET.fromstring(page.read("documentinfo.xml")) calligra = str("{http://www.calligra.org/DTD/document-info}") authorelem = xmlDoc.find(calligra + 'author') author = {} if ET.iselement(authorelem.find(calligra + 'full-name')): author["nickname"] = str(authorelem.find(calligra + 'full-name').text) if ET.iselement(authorelem.find(calligra + 'creator-first-name')): author["first-name"] = str(authorelem.find(calligra + 'creator-first-name').text) if ET.iselement(authorelem.find(calligra + 'initial')): author["initials"] = str(authorelem.find(calligra + 'initial').text) if ET.iselement(authorelem.find(calligra + 'creator-last-name')): author["last-name"] = str(authorelem.find(calligra + 'creator-last-name').text) if ET.iselement(authorelem.find(calligra + 'email')): author["email"] = str(authorelem.find(calligra + 'email').text) if ET.iselement(authorelem.find(calligra + 'contact')): contact = authorelem.find(calligra + 'contact') contactMode = contact.get("type") if contactMode == "email": author["email"] = str(contact.text) if contactMode == "homepage": author["homepage"] = str(contact.text) if ET.iselement(authorelem.find(calligra + 'position')): author["role"] = str(authorelem.find(calligra + 'position').text) listOfAuthors.append(author) page.close() self.setupDictionary["authorList"] = listOfAuthors """ Edit the general project settings like the project name, concept, pages location, export location, template location, metadata """ def slot_edit_project_settings(self): dialog = comics_project_settings_dialog.comics_project_details_editor(self.projecturl) dialog.setConfig(self.setupDictionary, self.projecturl) if dialog.exec_() == QDialog.Accepted: self.setupDictionary = dialog.getConfig(self.setupDictionary) self.slot_write_config() self.setWindowTitle(self.stringName + ": " + str(self.setupDictionary["projectName"])) """ This allows users to select existing pages and add them to the pages list. The pages are currently not copied to the pages folder. Useful for existing projects. """ def slot_add_page_from_url(self): # get the pages. urlList = QFileDialog.getOpenFileNames(caption=i18n("Which existing pages to add?"), directory=self.projecturl, filter=str(i18n("Krita files") + "(*.kra)"))[0] # get the existing pages list. pagesList = [] if "pages" in self.setupDictionary.keys(): pagesList = self.setupDictionary["pages"] # And add each url in the url list to the pages list and the model. for url in urlList: if self.projecturl not in urlList: newUrl = os.path.join(self.projecturl, self.setupDictionary["pagesLocation"], os.path.basename(url)) shutil.move(url, newUrl) url = newUrl relative = os.path.relpath(url, self.projecturl) if url not in pagesList: page = zipfile.ZipFile(url, "r") thumbnail = QImage.fromData(page.read("preview.png")) dataList = self.get_description_and_title(page.read("documentinfo.xml")) if (dataList[0].isspace() or len(dataList[0]) < 1): dataList[0] = os.path.basename(url) newPageItem = QStandardItem() newPageItem.setIcon(QIcon(QPixmap.fromImage(thumbnail))) newPageItem.setDragEnabled(True) newPageItem.setDropEnabled(False) newPageItem.setEditable(False) newPageItem.setText(dataList[0].replace("_", " ")) newPageItem.setToolTip(relative) page.close() description = QStandardItem() description.setText(dataList[1]) description.setEditable(False) listItem = [] listItem.append(newPageItem) listItem.append(description) self.pagesModel.appendRow(listItem) self.comicPageList.resizeRowsToContents() """ Remove the selected page from the list of pages. This does not remove it from disk(far too dangerous). """ def slot_remove_selected_page(self): index = self.comicPageList.currentIndex() self.pagesModel.removeRow(index.row()) """ This function adds a new page from the default template. If there's no default template, or the file does not exist, it will show the create/import template dialog. It will remember the selected item as the default template. """ def slot_add_new_page_single(self): templateUrl = "templatepage" templateExists = False if "singlePageTemplate" in self.setupDictionary.keys(): templateUrl = self.setupDictionary["singlePageTemplate"] if os.path.exists(os.path.join(self.projecturl, templateUrl)): templateExists = True if templateExists is False: if "templateLocation" not in self.setupDictionary.keys(): self.setupDictionary["templateLocation"] = os.path.relpath(QFileDialog.getExistingDirectory(caption=i18n("Where are the templates located?"), options=QFileDialog.ShowDirsOnly), self.projecturl) templateDir = os.path.join(self.projecturl, self.setupDictionary["templateLocation"]) template = comics_template_dialog.comics_template_dialog(templateDir) if template.exec_() == QDialog.Accepted: templateUrl = os.path.relpath(template.url(), self.projecturl) self.setupDictionary["singlePageTemplate"] = templateUrl if os.path.exists(os.path.join(self.projecturl, templateUrl)): self.add_new_page(templateUrl) """ This function always asks for a template showing the new template window. This allows users to have multiple different templates created for back covers, spreads, other and have them accesible, while still having the convenience of a singular "add page" that adds a default. """ def slot_add_new_page_from_template(self): if "templateLocation" not in self.setupDictionary.keys(): self.setupDictionary["templateLocation"] = os.path.relpath(QFileDialog.getExistingDirectory(caption=i18n("Where are the templates located?"), options=QFileDialog.ShowDirsOnly), self.projecturl) templateDir = os.path.join(self.projecturl, self.setupDictionary["templateLocation"]) template = comics_template_dialog.comics_template_dialog(templateDir) if template.exec_() == QDialog.Accepted: templateUrl = os.path.relpath(template.url(), self.projecturl) self.add_new_page(templateUrl) """ This is the actual function that adds the template using the template url. It will attempt to name the new page projectName+number. """ def add_new_page(self, templateUrl): # check for page list and or location. pagesList = [] if "pages" in self.setupDictionary.keys(): pagesList = self.setupDictionary["pages"] if not "pageNumber" in self.setupDictionary.keys(): self.setupDictionary['pageNumber'] = 0 if (str(self.setupDictionary["pagesLocation"]).isspace()): self.setupDictionary["pagesLocation"] = os.path.relpath(QFileDialog.getExistingDirectory(caption=i18n("Where should the pages go?"), options=QFileDialog.ShowDirsOnly), self.projecturl) # Search for the possible name. extraUnderscore = str() if str(self.setupDictionary["projectName"])[-1].isdigit(): extraUnderscore = "_" self.setupDictionary['pageNumber'] += 1 pageName = str(self.setupDictionary["projectName"]).replace(" ", "_") + extraUnderscore + str(format(self.setupDictionary['pageNumber'], "03d")) url = os.path.join(str(self.setupDictionary["pagesLocation"]), pageName + ".kra") # open the page by opening the template and resaving it, or just opening it. absoluteUrl = os.path.join(self.projecturl, url) if (os.path.exists(absoluteUrl)): newPage = Application.openDocument(absoluteUrl) else: booltemplateExists = os.path.exists(os.path.join(self.projecturl, templateUrl)) if booltemplateExists is False: templateUrl = os.path.relpath(QFileDialog.getOpenFileName(caption=i18n("Which image should be the basis the new page?"), directory=self.projecturl, filter=str(i18n("Krita files") + "(*.kra)"))[0], self.projecturl) newPage = Application.openDocument(os.path.join(self.projecturl, templateUrl)) newPage.waitForDone() newPage.setFileName(absoluteUrl) newPage.setName(pageName.replace("_", " ")) newPage.save() newPage.waitForDone() # Get out the extra data for the standard item. newPageItem = QStandardItem() newPageItem.setIcon(QIcon(QPixmap.fromImage(newPage.thumbnail(256, 256)))) newPageItem.setDragEnabled(True) newPageItem.setDropEnabled(False) newPageItem.setEditable(False) newPageItem.setText(pageName.replace("_", " ")) newPageItem.setToolTip(url) # close page document. while os.path.exists(absoluteUrl) is False: qApp.processEvents() newPage.close() # add item to page. description = QStandardItem() description.setText(str("")) description.setEditable(False) listItem = [] listItem.append(newPageItem) listItem.append(description) self.pagesModel.appendRow(listItem) self.comicPageList.resizeRowsToContents() self.comicPageList.resizeColumnToContents(0) """ Write to the json configuratin file. This also checks the current state of the pages list. """ def slot_write_config(self): # Don't load when the pages are still being loaded, otherwise we'll be overwriting our own pages list. if (self.loadingPages is False): print("CPMT: writing comic configuration...") # Generate a pages list from the pagesmodel. # Because we made the drag-and-drop use the tableview header, we need to first request the logicalIndex # for the visualIndex, and then request the items for the logical index in the pagesmodel. # After that, we rename the verticalheader to have the appropriate numbers it will have when reloading. pagesList = [] listOfHeaderLabels = [] for i in range(self.pagesModel.rowCount()): listOfHeaderLabels.append(str(i)) for i in range(self.pagesModel.rowCount()): iLogical = self.comicPageList.verticalHeader().logicalIndex(i) index = self.pagesModel.index(iLogical, 0) if index.isValid() is False: index = self.pagesModel.index(i, 0) url = str(self.pagesModel.data(index, role=Qt.ToolTipRole)) if url not in pagesList: pagesList.append(url) listOfHeaderLabels[iLogical] = str(i + 1) self.pagesModel.setVerticalHeaderLabels(listOfHeaderLabels) self.comicPageList.verticalHeader().update() self.setupDictionary["pages"] = pagesList # Save to our json file. configFile = open(self.path_to_config, "w", newline="", encoding="utf-16") json.dump(self.setupDictionary, configFile, indent=4, sort_keys=True, ensure_ascii=False) configFile.close() print("CPMT: done") """ Open a page in the pagesmodel in Krita. """ def slot_open_page(self, index): if index.column() is 0: # Get the absolute url from the relative one in the pages model. absoluteUrl = os.path.join(self.projecturl, str(self.pagesModel.data(index, role=Qt.ToolTipRole))) # Make sure the page exists. if os.path.exists(absoluteUrl): page = Application.openDocument(absoluteUrl) # Set the title to the filename if it was empty. It looks a bit neater. if page.name().isspace or len(page.name()) < 1: page.setName(str(self.pagesModel.data(index, role=Qt.DisplayRole)).replace("_", " ")) # Add views for the document so the user can use it. Application.activeWindow().addView(page) Application.setActiveDocument(page) else: print("CPMT: The page cannot be opened because the file doesn't exist:", absoluteUrl) """ Call up the metadata editor dialog. Only when the dialog is "Accepted" will the metadata be saved. """ def slot_edit_meta_data(self): dialog = comics_metadata_dialog.comic_meta_data_editor() dialog.setConfig(self.setupDictionary) if (dialog.exec_() == QDialog.Accepted): self.setupDictionary = dialog.getConfig(self.setupDictionary) self.slot_write_config() """ An attempt at making the description editable from the comic pages list. It is currently not working because ZipFile has no overwrite mechanism, and I don't have the energy to write one yet. """ def slot_write_description(self, index): for row in range(self.pagesModel.rowCount()): index = self.pagesModel.index(row, 1) indexUrl = self.pagesModel.index(row, 0) absoluteUrl = os.path.join(self.projecturl, str(self.pagesModel.data(indexUrl, role=Qt.ToolTipRole))) page = zipfile.ZipFile(absoluteUrl, "a") xmlDoc = ET.ElementTree() ET.register_namespace("", "http://www.calligra.org/DTD/document-info") location = os.path.join(self.projecturl, "documentinfo.xml") xmlDoc.parse(location) xmlroot = ET.fromstring(page.read("documentinfo.xml")) calligra = "{http://www.calligra.org/DTD/document-info}" aboutelem = xmlroot.find(calligra + 'about') if ET.iselement(aboutelem.find(calligra + 'subject')): desc = aboutelem.find(calligra + 'subject') desc.text = self.pagesModel.data(index, role=Qt.EditRole) xmlstring = ET.tostring(xmlroot, encoding='unicode', method='xml', short_empty_elements=False) page.writestr(zinfo_or_arcname="documentinfo.xml", data=xmlstring) for document in Application.documents(): if str(document.fileName()) == str(absoluteUrl): document.setDocumentInfo(xmlstring) page.close() """ Calls up the export settings dialog. Only when accepted will the configuration be written. """ def slot_edit_export_settings(self): dialog = comics_export_dialog.comic_export_setting_dialog() dialog.setConfig(self.setupDictionary) if (dialog.exec_() == QDialog.Accepted): self.setupDictionary = dialog.getConfig(self.setupDictionary) self.slot_write_config() """ Export the comic. Won't work without export settings set. """ def slot_export(self): exporter = comics_exporter.comicsExporter() exporter.set_config(self.setupDictionary, self.projecturl) exportSuccess = exporter.export() if exportSuccess: print("CPMT: Export success! The files have been written to the export folder!") """ Calls up the comics project setup wizard so users can create a new json file with the basic information. """ def slot_new_project(self): setup = comics_project_setup_wizard.ComicsProjectSetupWizard() setup.showDialog() """ This is triggered by any document save. It checks if the given url in in the pages list, and if so, updates the appropriate page thumbnail. This helps with the management of the pages, because the user will be able to see the thumbnails as a todo for the whole comic, giving a good overview over whether they still need to ink, color or the like for a given page, and it thus also rewards the user whenever they save. """ def slot_check_for_page_update(self, url): if "pages" in self.setupDictionary.keys(): relUrl = os.path.relpath(url, self.projecturl) if relUrl in self.setupDictionary["pages"]: index = self.pagesModel.index(self.setupDictionary["pages"].index(relUrl), 0) index2 = self.pagesModel.index(index.row(), 1) if index.isValid(): pageItem = self.pagesModel.itemFromIndex(index) page = zipfile.ZipFile(url, "r") dataList = self.get_description_and_title(page.read("documentinfo.xml")) if (dataList[0].isspace() or len(dataList[0]) < 1): dataList[0] = os.path.basename(url) thumbnail = QImage.fromData(page.read("preview.png")) pageItem.setIcon(QIcon(QPixmap.fromImage(thumbnail))) pageItem.setText(dataList[0]) self.pagesModel.setItem(index.row(), index.column(), pageItem) self.pagesModel.setData(index2, str(dataList[1]), Qt.DisplayRole) """ Resize all the pages in the pages list. It will show a dialog with the options for resizing. Then, it will try to pop up a progress dialog while resizing. The progress dialog shows the remaining time and pages. """ def slot_batch_resize(self): dialog = QDialog() dialog.setWindowTitle(i18n("Risize all pages.")) buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) buttons.accepted.connect(dialog.accept) buttons.rejected.connect(dialog.reject) sizesBox = comics_export_dialog.comic_export_resize_widget("Scale", batch=True, fileType=False) exporterSizes = comics_exporter.sizesCalculator() dialog.setLayout(QVBoxLayout()) dialog.layout().addWidget(sizesBox) dialog.layout().addWidget(buttons) if dialog.exec_() == QDialog.Accepted: progress = QProgressDialog(i18n("Resizing pages..."), str(), 0, len(self.setupDictionary["pages"])) progress.setWindowTitle(i18n("Resizing pages.")) progress.setCancelButton(None) timer = QElapsedTimer() timer.start() config = {} config = sizesBox.get_config(config) for p in range(len(self.setupDictionary["pages"])): absoluteUrl = os.path.join(self.projecturl, self.setupDictionary["pages"][p]) progress.setValue(p) timePassed = timer.elapsed() if (p > 0): timeEstimated = (len(self.setupDictionary["pages"]) - p) * (timePassed / p) passedString = str(int(timePassed / 60000)) + ":" + format(int(timePassed / 1000), "02d") + ":" + format(timePassed % 1000, "03d") estimatedString = str(int(timeEstimated / 60000)) + ":" + format(int(timeEstimated / 1000), "02d") + ":" + format(int(timeEstimated % 1000), "03d") progress.setLabelText(str(i18n("{pages} of {pagesTotal} done. \nTime passed: {passedString}:\n Estimated:{estimated}")).format(pages=p, pagesTotal=len(self.setupDictionary["pages"]), passedString=passedString, estimated=estimatedString)) qApp.processEvents() if os.path.exists(absoluteUrl): doc = Application.openDocument(absoluteUrl) listScales = exporterSizes.get_scale_from_resize_config(config["Scale"], [doc.width(), doc.height(), doc.resolution(), doc.resolution()]) doc.scaleImage(listScales[0], listScales[1], listScales[2], listScales[3], "bicubic") doc.waitForDone() doc.save() doc.waitForDone() doc.close() def slot_show_page_viewer(self): index = self.comicPageList.currentIndex() if index.column() is not 0: index = self.pagesModel.index(index.row(), 0) # Get the absolute url from the relative one in the pages model. absoluteUrl = os.path.join(self.projecturl, str(self.pagesModel.data(index, role=Qt.ToolTipRole))) # Make sure the page exists. if os.path.exists(absoluteUrl): page = zipfile.ZipFile(absoluteUrl, "r") image = QImage.fromData(page.read("mergedimage.png")) self.page_viewer_dialog.update_image(image) self.page_viewer_dialog.show() page.close() """ Function to copy the current project location into the clipboard. This is useful for users because they'll be able to use that url to quickly move to the project location in outside applications. """ def slot_copy_project_url(self): if self.projecturl is not None: clipboard = qApp.clipboard() clipboard.setText(str(self.projecturl)) """ This is required by the dockwidget class, otherwise unused. """ def canvasChanged(self, canvas): pass """ Add docker to program """ Application.addDockWidgetFactory(DockWidgetFactory("comics_project_manager_docker", DockWidgetFactoryBase.DockRight, comics_project_manager_docker)) diff --git a/plugins/python/comics_project_management_tools/comics_project_page_viewer.py b/plugins/python/comics_project_management_tools/comics_project_page_viewer.py index 3c7523e75d..2a2292bc39 100644 --- a/plugins/python/comics_project_management_tools/comics_project_page_viewer.py +++ b/plugins/python/comics_project_management_tools/comics_project_page_viewer.py @@ -1,51 +1,68 @@ """ -Part of the comics project management tools (CPMT). +Copyright (c) 2017 Wolthera van Hövell tot Westerflier +This file is part of the Comics Project Management Tools(CPMT). + +CPMT 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 3 of the License, or +(at your option) any later version. + +CPMT 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 the CPMT. If not, see . +""" + +""" This is a docker that shows your comic pages. """ from PyQt5.QtGui import QImage, QPainter from PyQt5.QtWidgets import QDialog, QWidget, QVBoxLayout, QSizePolicy, QDialogButtonBox from PyQt5.QtCore import QSize, Qt from krita import * class page_viewer(QWidget): def __init__(self, parent=None, flags=None): super(page_viewer, self).__init__(parent) self.image = QImage() self.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding) def set_image(self, image=QImage()): self.image = image self.update() def paintEvent(self, event): painter = QPainter(self) image = self.image.scaled(self.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation) painter.drawImage(0, 0, image) def sizeHint(self): return QSize(256, 256) class comics_project_page_viewer(QDialog): currentPageNumber = 0 def __init__(self): super().__init__() self.setModal(False) self.setWindowTitle(i18n("Comics page viewer.")) self.setMinimumSize(200, 200) self.listOfImages = [QImage()] self.setLayout(QVBoxLayout()) self.viewer = page_viewer() self.layout().addWidget(self.viewer) buttonBox = QDialogButtonBox(QDialogButtonBox.Close) buttonBox.rejected.connect(self.close) self.layout().addWidget(buttonBox) def update_image(self, image=QImage()): self.viewer.set_image(image) diff --git a/plugins/python/comics_project_management_tools/comics_project_settings_dialog.py b/plugins/python/comics_project_management_tools/comics_project_settings_dialog.py index 3f33b933da..8a44e85420 100644 --- a/plugins/python/comics_project_management_tools/comics_project_settings_dialog.py +++ b/plugins/python/comics_project_management_tools/comics_project_settings_dialog.py @@ -1,171 +1,188 @@ """ -Part of the comics project management tools (CPMT). +Copyright (c) 2017 Wolthera van Hövell tot Westerflier +This file is part of the Comics Project Management Tools(CPMT). + +CPMT 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 3 of the License, or +(at your option) any later version. + +CPMT 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 the CPMT. If not, see . +""" + +""" A dialog for editing the general project settings. """ import os from PyQt5.QtWidgets import QWidget, QDialog, QDialogButtonBox, QHBoxLayout, QFormLayout, QPushButton, QLabel, QLineEdit, QToolButton, QFrame, QAction, QFileDialog, QComboBox, QSizePolicy from PyQt5.QtCore import QDir, Qt, pyqtSignal from krita import * """ A Widget that contains both a qlabel and a button for selecting a path. """ class path_select(QWidget): projectUrl = "" question = i18n("Which folder?") """ emits when a new directory has been chosen. """ locationChanged = pyqtSignal() """ Initialise the widget. @param question is the question asked when selecting a directory. @param project url is the url to which the label is relative. """ def __init__(self, parent=None, flags=None, question=str(), projectUrl=None): super(path_select, self).__init__(parent) self.setLayout(QHBoxLayout()) self.location = QLabel() self.button = QToolButton() # Until we have a proper icon self.layout().addWidget(self.location) self.layout().addWidget(self.button) self.layout().setContentsMargins(0, 0, 0, 0) self.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Minimum) self.location.setFrameStyle(QFrame.StyledPanel | QFrame.Sunken) self.button.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) self.location.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Minimum) self.location.setAlignment(Qt.AlignRight) self.location.setLineWidth(1) if projectUrl is None: self.projectUrl = QDir.homePath() else: self.projectUrl = projectUrl self.question = question self.action_change_folder = QAction(i18n("Change Folder"), self) self.action_change_folder.setIconText("...") self.action_change_folder.triggered.connect(self.slot_change_location) self.button.setDefaultAction(self.action_change_folder) """ pops up a directory chooser widget, and when a directory is chosen a locationChanged signal is emitted. """ def slot_change_location(self): location = QFileDialog.getExistingDirectory(caption=self.question, directory=self.projectUrl) if location is not None and location.isspace() is False and len(location) > 0: location = os.path.relpath(location, self.projectUrl) self.location.setText(location) self.locationChanged.emit() """ Set the location. @param path - the location relative to the projectUrl. """ def setLocation(self, path=str()): self.location.setText(path) """ Get the location. @returns a string with the location relative to the projectUrl. """ def getLocation(self): return str(self.location.text()) """ Dialog for editing basic proect details like the project name, default template, template location, etc. """ class comics_project_details_editor(QDialog): configGroup = "ComicsProjectManagementTools" """ Initialise the editor. @param projectUrl - The directory to which all paths are relative. """ def __init__(self, projectUrl=str()): super().__init__() self.projectUrl = projectUrl layout = QFormLayout() self.setLayout(layout) self.setWindowTitle(i18n("Comic Project Settings")) buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) buttons.accepted.connect(self.accept) buttons.rejected.connect(self.reject) self.lnProjectName = QLineEdit() self.lnProjectConcept = QLineEdit() self.cmb_defaultTemplate = QComboBox() self.pagesLocation = path_select(question=i18n("Where should the pages go?"), projectUrl=self.projectUrl) self.exportLocation = path_select(question=i18n("Where should the export go?"), projectUrl=self.projectUrl) self.templateLocation = path_select(question=i18n("Where are the templates?"), projectUrl=self.projectUrl) self.keyLocation = path_select(question=i18n("Where are the extra auto-completion keys located?")) self.keyLocation.setToolTip(i18n("The location for extra autocompletion keys in the meta-data editor. Point this at a folder containing key_characters/key_format/key_genre/key_rating/key_author_roles/key_other with inside txt files(csv for tating) containing the extra auto-completion keys, each on a new line. This path is stored in the krita configuration, and not the project configuration.")) self.templateLocation.locationChanged.connect(self.refill_templates) layout.addRow(i18n("Project Name:"), self.lnProjectName) layout.addRow(i18n("Project Concept:"), self.lnProjectConcept) layout.addRow(i18n("Pages Folder:"), self.pagesLocation) layout.addRow(i18n("Export Folder:"), self.exportLocation) layout.addRow(i18n("Template Folder:"), self.templateLocation) layout.addRow(i18n("Default Template:"), self.cmb_defaultTemplate) layout.addRow(i18n("Extra Keys Folder:"), self.keyLocation) self.layout().addWidget(buttons) """ Fill the templates doc with the kra files found in the templates directory. Might want to extend this to other files as well, as they basically get resaved anyway... """ def refill_templates(self): self.cmb_defaultTemplate.clear() templateLocation = os.path.join(self.projectUrl, self.templateLocation.getLocation()) for entry in os.scandir(templateLocation): if entry.name.endswith('.kra') and entry.is_file(): name = os.path.relpath(entry.path, templateLocation) self.cmb_defaultTemplate.addItem(name) """ Load the UI values from the config dictionary given. """ def setConfig(self, config, projectUrl): self.projectUrl = projectUrl if "projectName"in config.keys(): self.lnProjectName.setText(config["projectName"]) if "concept"in config.keys(): self.lnProjectConcept.setText(config["concept"]) if "pagesLocation" in config.keys(): self.pagesLocation.setLocation(config["pagesLocation"]) if "exportLocation" in config.keys(): self.exportLocation.setLocation(config["exportLocation"]) if "templateLocation" in config.keys(): self.templateLocation.setLocation(config["templateLocation"]) self.refill_templates() self.keyLocation.setLocation(Application.readSetting(self.configGroup, "extraKeysLocation", str())) """ Store the GUI values into the config dictionary given. @return the config diactionary filled with new values. """ def getConfig(self, config): config["projectName"] = self.lnProjectName.text() config["concept"] = self.lnProjectConcept.text() config["pagesLocation"] = self.pagesLocation.getLocation() config["exportLocation"] = self.exportLocation.getLocation() config["templateLocation"] = self.templateLocation.getLocation() config["singlePageTemplate"] = os.path.join(self.templateLocation.getLocation(), self.cmb_defaultTemplate.currentText()) Application.writeSetting(self.configGroup, "extraKeysLocation", self.keyLocation.getLocation()) return config diff --git a/plugins/python/comics_project_management_tools/comics_project_setup_wizard.py b/plugins/python/comics_project_management_tools/comics_project_setup_wizard.py index fca3f4b80c..48f9575c43 100644 --- a/plugins/python/comics_project_management_tools/comics_project_setup_wizard.py +++ b/plugins/python/comics_project_management_tools/comics_project_setup_wizard.py @@ -1,176 +1,193 @@ """ -Part of the comics project management tools (CPMT). +Copyright (c) 2017 Wolthera van Hövell tot Westerflier +This file is part of the Comics Project Management Tools(CPMT). + +CPMT 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 3 of the License, or +(at your option) any later version. + +CPMT 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 the CPMT. If not, see . +""" + +""" This is a wizard that helps you set up a comics project in Krita. """ import json # For writing to json. import os # For finding the script location. from pathlib import Path # For reading all the files in a directory. import random # For selecting two random words from a list. from PyQt5.QtWidgets import QWidget, QWizard, QWizardPage, QHBoxLayout, QFormLayout, QFileDialog, QLineEdit, QPushButton, QCheckBox, QLabel, QDialog from PyQt5.QtCore import QDate, QLocale from krita import * from . import comics_metadata_dialog """ The actual wizard. """ class ComicsProjectSetupWizard(): setupDictionary = {} projectDirectory = "" def __init__(self): # super().__init__(parent) # Search the location of the script for the two lists that are used with the projectname generator. mainP = Path(__file__).parent self.generateListA = [] self.generateListB = [] if Path(mainP / "projectGenLists" / "listA.txt").exists(): for l in open(str(mainP / "projectGenLists" / "listA.txt"), "r"): if l.isspace() == False: self.generateListA.append(l.strip("\n")) if Path(mainP / "projectGenLists" / "listB.txt").exists(): for l in open(str(mainP / "projectGenLists" / "listB.txt"), "r"): if l.isspace() == False: self.generateListB.append(l.strip("\n")) def showDialog(self): # Initialise the setup directory empty toavoid exceptions. self.setupDictionary = {} # ask for a project directory. self.projectDirectory = QFileDialog.getExistingDirectory(caption=i18n("Where should the comic project go?"), options=QFileDialog.ShowDirsOnly) if os.path.exists(self.projectDirectory) is False: return self.pagesDirectory = os.path.relpath(self.projectDirectory, self.projectDirectory) self.exportDirectory = os.path.relpath(self.projectDirectory, self.projectDirectory) wizard = QWizard() wizard.setWindowTitle(i18n("Comic Project Setup")) wizard.setOption(QWizard.IndependentPages, True) # Set up the UI for the wizard basicsPage = QWizardPage() basicsPage.setTitle(i18n("Basic Comic Project Settings")) formLayout = QFormLayout() basicsPage.setLayout(formLayout) projectLayout = QHBoxLayout() self.lnProjectName = QLineEdit() basicsPage.registerField("Project Name*", self.lnProjectName) self.lnProjectName.setToolTip(i18n("A Project name. This can be different from the eventual title")) btnRandom = QPushButton() btnRandom.setText(i18n("Generate")) btnRandom.setToolTip(i18n("If you cannot come up with a project name, our highly sophisticated project name generator will serve to give a classy yet down to earth name.")) btnRandom.clicked.connect(self.slot_generate) projectLayout.addWidget(self.lnProjectName) projectLayout.addWidget(btnRandom) lnConcept = QLineEdit() lnConcept.setToolTip(i18n("What is your comic about? This is mostly for your own convenience so don't worry about what it says too much.")) self.cmbLanguage = comics_metadata_dialog.language_combo_box() self.cmbLanguage.setToolTip(i18n("The main language the comic is in")) self.cmbLanguage.setEntryToCode(str(QLocale.system().name()).split("_")[0]) self.lnProjectDirectory = QLabel(self.projectDirectory) self.chkMakeProjectDirectory = QCheckBox(i18n("Make a new directory with the project name.")) self.chkMakeProjectDirectory.setToolTip(i18n("This allows you to select a generic comics project directory, in which a new folder will be made for the project using the given project name.")) self.chkMakeProjectDirectory.setChecked(True) self.lnPagesDirectory = QLineEdit() self.lnPagesDirectory.setText(i18n("pages")) self.lnPagesDirectory.setToolTip(i18n("The name for the folder where the pages are contained. If it doesn't exist, it will be created.")) self.lnExportDirectory = QLineEdit() self.lnExportDirectory.setText(i18n("export")) self.lnExportDirectory.setToolTip(i18n("The name for the folder where the export is put. If it doesn't exist, it will be created.")) self.lnTemplateLocation = QLineEdit() self.lnTemplateLocation.setText(i18n("templates")) self.lnTemplateLocation.setToolTip(i18n("The name for the folder where the page templates are sought in.")) formLayout.addRow(i18n("Comic Concept:"), lnConcept) formLayout.addRow(i18n("Project Name:"), projectLayout) formLayout.addRow(i18n("Main Language:"), self.cmbLanguage) buttonMetaData = QPushButton(i18n("Meta Data")) buttonMetaData.clicked.connect(self.slot_edit_meta_data) wizard.addPage(basicsPage) foldersPage = QWizardPage() foldersPage.setTitle(i18n("Folder names and other.")) folderFormLayout = QFormLayout() foldersPage.setLayout(folderFormLayout) folderFormLayout.addRow(i18n("Project Directory:"), self.lnProjectDirectory) folderFormLayout.addRow("", self.chkMakeProjectDirectory) folderFormLayout.addRow(i18n("Pages Directory"), self.lnPagesDirectory) folderFormLayout.addRow(i18n("Export Directory"), self.lnExportDirectory) folderFormLayout.addRow(i18n("Template Directory"), self.lnTemplateLocation) folderFormLayout.addRow("", buttonMetaData) wizard.addPage(foldersPage) # Execute the wizard, and after wards... if (wizard.exec_()): # First get the directories, check if the directories exist, and otherwise make them. self.pagesDirectory = self.lnPagesDirectory.text() self.exportDirectory = self.lnExportDirectory.text() self.templateLocation = self.lnTemplateLocation.text() projectPath = Path(self.projectDirectory) # Only make a project directory if the checkbox for that has been checked. if self.chkMakeProjectDirectory.isChecked(): projectPath = projectPath / self.lnProjectName.text() if projectPath.exists() is False: projectPath.mkdir() self.projectDirectory = str(projectPath) if Path(projectPath / self.pagesDirectory).exists() is False: Path(projectPath / self.pagesDirectory).mkdir() if Path(projectPath / self.exportDirectory).exists() is False: Path(projectPath / self.exportDirectory).mkdir() if Path(projectPath / self.templateLocation).exists() is False: Path(projectPath / self.templateLocation).mkdir() # Then store the information into the setup diactionary. self.setupDictionary["projectName"] = self.lnProjectName.text() self.setupDictionary["concept"] = lnConcept.text() self.setupDictionary["language"] = str(self.cmbLanguage.codeForCurrentEntry()) self.setupDictionary["pagesLocation"] = self.pagesDirectory self.setupDictionary["exportLocation"] = self.exportDirectory self.setupDictionary["templateLocation"] = self.templateLocation # Finally, write the dictionary into the json file. self.writeConfig() """ This calls up the metadata dialog, for if people already have information they want to type in at the setup stage. Not super likely, but the organisation and management aspect of the comic manager means we should give the option to organise as smoothly as possible. """ def slot_edit_meta_data(self): dialog = comics_metadata_dialog.comic_meta_data_editor() self.setupDictionary["language"] = str(self.cmbLanguage.codeForCurrentEntry()) dialog.setConfig(self.setupDictionary) dialog.setConfig(self.setupDictionary) if (dialog.exec_() == QDialog.Accepted): self.setupDictionary = dialog.getConfig(self.setupDictionary) self.cmbLanguage.setEntryToCode(self.setupDictionary["language"]) """ Write the actual config to the chosen project directory. """ def writeConfig(self): print("CPMT: writing comic configuration...") print(self.projectDirectory) configFile = open(os.path.join(self.projectDirectory, "comicConfig.json"), "w", newline="", encoding="utf-16") json.dump(self.setupDictionary, configFile, indent=4, sort_keys=True, ensure_ascii=False) configFile.close() print("CPMT: done") """ As you may be able to tell, the random projectname generator is hardly sophisticated. It picks a word from a list of names of figures from Greek Mythology, and a name from a list of vegetables and fruits and combines the two camelcased. It makes for good codenames at the least. """ def slot_generate(self): if len(self.generateListA) > 0 and len(self.generateListB) > 0: nameA = self.generateListA[random.randint(0, len(self.generateListA) - 1)] nameB = self.generateListB[random.randint(0, len(self.generateListB) - 1)] self.lnProjectName.setText(str(nameA.title() + nameB.title())) diff --git a/plugins/python/comics_project_management_tools/comics_template_dialog.py b/plugins/python/comics_project_management_tools/comics_template_dialog.py index ca0f1eebb0..7ccaac7eab 100644 --- a/plugins/python/comics_project_management_tools/comics_template_dialog.py +++ b/plugins/python/comics_project_management_tools/comics_template_dialog.py @@ -1,421 +1,438 @@ """ -Part of the comics project management tools (CPMT). +Copyright (c) 2017 Wolthera van Hövell tot Westerflier +This file is part of the Comics Project Management Tools(CPMT). + +CPMT 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 3 of the License, or +(at your option) any later version. + +CPMT 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 the CPMT. If not, see . +""" + +""" Template dialog """ import os import shutil #from PyQt5.QtGui import * from PyQt5.QtWidgets import QDialog, QComboBox, QDialogButtonBox, QVBoxLayout, QFormLayout, QGridLayout, QWidget, QPushButton, QHBoxLayout, QLabel, QSpinBox, QDoubleSpinBox, QLineEdit, QTabWidget from PyQt5.QtCore import QLocale, Qt, QByteArray, QRectF from PyQt5.QtGui import QImage, QPainter, QPixmap from krita import * """ Quick and dirty QComboBox subclassing that handles unitconversion for us. """ class simpleUnitBox(QComboBox): pixels = i18n("Pixels") inches = i18n("Inches") centimeter = i18n("Centimeter") millimeter = i18n("millimeter") def __init__(self): super(simpleUnitBox, self).__init__() self.addItem(self.pixels) self.addItem(self.inches) self.addItem(self.centimeter) self.addItem(self.millimeter) if QLocale().system().measurementSystem() is QLocale.MetricSystem: self.setCurrentIndex(2) # set to centimeter if metric system. else: self.setCurrentIndex(1) def pixelsForUnit(self, unit, DPI): if (self.currentText() == self.pixels): return unit elif (self.currentText() == self.inches): return self.inchesToPixels(unit, DPI) elif (self.currentText() == self.centimeter): return self.centimeterToPixels(unit, DPI) elif (self.currentText() == self.millimeter): return self.millimeterToPixels(unit, DPI) def inchesToPixels(self, inches, DPI): return DPI * inches def centimeterToInches(self, cm): return cm / 2.54 def centimeterToPixels(self, cm, DPI): return self.inchesToPixels(self.centimeterToInches(cm), DPI) def millimeterToCentimeter(self, mm): return mm / 10 def millimeterToPixels(self, mm, DPI): return self.inchesToPixels(self.centimeterToInches(self.millimeterToCentimeter(mm)), DPI) class comics_template_dialog(QDialog): templateDirectory = str() templates = QComboBox() buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) def __init__(self, templateDirectory): super().__init__() self.templateDirectory = templateDirectory self.setWindowTitle(i18n("Add new template")) self.setLayout(QVBoxLayout()) self.templates.setEnabled(False) self.fill_templates() self.buttons.accepted.connect(self.accept) self.buttons.rejected.connect(self.reject) self.buttons.button(QDialogButtonBox.Ok).setEnabled(False) mainWidget = QWidget() self.layout().addWidget(mainWidget) self.layout().addWidget(self.buttons) mainWidget.setLayout(QVBoxLayout()) btn_create = QPushButton(i18n("Create a template")) btn_create.clicked.connect(self.slot_create_template) btn_import = QPushButton(i18n("Import templates")) btn_import.clicked.connect(self.slot_import_template) mainWidget.layout().addWidget(self.templates) mainWidget.layout().addWidget(btn_create) mainWidget.layout().addWidget(btn_import) def fill_templates(self): self.templates.clear() for entry in os.scandir(self.templateDirectory): if entry.name.endswith('.kra') and entry.is_file(): name = os.path.relpath(entry.path, self.templateDirectory) self.templates.addItem(name) if self.templates.model().rowCount() > 0: self.templates.setEnabled(True) self.buttons.button(QDialogButtonBox.Ok).setEnabled(True) def slot_create_template(self): create = comics_template_create(self.templateDirectory) if create.exec_() == QDialog.Accepted: if (create.prepare_krita_file()): self.fill_templates() def slot_import_template(self): filenames = QFileDialog.getOpenFileNames(caption=i18n("Which files should be added to the template folder?"), directory=self.templateDirectory, filter=str(i18n("Krita files") + "(*.kra)"))[0] for file in filenames: shutil.copy2(file, self.templateDirectory) self.fill_templates() def url(self): return os.path.join(self.templateDirectory, self.templates.currentText()) class comics_template_create(QDialog): urlSavedTemplate = str() templateDirectory = str() def __init__(self, templateDirectory): super().__init__() self.templateDirectory = templateDirectory self.setWindowTitle(i18n("Create new template")) self.setLayout(QVBoxLayout()) buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) buttons.accepted.connect(self.accept) buttons.rejected.connect(self.reject) mainWidget = QWidget() explanation = QLabel(i18n("This allows you to make a template document with guides.\nThe width and height are the size of the live-area, the safe area is the live area minus the margins, and the full image is the live area plus the bleeds.")) explanation.setWordWrap(True) self.layout().addWidget(explanation) self.layout().addWidget(mainWidget) self.layout().addWidget(buttons) mainWidget.setLayout(QHBoxLayout()) elements = QWidget() elements.setLayout(QVBoxLayout()) mainWidget.layout().addWidget(elements) self.imagePreview = QLabel() self.imagePreview.setMinimumSize(256, 256) mainWidget.layout().addWidget(self.imagePreview) self.templateName = QLineEdit() self.templateName.setPlaceholderText("...") elements.layout().addWidget(self.templateName) self.DPI = QSpinBox() self.DPI.setMaximum(1200) self.DPI.setValue(300) self.spn_width = QDoubleSpinBox() self.spn_width.setMaximum(10000) self.spn_height = QDoubleSpinBox() self.spn_height.setMaximum(10000) self.widthUnit = simpleUnitBox() self.heightUnit = simpleUnitBox() widgetSize = QWidget() sizeForm = QFormLayout() sizeForm.addRow(i18n("DPI:"), self.DPI) widthLayout = QHBoxLayout() widthLayout.addWidget(self.spn_width) widthLayout.addWidget(self.widthUnit) sizeForm.addRow(i18n("Width:"), widthLayout) heightLayout = QHBoxLayout() heightLayout.addWidget(self.spn_height) heightLayout.addWidget(self.heightUnit) sizeForm.addRow(i18n("Height:"), heightLayout) widgetSize.setLayout(sizeForm) elements.layout().addWidget(widgetSize) marginAndBleed = QTabWidget() elements.layout().addWidget(marginAndBleed) margins = QWidget() marginForm = QGridLayout() margins.setLayout(marginForm) self.marginLeft = QDoubleSpinBox() self.marginLeft.setMaximum(1000) self.marginLeftUnit = simpleUnitBox() self.marginRight = QDoubleSpinBox() self.marginRight.setMaximum(1000) self.marginRightUnit = simpleUnitBox() self.marginTop = QDoubleSpinBox() self.marginTop.setMaximum(1000) self.marginTopUnit = simpleUnitBox() self.marginBottom = QDoubleSpinBox() self.marginBottom.setMaximum(1000) self.marginBottomUnit = simpleUnitBox() marginForm.addWidget(QLabel(i18n("Left:")), 0, 0, Qt.AlignRight) marginForm.addWidget(self.marginLeft, 0, 1) marginForm.addWidget(self.marginLeftUnit, 0, 2) marginForm.addWidget(QLabel(i18n("Top:")), 1, 0, Qt.AlignRight) marginForm.addWidget(self.marginTop, 1, 1) marginForm.addWidget(self.marginTopUnit, 1, 2) marginForm.addWidget(QLabel(i18n("Right:")), 2, 0, Qt.AlignRight) marginForm.addWidget(self.marginRight, 2, 1) marginForm.addWidget(self.marginRightUnit, 2, 2) marginForm.addWidget(QLabel(i18n("Bottom:")), 3, 0, Qt.AlignRight) marginForm.addWidget(self.marginBottom, 3, 1) marginForm.addWidget(self.marginBottomUnit, 3, 2) marginAndBleed.addTab(margins, i18n("Margins")) bleeds = QWidget() bleedsForm = QGridLayout() bleeds.setLayout(bleedsForm) self.bleedLeft = QDoubleSpinBox() self.bleedLeft.setMaximum(1000) self.bleedLeftUnit = simpleUnitBox() self.bleedRight = QDoubleSpinBox() self.bleedRight.setMaximum(1000) self.bleedRightUnit = simpleUnitBox() self.bleedTop = QDoubleSpinBox() self.bleedTop.setMaximum(1000) self.bleedTopUnit = simpleUnitBox() self.bleedBottom = QDoubleSpinBox() self.bleedBottom.setMaximum(1000) self.bleedBottomUnit = simpleUnitBox() bleedsForm.addWidget(QLabel(i18n("Left:")), 0, 0, Qt.AlignRight) bleedsForm.addWidget(self.bleedLeft, 0, 1) bleedsForm.addWidget(self.bleedLeftUnit, 0, 2) bleedsForm.addWidget(QLabel(i18n("Top:")), 1, 0, Qt.AlignRight) bleedsForm.addWidget(self.bleedTop, 1, 1) bleedsForm.addWidget(self.bleedTopUnit, 1, 2) bleedsForm.addWidget(QLabel(i18n("Right:")), 2, 0, Qt.AlignRight) bleedsForm.addWidget(self.bleedRight, 2, 1) bleedsForm.addWidget(self.bleedRightUnit, 2, 2) bleedsForm.addWidget(QLabel(i18n("Bottom:")), 3, 0, Qt.AlignRight) bleedsForm.addWidget(self.bleedBottom, 3, 1) bleedsForm.addWidget(self.bleedBottomUnit, 3, 2) marginAndBleed.addTab(bleeds, i18n("Bleeds")) if QLocale().system().measurementSystem() is QLocale.MetricSystem: self.setDefaults("European") else: self.setDefaults("American") self.spn_width.valueChanged.connect(self.updateImagePreview) self.widthUnit.currentIndexChanged.connect(self.updateImagePreview) self.spn_height.valueChanged.connect(self.updateImagePreview) self.heightUnit.currentIndexChanged.connect(self.updateImagePreview) self.marginLeft.valueChanged.connect(self.updateImagePreview) self.marginLeftUnit.currentIndexChanged.connect(self.updateImagePreview) self.marginRight.valueChanged.connect(self.updateImagePreview) self.marginRightUnit.currentIndexChanged.connect(self.updateImagePreview) self.marginTop.valueChanged.connect(self.updateImagePreview) self.marginTopUnit.currentIndexChanged.connect(self.updateImagePreview) self.marginBottom.valueChanged.connect(self.updateImagePreview) self.marginBottomUnit.currentIndexChanged.connect(self.updateImagePreview) self.bleedLeft.valueChanged.connect(self.updateImagePreview) self.bleedLeftUnit.currentIndexChanged.connect(self.updateImagePreview) self.bleedRight.valueChanged.connect(self.updateImagePreview) self.bleedRightUnit.currentIndexChanged.connect(self.updateImagePreview) self.bleedTop.valueChanged.connect(self.updateImagePreview) self.bleedTopUnit.currentIndexChanged.connect(self.updateImagePreview) self.bleedBottom.valueChanged.connect(self.updateImagePreview) self.bleedBottomUnit.currentIndexChanged.connect(self.updateImagePreview) self.updateImagePreview() def prepare_krita_file(self): wBase = max(self.widthUnit.pixelsForUnit(self.spn_width.value(), self.DPI.value()), 1) bL = self.bleedLeftUnit.pixelsForUnit(self.bleedLeft.value(), self.DPI.value()) bR = self.bleedRightUnit.pixelsForUnit(self.bleedRight.value(), self.DPI.value()) mL = self.marginLeftUnit.pixelsForUnit(self.marginLeft.value(), self.DPI.value()) mR = self.marginRightUnit.pixelsForUnit(self.marginRight.value(), self.DPI.value()) hBase = max(self.heightUnit.pixelsForUnit(self.spn_height.value(), self.DPI.value()), 1) bT = self.bleedTopUnit.pixelsForUnit(self.bleedTop.value(), self.DPI.value()) bB = self.bleedBottomUnit.pixelsForUnit(self.bleedBottom.value(), self.DPI.value()) mT = self.marginTopUnit.pixelsForUnit(self.marginTop.value(), self.DPI.value()) mB = self.marginBottomUnit.pixelsForUnit(self.marginBottom.value(), self.DPI.value()) template = Application.createDocument((wBase + bL + bR), (hBase + bT + bB), self.templateName.text(), "RGBA", "U8", "sRGB built-in", self.DPI.value()) backgroundNode = template.activeNode() backgroundNode.setName(i18n("Background")) pixelByteArray = QByteArray() pixelByteArray = backgroundNode.pixelData(0, 0, (wBase + bL + bR), (hBase + bT + bB)) white = int(255) pixelByteArray.fill(white.to_bytes(1, byteorder='little')) backgroundNode.setPixelData(pixelByteArray, 0, 0, (wBase + bL + bR), (hBase + bT + bB)) backgroundNode.setLocked(True) sketchNode = template.createNode(i18n("Sketch"), "paintlayer") template.rootNode().setChildNodes([backgroundNode, sketchNode]) verticalGuides = [] verticalGuides.append(bL) verticalGuides.append(bL + mL) verticalGuides.append((bL + wBase) - mR) verticalGuides.append(bL + wBase) horizontalGuides = [] horizontalGuides.append(bT) horizontalGuides.append(bT + mT) horizontalGuides.append((bT + hBase) - mB) horizontalGuides.append(bT + hBase) template.setHorizontalGuides(horizontalGuides) template.setVerticalGuides(verticalGuides) template.setGuidesVisible(True) template.setGuidesLocked(True) self.urlSavedTemplate = os.path.join(self.templateDirectory, self.templateName.text() + ".kra") success = template.exportImage(self.urlSavedTemplate, InfoObject()) print("CPMT: Template", self.templateName.text(), "made and saved.") template.waitForDone() template.close() return success def updateImagePreview(self): maxSize = 256 wBase = max(self.widthUnit.pixelsForUnit(self.spn_width.value(), self.DPI.value()), 1) bL = self.bleedLeftUnit.pixelsForUnit(self.bleedLeft.value(), self.DPI.value()) bR = self.bleedRightUnit.pixelsForUnit(self.bleedRight.value(), self.DPI.value()) mL = self.marginLeftUnit.pixelsForUnit(self.marginLeft.value(), self.DPI.value()) mR = self.marginRightUnit.pixelsForUnit(self.marginRight.value(), self.DPI.value()) hBase = max(self.heightUnit.pixelsForUnit(self.spn_height.value(), self.DPI.value()), 1) bT = self.bleedTopUnit.pixelsForUnit(self.bleedTop.value(), self.DPI.value()) bB = self.bleedBottomUnit.pixelsForUnit(self.bleedBottom.value(), self.DPI.value()) mT = self.marginTopUnit.pixelsForUnit(self.marginTop.value(), self.DPI.value()) mB = self.marginBottomUnit.pixelsForUnit(self.marginBottom.value(), self.DPI.value()) scaleRatio = maxSize / (hBase + bT + bB) if wBase > hBase: scaleRatio = maxSize / (wBase + bR + bL) width = (wBase + bL + bR) * scaleRatio height = (hBase + bT + bB) * scaleRatio topLeft = [max((maxSize - width) / 2, 0), max((maxSize - height) / 2, 0)] image = QImage(maxSize, maxSize, QImage.Format_ARGB32) image.fill(Qt.transparent) p = QPainter(image) p.setBrush(Qt.white) CanvasSize = QRectF(topLeft[0], topLeft[1], width, height) p.drawRect(CanvasSize.toRect()) # Draw bleeds. PageSize = CanvasSize PageSize.setWidth(width - (bR * scaleRatio)) PageSize.setHeight(height - (bB * scaleRatio)) PageSize.setX(PageSize.x() + (bL * scaleRatio)) PageSize.setY(PageSize.y() + (bT * scaleRatio)) p.setPen(Qt.blue) p.setBrush(Qt.transparent) p.drawRect(PageSize.toRect()) # Draw liveArea LiveArea = PageSize LiveArea.setWidth(LiveArea.width() - (mR * scaleRatio)) LiveArea.setHeight(LiveArea.height() - (mB * scaleRatio)) LiveArea.setX(LiveArea.x() + (mL * scaleRatio)) LiveArea.setY(LiveArea.y() + (mT * scaleRatio)) p.setPen(Qt.blue) p.drawRect(LiveArea.toRect()) p.end() self.imagePreview.setPixmap(QPixmap.fromImage(image)) def setDefaults(self, type): if type == "American": # American 11x17 inch self.spn_width.setValue(11) self.widthUnit.setCurrentIndex(1) self.spn_height.setValue(17) self.heightUnit.setCurrentIndex(1) self.bleedBottom.setValue(1) self.bleedBottomUnit.setCurrentIndex(1) self.bleedTop.setValue(1) self.bleedTopUnit.setCurrentIndex(1) self.bleedLeft.setValue(0.5) self.bleedLeftUnit.setCurrentIndex(1) self.bleedRight.setValue(0.5) self.bleedRightUnit.setCurrentIndex(1) self.marginBottom.setValue(0.745) self.marginBottomUnit.setCurrentIndex(1) self.marginTop.setValue(0.745) self.marginTopUnit.setCurrentIndex(1) self.marginRight.setValue(0.5) self.marginRightUnit.setCurrentIndex(1) self.marginLeft.setValue(0.5) self.marginLeftUnit.setCurrentIndex(1) if type == "European": # European A4 self.spn_width.setValue(21) self.widthUnit.setCurrentIndex(2) self.spn_height.setValue(29.7) self.heightUnit.setCurrentIndex(2) self.bleedBottom.setValue(5) self.bleedBottomUnit.setCurrentIndex(3) self.bleedTop.setValue(5) self.bleedTopUnit.setCurrentIndex(3) self.bleedLeft.setValue(5) self.bleedLeftUnit.setCurrentIndex(3) self.bleedRight.setValue(5) self.bleedRightUnit.setCurrentIndex(3) self.marginBottom.setValue(1.5) self.marginBottomUnit.setCurrentIndex(2) self.marginTop.setValue(1.5) self.marginTopUnit.setCurrentIndex(2) self.marginRight.setValue(1) self.marginRightUnit.setCurrentIndex(2) self.marginLeft.setValue(1) self.marginLeftUnit.setCurrentIndex(2) def url(self): return self.urlSavedTemplate diff --git a/plugins/python/palette_docker/palette_docker.py b/plugins/python/palette_docker/palette_docker.py index fc01d373ed..74e566009d 100644 --- a/plugins/python/palette_docker/palette_docker.py +++ b/plugins/python/palette_docker/palette_docker.py @@ -1,231 +1,246 @@ -# Description: A Python based docker that allows you to edit KPL color palettes. -# By Wolthera +''' +Description: A Python based docker that allows you to edit KPL color palettes. + +By Wolthera(originally) + +This script is licensed CC 0 1.0, so that you can learn from it. + +------ CC 0 1.0 --------------- + +The person who associated a work with this deed has dedicated the work to the public domain by waiving all of his or her rights to the work worldwide under copyright law, including all related and neighboring rights, to the extent allowed by law. + +You can copy, modify, distribute and perform the work, even for commercial purposes, all without asking permission. + +https://creativecommons.org/publicdomain/zero/1.0/legalcode + +@package palette_docker +''' # Importing the relevant dependencies: import sys from PyQt5.QtGui import QPixmap, QIcon, QImage, QPainter, QBrush, QPalette from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QComboBox, QAction, QTabWidget, QLineEdit, QSpinBox, QDialogButtonBox, QToolButton, QDialog, QPlainTextEdit, QCompleter, QMenu from PyQt5.Qt import Qt, pyqtSignal, pyqtSlot import math from krita import * # import the exporters from . import palette_exporter_gimppalette, palette_exporter_inkscapeSVG, palette_sortColors class Palette_Docker(DockWidget): # Init the docker def __init__(self): super().__init__() # make base-widget and layout widget = QWidget() layout = QVBoxLayout() buttonLayout = QHBoxLayout() widget.setLayout(layout) self.setWindowTitle("Python Palette Docker") # Make a combobox and add palettes self.cmb_palettes = QComboBox() allPalettes = Application.resources("palette") for palette_name in allPalettes: self.cmb_palettes.addItem(palette_name) self.cmb_palettes.model().sort(0) self.currentPalette = Palette(allPalettes[list(allPalettes.keys())[0]]) self.cmb_palettes.currentTextChanged.connect(self.slot_paletteChanged) layout.addWidget(self.cmb_palettes) # add combobox to the layout self.paletteView = PaletteView() self.paletteView.setPalette(self.currentPalette) layout.addWidget(self.paletteView) self.paletteView.entrySelectedForeGround.connect(self.slot_swatchSelected) self.colorComboBox = QComboBox() self.colorList = list() buttonLayout.addWidget(self.colorComboBox) self.addEntry = QAction(self) self.addEntry.setIconText("+") self.addEntry.triggered.connect(self.slot_add_entry) self.addGroup = QAction(self) self.addGroup.triggered.connect(self.slot_add_group) self.addGroup.setText("Add Group") self.addGroup.setIconText(str("\U0001F4C2")) self.removeEntry = QAction(self) self.removeEntry.setText("Remove Entry") self.removeEntry.setIconText("-") self.removeEntry.triggered.connect(self.slot_remove_entry) addEntryButton = QToolButton() addEntryButton.setDefaultAction(self.addEntry) buttonLayout.addWidget(addEntryButton) addGroupButton = QToolButton() addGroupButton.setDefaultAction(self.addGroup) buttonLayout.addWidget(addGroupButton) removeEntryButton = QToolButton() removeEntryButton.setDefaultAction(self.removeEntry) buttonLayout.addWidget(removeEntryButton) # QActions self.extra = QToolButton() self.editPaletteData = QAction(self) self.editPaletteData.setText("Edit Palette Settings") self.editPaletteData.triggered.connect(self.slot_edit_palette_data) self.extra.setDefaultAction(self.editPaletteData) buttonLayout.addWidget(self.extra) self.actionMenu = QMenu() self.exportToGimp = QAction(self) self.exportToGimp.setText("Export as GIMP palette file.") self.exportToGimp.triggered.connect(self.slot_export_to_gimp_palette) self.exportToInkscape = QAction(self) self.exportToInkscape.setText("Export as Inkscape SVG with swatches.") self.exportToInkscape.triggered.connect(self.slot_export_to_inkscape_svg) self.sortColors = QAction(self) self.sortColors.setText("Sort colors") self.sortColors.triggered.connect(self.slot_sort_colors) self.actionMenu.addAction(self.editPaletteData) self.actionMenu.addAction(self.exportToGimp) self.actionMenu.addAction(self.exportToInkscape) # self.actionMenu.addAction(self.sortColors) self.extra.setMenu(self.actionMenu) layout.addLayout(buttonLayout) self.slot_fill_combobox() self.setWidget(widget) # add widget to the docker def slot_paletteChanged(self, name): self.currentPalette = Palette(Application.resources("palette")[name]) self.paletteView.setPalette(self.currentPalette) self.slot_fill_combobox() @pyqtSlot('KoColorSetEntry') def slot_swatchSelected(self, entry): print("entry " + entry.name) if (self.canvas()) is not None: if (self.canvas().view()) is not None: name = entry.name if len(entry.id) > 0: name = entry.id + " - " + entry.name if len(name) > 0: if name in self.colorList: self.colorComboBox.setCurrentIndex(self.colorList.index(name)) color = self.currentPalette.colorForEntry(entry) self.canvas().view().setForeGroundColor(color) ''' A function for making a combobox with the available colors. We use QCompleter on the colorComboBox so that people can type in the name of a color to select it. This is useful for people with carefully made palettes where the colors are named properly, which makes it easier for them to find colors. ''' def slot_fill_combobox(self): if self.currentPalette is None: pass palette = self.currentPalette self.colorComboBox.clear() self.colorList.clear() for i in range(palette.colorsCountTotal()): entry = palette.colorSetEntryByIndex(i) color = palette.colorForEntry(entry).colorForCanvas(self.canvas()) colorSquare = QPixmap(12, 12) if entry.spotColor is True: img = colorSquare.toImage() circlePainter = QPainter() img.fill(self.colorComboBox.palette().color(QPalette.Base)) circlePainter.begin(img) brush = QBrush(Qt.SolidPattern) brush.setColor(color) circlePainter.setBrush(brush) circlePainter.pen().setWidth(0) circlePainter.drawEllipse(0, 0, 11, 11) circlePainter.end() colorSquare = QPixmap.fromImage(img) else: colorSquare.fill(color) name = entry.name if len(entry.id) > 0: name = entry.id + " - " + entry.name self.colorList.append(name) self.colorComboBox.addItem(QIcon(colorSquare), name) self.colorComboBox.setEditable(True) self.colorComboBox.setInsertPolicy(QComboBox.NoInsert) self.colorComboBox.completer().setCompletionMode(QCompleter.PopupCompletion) self.colorComboBox.completer().setCaseSensitivity(False) self.colorComboBox.completer().setFilterMode(Qt.MatchContains) self.colorComboBox.currentIndexChanged.connect(self.slot_get_color_from_combobox) def slot_get_color_from_combobox(self): if self.currentPalette is not None: entry = self.currentPalette.colorSetEntryByIndex(self.colorComboBox.currentIndex()) self.slot_swatchSelected(entry) def slot_add_entry(self): if (self.canvas()) is not None: if (self.canvas().view()) is not None: color = self.canvas().view().foreGroundColor() success = self.paletteView.addEntryWithDialog(color) if success is True: self.slot_fill_combobox() def slot_add_group(self): success = self.paletteView.addGroupWithDialog() if success is True: self.slot_fill_combobox() def slot_remove_entry(self): success = self.paletteView.removeSelectedEntryWithDialog() if success is True: self.slot_fill_combobox() ''' A function for giving a gui to edit palette metadata... I also want this to be the way to edit the settings of the palette docker. ''' def slot_edit_palette_data(self): dialog = QDialog(self) tabWidget = QTabWidget() dialog.setWindowTitle("Edit Palette Data") dialog.setLayout(QVBoxLayout()) dialog.layout().addWidget(tabWidget) paletteWidget = QWidget() paletteWidget.setLayout(QVBoxLayout()) tabWidget.addTab(paletteWidget, "Palette Data") paletteName = QLineEdit() paletteName.setText(self.cmb_palettes.currentText()) paletteWidget.layout().addWidget(paletteName) paletteColumns = QSpinBox() paletteColumns.setValue(self.currentPalette.columnCount()) paletteWidget.layout().addWidget(paletteColumns) paletteComment = QPlainTextEdit() paletteComment.appendPlainText(self.currentPalette.comment()) paletteWidget.layout().addWidget(paletteComment) buttons = QDialogButtonBox(QDialogButtonBox.Ok) dialog.layout().addWidget(buttons) buttons.accepted.connect(dialog.accept) # buttons.rejected.connect(dialog.reject()) if dialog.exec_() == QDialog.Accepted: Resource = Application.resources("palette")[self.cmb_palettes.currentText()] Resource.setName(paletteName.text()) self.currentPalette = Palette(Resource) print(paletteColumns.value()) self.currentPalette.setColumnCount(paletteColumns.value()) self.paletteView.setPalette(self.currentPalette) self.slot_fill_combobox() self.currentPalette.setComment(paletteComment.toPlainText()) self.currentPalette.save() def slot_export_to_gimp_palette(self): palette_exporter_gimppalette.gimpPaletteExporter(self.cmb_palettes.currentText()) def slot_export_to_inkscape_svg(self): palette_exporter_inkscapeSVG.inkscapeSVGExporter(self.cmb_palettes.currentText()) def slot_sort_colors(self): colorSorter = palette_sortColors.sortColors(self.cmb_palettes.currentText()) self.paletteView.setPalette(colorSorter.palette()) def canvasChanged(self, canvas): pass # Add docker to the application :) Application.addDockWidgetFactory(DockWidgetFactory("palette_docker", DockWidgetFactoryBase.DockRight, Palette_Docker)) diff --git a/plugins/python/palette_docker/palette_exporter_gimppalette.py b/plugins/python/palette_docker/palette_exporter_gimppalette.py index 1eb93c7d35..e5f4f6c020 100644 --- a/plugins/python/palette_docker/palette_exporter_gimppalette.py +++ b/plugins/python/palette_docker/palette_exporter_gimppalette.py @@ -1,60 +1,73 @@ ''' A script that converts the palette with the given name to a gimp palette at the location asked for. -By Wolthera. + +By Wolthera(originally) + +This script is licensed CC 0 1.0, so that you can learn from it. + +------ CC 0 1.0 --------------- + +The person who associated a work with this deed has dedicated the work to the public domain by waiving all of his or her rights to the work worldwide under copyright law, including all related and neighboring rights, to the extent allowed by law. + +You can copy, modify, distribute and perform the work, even for commercial purposes, all without asking permission. + +https://creativecommons.org/publicdomain/zero/1.0/legalcode + +@package palette_docker ''' # Importing the relevant dependencies: import sys from PyQt5.QtWidgets import QFileDialog, QMessageBox import math from krita import * class gimpPaletteExporter: def __init__(self, name): # We want people to select a palette and a location to save to... self.fileName = QFileDialog.getExistingDirectory() allPalettes = Application.resources("palette") self.paletteName = name self.currentPalette = Palette(allPalettes[self.paletteName]) self.export() done = QMessageBox() done.setWindowTitle("Export successful") done.setText(self.paletteName + " has been exported to " + self.fileName + "!") done.exec_() pass def export(self): # open the appropriate file... gplFile = open(self.fileName + "/" + self.paletteName + ".gpl", "w") gplFile.write("GIMP Palette\n") gplFile.write("Name: " + self.paletteName + "\n") gplFile.write("Columns: " + str(self.currentPalette.columnCount()) + "\n") gplFile.write("#" + self.currentPalette.comment() + "\n") colorCount = self.currentPalette.colorsCountGroup("") for i in range(colorCount): entry = self.currentPalette.colorSetEntryFromGroup(i, "") color = self.currentPalette.colorForEntry(entry) # convert to sRGB color.setColorSpace("RGBA", "U8", "sRGB built-in") red = max(min(int(color.componentsOrdered()[0] * 255), 255), 0) green = max(min(int(color.componentsOrdered()[1] * 255), 255), 0) blue = max(min(int(color.componentsOrdered()[2] * 255), 255), 0) gplFile.write(str(red) + " " + str(green) + " " + str(blue) + " " + entry.id + "-" + entry.name + "\n") groupNames = self.currentPalette.groupNames() for groupName in groupNames: colorCount = self.currentPalette.colorsCountGroup(groupName) for i in range(colorCount): entry = self.currentPalette.colorSetEntryFromGroup(i, groupName) color = self.currentPalette.colorForEntry(entry) # convert to sRGB color.setColorSpace("RGBA", "U8", "sRGB built-in") red = max(min(int(color.componentsOrdered()[0] * 255), 255), 0) green = max(min(int(color.componentsOrdered()[1] * 255), 255), 0) blue = max(min(int(color.componentsOrdered()[2] * 255), 255), 0) gplFile.write(str(red) + " " + str(green) + " " + str(blue) + " " + entry.id + "-" + entry.name + "\n") gplFile.close() diff --git a/plugins/python/palette_docker/palette_exporter_inkscapeSVG.py b/plugins/python/palette_docker/palette_exporter_inkscapeSVG.py index d0cb9b0cc3..4e92bd5735 100644 --- a/plugins/python/palette_docker/palette_exporter_inkscapeSVG.py +++ b/plugins/python/palette_docker/palette_exporter_inkscapeSVG.py @@ -1,174 +1,186 @@ ''' A script that converts the palette named "Default" to a SVG so that Inkscape may use the colors The icc-color stuff doesn't work right, because we'd need the ability to get the url of the colorprofile somehow, and then we can make color-profile things in the definitions. -By Wolthera. -''' +By Wolthera(originally) + +This script is licensed CC 0 1.0, so that you can learn from it. + +------ CC 0 1.0 --------------- + +The person who associated a work with this deed has dedicated the work to the public domain by waiving all of his or her rights to the work worldwide under copyright law, including all related and neighboring rights, to the extent allowed by law. + +You can copy, modify, distribute and perform the work, even for commercial purposes, all without asking permission. + +https://creativecommons.org/publicdomain/zero/1.0/legalcode + +@package palette_docker +''' # Importing the relevant dependencies: import sys from PyQt5.QtXml import QDomDocument from PyQt5.QtWidgets import QFileDialog, QMessageBox import math from krita import * class inkscapeSVGExporter: def __init__(self, name): # We want people to select a palette and a location to save to... self.fileName = QFileDialog.getExistingDirectory() allPalettes = Application.resources("palette") self.paletteName = name self.currentPalette = Palette(allPalettes[self.paletteName]) self.export() done = QMessageBox() done.setWindowTitle("Export successful") done.setText(self.paletteName + " has been exported to " + self.fileName + "!") done.exec_() pass def export(self): # open the appropriate file... svgFile = open(self.fileName + "/" + self.paletteName + ".svg", "w") svgDoc = QDomDocument() svgBaseElement = svgDoc.createElement("svg") svgBaseElement.setAttribute("xmlns:osb", "http://www.openswatchbook.org/uri/2009/osb") svgBaseElement.setAttribute("xmlns:svg", "http://www.w3.org/2000/svg") svgBaseElement.setAttribute("xmlns:dc", "http://purl.org/dc/elements/1.1/") svgBaseElement.setAttribute("xmlns:cc", "http://creativecommons.org/ns#") svgBaseElement.setAttribute("xmlns:rdf", "http://www.w3.org/1999/02/22-rdf-syntax-ns#") svgDefs = svgDoc.createElement("defs") svgSwatches = svgDoc.createElement("g") svgSwatches.setAttribute("id", "Swatches") svgMeta = svgDoc.createElement("metadata") svgBaseElement.appendChild(svgMeta) rdf = svgDoc.createElement("rdf:RDF") ccwork = svgDoc.createElement("cc:Work") dctitle = svgDoc.createElement("dc:title") dcdescription = svgDoc.createElement("dc:description") dctitle.appendChild(svgDoc.createTextNode(self.paletteName)) dcdescription.appendChild(svgDoc.createTextNode(self.currentPalette.comment())) ccwork.appendChild(dctitle) ccwork.appendChild(dcdescription) rdf.appendChild(ccwork) svgMeta.appendChild(rdf) Row = 0 Column = 0 iccProfileList = [] colorCount = self.currentPalette.colorsCountGroup("") for i in range(colorCount): entry = self.currentPalette.colorSetEntryFromGroup(i, "") color = self.currentPalette.colorForEntry(entry) iccColor = "icc-color(" + color.colorProfile() for c in range(len(color.componentsOrdered()) - 1): iccColor = iccColor + "," + str(color.componentsOrdered()[c]) iccColor = iccColor + ")" if color.colorProfile() not in iccProfileList: iccProfileList.append(color.colorProfile()) # convert to sRGB color.setColorSpace("RGBA", "U8", "sRGB built-in") red = max(min(int(color.componentsOrdered()[0] * 255), 255), 0) green = max(min(int(color.componentsOrdered()[1] * 255), 255), 0) blue = max(min(int(color.componentsOrdered()[2] * 255), 255), 0) hexcode = "#" + str(format(red, '02x')) + str(format(green, '02x')) + str(format(blue, '02x')) swatchName = str(i) + "-" + entry.name swatchName = swatchName.replace(" ", "-") swatchName = swatchName.replace("(", "-") swatchName = swatchName.replace(")", "-") swatchMain = svgDoc.createElement("linearGradient") swatchMain.setAttribute("osb:paint", "solid") swatchMain.setAttribute("id", swatchName) swatchSub = svgDoc.createElement("stop") swatchSub.setAttribute("style", "stop-color: " + hexcode + " " + iccColor + ";stop-opacity:1;") swatchMain.appendChild(swatchSub) svgDefs.appendChild(swatchMain) svgSingleSwatch = svgDoc.createElement("rect") svgSingleSwatch.setAttribute("x", str(int(Column * 20))) svgSingleSwatch.setAttribute("y", str(int(Row * 20))) svgSingleSwatch.setAttribute("width", str(int(20))) svgSingleSwatch.setAttribute("height", str(int(20))) svgSingleSwatch.setAttribute("fill", "url(#" + swatchName + ")") svgSingleSwatch.setAttribute("id", "swatch" + swatchName) if entry.spotColor is True: svgSingleSwatch.setAttribute("rx", str(10)) svgSingleSwatch.setAttribute("ry", str(10)) svgSwatches.appendChild(svgSingleSwatch) Column += 1 if (Column >= self.currentPalette.columnCount()): Column = 0 Row += 1 groupNames = self.currentPalette.groupNames() for groupName in groupNames: Column = 0 Row += 1 groupTitle = svgDoc.createElement("text") groupTitle.setAttribute("x", str(int(Column * 20))) groupTitle.setAttribute("y", str(int(Row * 20) + 15)) groupTitle.appendChild(svgDoc.createTextNode(groupName)) svgSwatches.appendChild(groupTitle) Row += 1 colorCount = self.currentPalette.colorsCountGroup(groupName) for i in range(colorCount): entry = self.currentPalette.colorSetEntryFromGroup(i, groupName) color = self.currentPalette.colorForEntry(entry) iccColor = "icc-color(" + color.colorProfile() for c in range(len(color.componentsOrdered()) - 1): iccColor = iccColor + "," + str(color.componentsOrdered()[c]) iccColor = iccColor + ")" if color.colorProfile() not in iccProfileList: iccProfileList.append(color.colorProfile()) # convert to sRGB color.setColorSpace("RGBA", "U8", "sRGB built-in") red = max(min(int(color.componentsOrdered()[0] * 255), 255), 0) green = max(min(int(color.componentsOrdered()[1] * 255), 255), 0) blue = max(min(int(color.componentsOrdered()[2] * 255), 255), 0) hexcode = "#" + str(format(red, '02x')) + str(format(green, '02x')) + str(format(blue, '02x')) swatchName = groupName + str(i) + "-" + entry.name swatchName = swatchName.replace(" ", "-") swatchName = swatchName.replace("(", "-") swatchName = swatchName.replace(")", "-") swatchMain = svgDoc.createElement("linearGradient") swatchMain.setAttribute("osb:paint", "solid") swatchMain.setAttribute("id", swatchName) swatchSub = svgDoc.createElement("stop") swatchSub.setAttribute("style", "stop-color: " + hexcode + " " + iccColor + ";stop-opacity:1;") swatchMain.appendChild(swatchSub) svgDefs.appendChild(swatchMain) svgSingleSwatch = svgDoc.createElement("rect") svgSingleSwatch.setAttribute("x", str(int(Column * 20))) svgSingleSwatch.setAttribute("y", str(int(Row * 20))) svgSingleSwatch.setAttribute("width", str(int(20))) svgSingleSwatch.setAttribute("height", str(int(20))) svgSingleSwatch.setAttribute("fill", "url(#" + swatchName + ")") svgSingleSwatch.setAttribute("id", "swatch " + swatchName) if entry.spotColor is True: svgSingleSwatch.setAttribute("rx", str(10)) svgSingleSwatch.setAttribute("ry", str(10)) svgSwatches.appendChild(svgSingleSwatch) Column += 1 if (Column >= self.currentPalette.columnCount()): Column = 0 Row += 1 for profile in iccProfileList: svgProfileDesc = svgDoc.createElement("color-profile") svgProfileDesc.setAttribute("name", profile) # This is incomplete because python api doesn't have any way to ask for this data yet. # svgProfileDesc.setAttribute("local", "sRGB") # svgProfileDesc.setAttribute("xlink:href", colorprofileurl) svgProfileDesc.setAttribute("rendering-intent", "perceptual") svgDefs.appendChild(svgProfileDesc) svgBaseElement.appendChild(svgDefs) svgBaseElement.appendChild(svgSwatches) svgBaseElement.setAttribute("viewBox", "0 0 " + str(self.currentPalette.columnCount() * 20) + " " + str(int((Row + 1) * 20))) svgDoc.appendChild(svgBaseElement) svgFile.write(svgDoc.toString()) svgFile.close() diff --git a/plugins/python/palette_docker/palette_sortColors.py b/plugins/python/palette_docker/palette_sortColors.py index 204230b39b..78bad6a946 100644 --- a/plugins/python/palette_docker/palette_sortColors.py +++ b/plugins/python/palette_docker/palette_sortColors.py @@ -1,67 +1,80 @@ ''' A script that sorts the colors in the group. -By Wolthera. + +By Wolthera(originally) + +This script is licensed CC 0 1.0, so that you can learn from it. + +------ CC 0 1.0 --------------- + +The person who associated a work with this deed has dedicated the work to the public domain by waiving all of his or her rights to the work worldwide under copyright law, including all related and neighboring rights, to the extent allowed by law. + +You can copy, modify, distribute and perform the work, even for commercial purposes, all without asking permission. + +https://creativecommons.org/publicdomain/zero/1.0/legalcode + +@package palette_docker ''' # Importing the relevant dependencies: import sys import math from krita import * class sortColors: def __init__(self, name): # We want people to select a palette... allPalettes = Application.resources("palette") self.paletteName = name self.currentPalette = Palette(allPalettes[self.paletteName]) self.sort_all_groups() def sort_all_groups(self): self.sort_color_by_name(str()) groupNames = self.currentPalette.groupNames() for groupName in groupNames: self.sort_color_by_name(groupName) def sort_color_by_name(self, groupName): l = {} colorCount = self.currentPalette.colorsCountGroup(groupName) for i in range(colorCount - 1, -1, -1): entry = self.currentPalette.colorSetEntryFromGroup((i), groupName) l[entry.name + str(i)] = entry self.currentPalette.removeEntry((i), groupName) for s in sorted(l): self.currentPalette.addEntry(l[s], groupName) def sort_color_by_id(self, groupName): l = {} colorCount = self.currentPalette.colorsCountGroup(groupName) for i in range(colorCount - 1, -1, -1): entry = self.currentPalette.colorSetEntryFromGroup((i), groupName) l[entry.id + " " + str(i)] = entry self.currentPalette.removeEntry((i), groupName) for s in sorted(l): self.currentPalette.addEntry(l[s], groupName) def sort_by_value(self, groupName): l = {} colorCount = self.currentPalette.colorsCountGroup(groupName) for i in range(colorCount - 1, -1, -1): entry = self.currentPalette.colorSetEntryFromGroup((i), groupName) color = self.currentPalette.colorForEntry(entry) color.setColorSpace("RGBA", "U8", "sRGB built-in") l[color.components()[0] + color.components()[1] + color.components()[2]] = entry self.currentPalette.removeEntry((i), groupName) for s in sorted(l): self.currentPalette.addEntry(l[s], groupName) def sort_by_hue(self, stepsize, groupName): pass def palette(self): return self.currentPalette diff --git a/plugins/python/quick_settings_docker/quick_settings_docker.py b/plugins/python/quick_settings_docker/quick_settings_docker.py index 96e4048a57..24312be815 100644 --- a/plugins/python/quick_settings_docker/quick_settings_docker.py +++ b/plugins/python/quick_settings_docker/quick_settings_docker.py @@ -1,165 +1,175 @@ ''' Description: A Python based docker for quickly choosing the brushsize like similar dockers in other drawing programs. -By Wolthera +By Wolthera(originally) + +This script is licensed CC 0 1.0, so that you can learn from it. + +------ CC 0 1.0 --------------- + +The person who associated a work with this deed has dedicated the work to the public domain by waiving all of his or her rights to the work worldwide under copyright law, including all related and neighboring rights, to the extent allowed by law. + +You can copy, modify, distribute and perform the work, even for commercial purposes, all without asking permission. + +https://creativecommons.org/publicdomain/zero/1.0/legalcode @package quick_settings_docker ''' # Importing the relevant dependencies: import sys from PyQt5.QtCore import pyqtSlot, Qt, QPointF from PyQt5.QtGui import QStandardItem, QStandardItemModel, QPainter, QPalette, QPixmap, QImage, QBrush, QPen, QIcon from PyQt5.QtWidgets import QWidget, QTabWidget, QListView, QVBoxLayout from krita import * class QuickSettingsDocker(DockWidget): # Init the docker def __init__(self): super().__init__() # make base-widget and layout widget = QWidget() layout = QVBoxLayout() widget.setLayout(layout) self.setWindowTitle("Quick Settings Docker") tabWidget = QTabWidget() self.brushSizeTableView = QListView() self.brushSizeTableView.setViewMode(QListView.IconMode) self.brushSizeTableView.setMovement(QListView.Static) self.brushSizeTableView.setResizeMode(QListView.Adjust) self.brushSizeTableView.setUniformItemSizes(True) self.brushSizeTableView.setSelectionMode(QListView.SingleSelection) self.brushOpacityTableView = QListView() self.brushOpacityTableView.setViewMode(QListView.IconMode) self.brushOpacityTableView.setMovement(QListView.Static) self.brushOpacityTableView.setResizeMode(QListView.Adjust) self.brushOpacityTableView.setUniformItemSizes(True) self.brushOpacityTableView.setSelectionMode(QListView.SingleSelection) self.brushFlowTableView = QListView() self.brushFlowTableView.setViewMode(QListView.IconMode) self.brushFlowTableView.setMovement(QListView.Static) self.brushFlowTableView.setResizeMode(QListView.Adjust) self.brushFlowTableView.setUniformItemSizes(True) self.brushFlowTableView.setSelectionMode(QListView.SingleSelection) tabWidget.addTab(self.brushSizeTableView, "Size") tabWidget.addTab(self.brushOpacityTableView, "Opacity") tabWidget.addTab(self.brushFlowTableView, "Flow") layout.addWidget(tabWidget) self.setWidget(widget) # Add the widget to the docker. # amount of columns in each row for all the tables. # We want a grid with possible options to select. # To do this, we'll make a ListView widget and use a standarditemmodel for the entries. # The entries are filled out based on the sizes and opacity lists. # Sizes and opacity lists. The former is half-way copied from ptsai, the latter is based on personal experience of useful opacities. self.sizesList = [0.7, 1.0, 1.5, 2, 2.5, 3, 3.5, 4, 5, 6, 7, 8, 9, 10, 12, 14, 16, 20, 25, 30, 35, 40, 50, 60, 70, 80, 100, 120, 160, 200, 250, 300, 350, 400, 450, 500] self.opacityList = [0.1, 0.5, 1, 5, 10, 15, 20, 30, 40, 50, 60, 70, 80, 90, 100] self.brushSizeModel = QStandardItemModel() self.brushOpacityModel = QStandardItemModel() self.brushFlowModel = QStandardItemModel() self.fillSizesModel() self.fillOpacityModel() # Now we're done filling out our tables, we connect the views to the functions that'll change the settings. self.brushSizeTableView.clicked.connect(self.setBrushSize) self.brushOpacityTableView.clicked.connect(self.setBrushOpacity) self.brushFlowTableView.clicked.connect(self.setBrushFlow) def fillSizesModel(self): # First we empty the old model. We might wanna use this function in the future to fill it with the brushmask of the selected brush, but there's currently no API for recognising changes in the current brush nor is there a way to get its brushmask. self.brushSizeModel.clear() for s in range(len(self.sizesList)): # we're gonna itterate over our list, and make a new item for each entry. # We need to disable a bunch of stuff to make sure people won't do funny things to our entries. item = QStandardItem() item.setCheckable(False) item.setEditable(False) item.setDragEnabled(False) item.setText(str(self.sizesList[s])+" px") # And from here on we'll make an icon. brushImage = QPixmap(64, 64) img = QImage(64, 64, QImage.Format_RGBA8888) circlePainter = QPainter() img.fill(Qt.transparent) circlePainter.begin(img) brush = QBrush(Qt.SolidPattern) brush.setColor(self.brushSizeTableView.palette().color(QPalette.Text)) circlePainter.setBrush(brush) circlePainter.setPen(QPen(QBrush(Qt.transparent), 0)) brushSize = max(min(self.sizesList[s], 64), 1) brushSize = brushSize * 0.5 circlePainter.drawEllipse(QPointF(32, 32), brushSize, brushSize) circlePainter.end() brushImage = QPixmap.fromImage(img) # now we're done with drawing the icon, so we set it on the item. item.setIcon(QIcon(brushImage)) self.brushSizeModel.appendRow(item) self.brushSizeTableView.setModel(self.brushSizeModel) def fillOpacityModel(self): self.brushOpacityModel.clear() self.brushFlowModel.clear() for s in range(len(self.opacityList)): # we're gonna itterate over our list, and make a new item for each entry. item = QStandardItem() item.setCheckable(False) item.setEditable(False) item.setDragEnabled(False) item.setText(str(self.opacityList[s])+" %") brushImage = QPixmap(64, 64) img = QImage(64, 64, QImage.Format_RGBA8888) circlePainter = QPainter() img.fill(Qt.transparent) circlePainter.begin(img) brush = QBrush(Qt.SolidPattern) brush.setColor(self.brushSizeTableView.palette().color(QPalette.Text)) circlePainter.setBrush(brush) circlePainter.setPen(QPen(QBrush(Qt.transparent), 0)) circlePainter.setOpacity(self.opacityList[s] / 100) circlePainter.drawEllipse(QPointF(32, 32), 32, 32) circlePainter.end() brushImage = QPixmap.fromImage(img) item.setIcon(QIcon(brushImage)) # the flow and opacity models will use virtually the same items, but Qt would like us to make sure we understand # these are not really the same items, so hence the clone. itemFlow = item.clone() self.brushOpacityModel.appendRow(item) self.brushFlowModel.appendRow(itemFlow) self.brushOpacityTableView.setModel(self.brushOpacityModel) self.brushFlowTableView.setModel(self.brushFlowModel) def canvasChanged(self, canvas): pass @pyqtSlot('QModelIndex') def setBrushSize(self, index): i = index.row() brushSize = self.sizesList[i] if Application.activeWindow() and len(Application.activeWindow().views()) > 0: Application.activeWindow().views()[0].setBrushSize(brushSize) @pyqtSlot('QModelIndex') def setBrushOpacity(self, index): i = index.row() brushOpacity = self.opacityList[i] / 100 if Application.activeWindow() and len(Application.activeWindow().views()) > 0: Application.activeWindow().views()[0].setPaintingOpacity(brushOpacity) @pyqtSlot('QModelIndex') def setBrushFlow(self, index): i = index.row() brushOpacity = self.opacityList[i] / 100 if Application.activeWindow() and len(Application.activeWindow().views()) > 0: Application.activeWindow().views()[0].setPaintingFlow(brushOpacity) # Add docker to the application :) Application.addDockWidgetFactory(DockWidgetFactory("quick_settings_docker", DockWidgetFactoryBase.DockRight, QuickSettingsDocker)) diff --git a/plugins/tools/svgtexttool/SvgTextEditor.cpp b/plugins/tools/svgtexttool/SvgTextEditor.cpp index beebb93278..5ec4c0e3e2 100644 --- a/plugins/tools/svgtexttool/SvgTextEditor.cpp +++ b/plugins/tools/svgtexttool/SvgTextEditor.cpp @@ -1,1074 +1,1074 @@ /* This file is part of the KDE project * * Copyright 2017 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 "SvgTextEditor.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 #include #include #include "kis_font_family_combo_box.h" SvgTextEditor::SvgTextEditor(QWidget *parent, Qt::WindowFlags flags) : KXmlGuiWindow(parent, flags) , m_page(new QWidget(this)) , m_charSelectDialog(new KoDialog(this)) { m_textEditorWidget.setupUi(m_page); setCentralWidget(m_page); m_textEditorWidget.chkVertical->setVisible(false); KCharSelect *charSelector = new KCharSelect(m_charSelectDialog, 0, KCharSelect::AllGuiElements); m_charSelectDialog->setMainWidget(charSelector); connect(charSelector, SIGNAL(currentCharChanged(QChar)), SLOT(insertCharacter(QChar))); m_charSelectDialog->hide(); m_charSelectDialog->setButtons(KoDialog::Close); connect(m_textEditorWidget.buttons, SIGNAL(accepted()), this, SLOT(save())); connect(m_textEditorWidget.buttons, SIGNAL(rejected()), this, SLOT(close())); connect(m_textEditorWidget.buttons, SIGNAL(clicked(QAbstractButton*)), this, SLOT(dialogButtonClicked(QAbstractButton*))); KConfigGroup cg(KSharedConfig::openConfig(), "SvgTextTool"); actionCollection()->setConfigGroup("SvgTextTool"); actionCollection()->setComponentName("svgtexttool"); actionCollection()->setComponentDisplayName(i18n("Text Tool")); QByteArray state; if (cg.hasKey("WindowState")) { state = cg.readEntry("State", state); state = QByteArray::fromBase64(state); // One day will need to load the version number, but for now, assume 0 restoreState(state); } setAcceptDrops(true); //setStandardToolBarMenuEnabled(true); #ifdef Q_OS_OSX setUnifiedTitleAndToolBarOnMac(true); #endif setTabPosition(Qt::AllDockWidgetAreas, QTabWidget::North); m_syntaxHighlighter = new BasicXMLSyntaxHighlighter(m_textEditorWidget.svgTextEdit); m_textEditorWidget.svgTextEdit->setFont(QFontDatabase().systemFont(QFontDatabase::FixedFont)); createActions(); // If we have customized the toolbars, load that first setLocalXMLFile(KoResourcePaths::locateLocal("data", "svgtexttool.xmlgui")); setXMLFile(":/kxmlgui5/svgtexttool.xmlgui"); guiFactory()->addClient(this); // Create and plug toolbar list for Settings menu QList toolbarList; Q_FOREACH (QWidget* it, guiFactory()->containers("ToolBar")) { KToolBar * toolBar = ::qobject_cast(it); if (toolBar) { toolBar->setToolButtonStyle(Qt::ToolButtonIconOnly); KToggleAction* act = new KToggleAction(i18n("Show %1 Toolbar", toolBar->windowTitle()), this); actionCollection()->addAction(toolBar->objectName().toUtf8(), act); act->setCheckedState(KGuiItem(i18n("Hide %1 Toolbar", toolBar->windowTitle()))); connect(act, SIGNAL(toggled(bool)), this, SLOT(slotToolbarToggled(bool))); act->setChecked(!toolBar->isHidden()); toolbarList.append(act); } } plugActionList("toolbarlist", toolbarList); connect(m_textEditorWidget.textTab, SIGNAL(currentChanged(int)), this, SLOT(switchTextEditorTab())); switchTextEditorTab(); m_textEditorWidget.richTextEdit->document()->setDefaultStyleSheet("p {margin:0px;}"); applySettings(); } SvgTextEditor::~SvgTextEditor() { KConfigGroup g(KSharedConfig::openConfig(), "SvgTextTool"); QByteArray ba = saveState(); g.writeEntry("windowState", ba.toBase64()); } void SvgTextEditor::setShape(KoSvgTextShape *shape) { m_shape = shape; if (m_shape) { KoSvgTextShapeMarkupConverter converter(m_shape); QString svg; QString styles; QTextDocument *doc = m_textEditorWidget.richTextEdit->document(); if (converter.convertToSvg(&svg, &styles)) { m_textEditorWidget.svgTextEdit->setPlainText(svg); m_textEditorWidget.svgStylesEdit->setPlainText(styles); m_textEditorWidget.svgTextEdit->document()->setModified(false); if (converter.convertSvgToDocument(svg, doc)) { m_textEditorWidget.richTextEdit->setDocument(doc); } } else { QMessageBox::warning(this, i18n("Conversion failed"), "Could not get svg text from the shape:\n" + converter.errors().join('\n') + "\n" + converter.warnings().join('\n')); } } } void SvgTextEditor::save() { if (m_shape) { if (m_textEditorWidget.textTab->currentIndex() == Richtext) { QString svg; QString styles = m_textEditorWidget.svgStylesEdit->document()->toPlainText(); KoSvgTextShapeMarkupConverter converter(m_shape); if (!converter.convertDocumentToSvg(m_textEditorWidget.richTextEdit->document(), &svg)) { qWarning()<<"new converter doesn't work!"; } m_textEditorWidget.richTextEdit->document()->setModified(false); emit textUpdated(svg, styles); } else { emit textUpdated(m_textEditorWidget.svgTextEdit->document()->toPlainText(), m_textEditorWidget.svgStylesEdit->document()->toPlainText()); m_textEditorWidget.svgTextEdit->document()->setModified(false); } } } void SvgTextEditor::switchTextEditorTab() { KoSvgTextShape shape; KoSvgTextShapeMarkupConverter converter(&shape); if (m_currentEditor) { disconnect(m_currentEditor->document(), SIGNAL(modificationChanged(bool)), this, SLOT(setModified(bool))); } if (m_textEditorWidget.textTab->currentIndex() == Richtext) { //first, make buttons checkable enableRichTextActions(true); //then connect the cursor change to the checkformat(); connect(m_textEditorWidget.richTextEdit, SIGNAL(cursorPositionChanged()), this, SLOT(checkFormat())); if (m_shape) { // Convert the svg text to html XXX: Fix resolution! Also, the rect should be the image rect, not the shape rect. /** if (!converter.convertFromSvg(m_textEditorWidget.svgTextEdit->document()->toPlainText(), m_textEditorWidget.svgStylesEdit->document()->toPlainText(), m_shape->boundingRect(), 72.0)) { qDebug() << "Eeek 3"; } QString html; if (!converter.convertToHtml(&html)) { qDebug() << "Eeek 4"; } m_textEditorWidget.richTextEdit->document()->setHtml(html); */ QTextDocument *doc = m_textEditorWidget.richTextEdit->document(); if (!converter.convertSvgToDocument(m_textEditorWidget.svgTextEdit->document()->toPlainText(), doc)) { qWarning()<<"new converter svgToDoc doesn't work!"; } m_textEditorWidget.richTextEdit->setDocument(doc); } m_currentEditor = m_textEditorWidget.richTextEdit; } else { //first, make buttons uncheckable enableRichTextActions(false); disconnect(m_textEditorWidget.richTextEdit, SIGNAL(cursorPositionChanged()), this, SLOT(checkFormat())); // Convert the rich text to svg and styles strings if (m_shape) { QString svg; QString styles; if (!converter.convertDocumentToSvg(m_textEditorWidget.richTextEdit->document(), &svg)) { qWarning()<<"new converter docToSVG doesn't work!"; } m_textEditorWidget.svgTextEdit->setPlainText(svg); } m_currentEditor = m_textEditorWidget.svgTextEdit; } connect(m_currentEditor->document(), SIGNAL(modificationChanged(bool)), SLOT(setModified(bool))); } void SvgTextEditor::checkFormat() { QTextCharFormat format = m_textEditorWidget.richTextEdit->textCursor().charFormat(); QTextBlockFormat blockFormat = m_textEditorWidget.richTextEdit->textCursor().blockFormat(); if (format.fontWeight() > QFont::Normal) { actionCollection()->action("svg_weight_bold")->setChecked(true); } else { actionCollection()->action("svg_weight_bold")->setChecked(false); } actionCollection()->action("svg_format_italic")->setChecked(format.fontItalic()); actionCollection()->action("svg_format_underline")->setChecked(format.fontUnderline()); actionCollection()->action("svg_format_strike_through")->setChecked(format.fontStrikeOut()); qobject_cast(qobject_cast(actionCollection()->action("svg_font"))->defaultWidget())->setCurrentFont(format.font()); QComboBox *fontSizeCombo = qobject_cast(qobject_cast(actionCollection()->action("svg_font_size"))->defaultWidget()); fontSizeCombo->setCurrentIndex(QFontDatabase::standardSizes().indexOf(format.font().pointSize())); KoColor fg(format.foreground().color(), KoColorSpaceRegistry::instance()->rgb8()); qobject_cast(actionCollection()->action("svg_format_textcolor"))->setCurrentColor(fg); KoColor bg(format.foreground().color(), KoColorSpaceRegistry::instance()->rgb8()); qobject_cast(actionCollection()->action("svg_background_color"))->setCurrentColor(bg); QDoubleSpinBox *spnLineHeight = qobject_cast(qobject_cast(actionCollection()->action("svg_line_height"))->defaultWidget()); if (blockFormat.lineHeightType()==QTextBlockFormat::SingleHeight) { spnLineHeight->setValue(100.0); } else if(blockFormat.lineHeightType()==QTextBlockFormat::ProportionalHeight) { spnLineHeight->setValue(double(blockFormat.lineHeight())); } } void SvgTextEditor::undo() { m_currentEditor->undo(); } void SvgTextEditor::redo() { m_currentEditor->redo(); } void SvgTextEditor::cut() { m_currentEditor->cut(); } void SvgTextEditor::copy() { m_currentEditor->copy(); } void SvgTextEditor::paste() { m_currentEditor->paste(); } void SvgTextEditor::selectAll() { m_currentEditor->selectAll(); } void SvgTextEditor::deselect() { QTextCursor cursor(m_currentEditor->textCursor()); cursor.clearSelection(); m_currentEditor->setTextCursor(cursor); } void SvgTextEditor::find() { QDialog *findDialog = new QDialog(this); findDialog->setWindowTitle(i18n("Find Text")); QFormLayout *layout = new QFormLayout(); findDialog->setLayout(layout); QLineEdit *lnSearchKey = new QLineEdit(); layout->addRow(i18n("Find:"), lnSearchKey); QDialogButtonBox *buttons = new QDialogButtonBox(QDialogButtonBox::Ok|QDialogButtonBox::Cancel); findDialog->layout()->addWidget(buttons); connect(buttons, SIGNAL(accepted()), findDialog, SLOT(accept())); connect(buttons, SIGNAL(rejected()), findDialog, SLOT(reject())); if (findDialog->exec()==QDialog::Accepted) { m_searchKey = lnSearchKey->text(); m_currentEditor->find(m_searchKey); } } void SvgTextEditor::findNext() { if (!m_currentEditor->find(m_searchKey)) { QTextCursor cursor(m_currentEditor->textCursor()); cursor.movePosition(QTextCursor::Start); m_currentEditor->setTextCursor(cursor); m_currentEditor->find(m_searchKey); } } void SvgTextEditor::findPrev() { if (!m_currentEditor->find(m_searchKey,QTextDocument::FindBackward)) { QTextCursor cursor(m_currentEditor->textCursor()); cursor.movePosition(QTextCursor::End); m_currentEditor->setTextCursor(cursor); m_currentEditor->find(m_searchKey,QTextDocument::FindBackward); } } void SvgTextEditor::replace() { QDialog *findDialog = new QDialog(this); findDialog->setWindowTitle(i18n("Find and Replace all")); QFormLayout *layout = new QFormLayout(); findDialog->setLayout(layout); QLineEdit *lnSearchKey = new QLineEdit(); QLineEdit *lnReplaceKey = new QLineEdit(); layout->addRow(i18n("Find:"), lnSearchKey); QDialogButtonBox *buttons = new QDialogButtonBox(QDialogButtonBox::Ok|QDialogButtonBox::Cancel); layout->addRow(i18n("Replace:"), lnReplaceKey); findDialog->layout()->addWidget(buttons); connect(buttons, SIGNAL(accepted()), findDialog, SLOT(accept())); connect(buttons, SIGNAL(rejected()), findDialog, SLOT(reject())); if (findDialog->exec()==QDialog::Accepted) { QString search = lnSearchKey->text(); QString replace = lnReplaceKey->text(); QTextCursor cursor(m_currentEditor->textCursor()); cursor.movePosition(QTextCursor::Start); m_currentEditor->setTextCursor(cursor); while(m_currentEditor->find(search)) { m_currentEditor->textCursor().removeSelectedText(); m_currentEditor->textCursor().insertText(replace); } } } void SvgTextEditor::zoomOut() { m_currentEditor->zoomOut(); } void SvgTextEditor::zoomIn() { m_currentEditor->zoomIn(); } void SvgTextEditor::showInsertSpecialCharacterDialog() { m_charSelectDialog->setVisible(!m_charSelectDialog->isVisible()); } void SvgTextEditor::insertCharacter(const QChar &c) { m_currentEditor->textCursor().insertText(QString(c)); } void SvgTextEditor::setTextBold(QFont::Weight weight) { if (m_textEditorWidget.textTab->currentIndex() == Richtext) { QTextCharFormat format; if (m_textEditorWidget.richTextEdit->textCursor().charFormat().fontWeight() > QFont::Normal && weight==QFont::Bold) { format.setFontWeight(QFont::Normal); } else { format.setFontWeight(weight); } m_textEditorWidget.richTextEdit->mergeCurrentCharFormat(format); } else { QTextCursor cursor = m_textEditorWidget.svgTextEdit->textCursor(); if (cursor.hasSelection()) { QString selectionModified = "" + cursor.selectedText() + ""; cursor.removeSelectedText(); cursor.insertText(selectionModified); } } } void SvgTextEditor::setTextWeightLight() { if (m_textEditorWidget.richTextEdit->textCursor().charFormat().fontWeight() < QFont::Normal) { setTextBold(QFont::Normal); } else { setTextBold(QFont::Light); } } void SvgTextEditor::setTextWeightNormal() { setTextBold(QFont::Normal); } void SvgTextEditor::setTextWeightDemi() { if (m_textEditorWidget.richTextEdit->textCursor().charFormat().fontWeight()>QFont::Normal && m_textEditorWidget.richTextEdit->textCursor().charFormat().fontWeight()textCursor().charFormat().fontWeight()>QFont::Normal) { setTextBold(QFont::Normal); } else { setTextBold(QFont::Black); } } void SvgTextEditor::setTextItalic(QFont::Style style) { QTextCursor cursor = m_textEditorWidget.svgTextEdit->textCursor(); QString fontStyle = "inherit"; if (style == QFont::StyleItalic) { fontStyle = "italic"; } else if(style == QFont::StyleOblique) { fontStyle = "oblique"; } if (m_textEditorWidget.textTab->currentIndex() == Richtext) { QTextCharFormat format; format.setFontItalic(!m_textEditorWidget.richTextEdit->textCursor().charFormat().fontItalic()); m_textEditorWidget.richTextEdit->mergeCurrentCharFormat(format); } else { if (cursor.hasSelection()) { QString selectionModified = "" + cursor.selectedText() + ""; cursor.removeSelectedText(); cursor.insertText(selectionModified); } } } void SvgTextEditor::setTextDecoration(KoSvgText::TextDecoration decor) { QTextCursor cursor = m_textEditorWidget.svgTextEdit->textCursor(); QTextCharFormat currentFormat = m_textEditorWidget.richTextEdit->textCursor().charFormat(); QTextCharFormat format; QString textDecoration = "inherit"; if (decor == KoSvgText::DecorationUnderline) { textDecoration = "underline"; if (currentFormat.fontUnderline()) { format.setFontUnderline(false); } else { format.setFontUnderline(true); } format.setFontOverline(false); format.setFontStrikeOut(false); } else if (decor == KoSvgText::DecorationLineThrough) { textDecoration = "line-through"; format.setFontUnderline(false); format.setFontOverline(false); if (currentFormat.fontStrikeOut()) { format.setFontStrikeOut(false); } else { format.setFontStrikeOut(true); } } else if (decor == KoSvgText::DecorationOverline) { textDecoration = "overline"; format.setFontUnderline(false); if (currentFormat.fontOverline()) { format.setFontOverline(false); } else { format.setFontOverline(true); } format.setFontStrikeOut(false); } if (m_textEditorWidget.textTab->currentIndex() == Richtext) { m_textEditorWidget.richTextEdit->mergeCurrentCharFormat(format); } else { if (cursor.hasSelection()) { QString selectionModified = "" + cursor.selectedText() + ""; cursor.removeSelectedText(); cursor.insertText(selectionModified); } } } void SvgTextEditor::setTextUnderline() { setTextDecoration(KoSvgText::DecorationUnderline); } void SvgTextEditor::setTextOverline() { setTextDecoration(KoSvgText::DecorationOverline); } void SvgTextEditor::setTextStrikethrough() { setTextDecoration(KoSvgText::DecorationLineThrough); } void SvgTextEditor::setTextSubscript() { QTextCharFormat format = m_textEditorWidget.richTextEdit->textCursor().charFormat(); if (format.verticalAlignment()==QTextCharFormat::AlignSubScript) { format.setVerticalAlignment(QTextCharFormat::AlignNormal); } else { format.setVerticalAlignment(QTextCharFormat::AlignSubScript); } m_textEditorWidget.richTextEdit->mergeCurrentCharFormat(format); } void SvgTextEditor::setTextSuperScript() { QTextCharFormat format = m_textEditorWidget.richTextEdit->textCursor().charFormat(); if (format.verticalAlignment()==QTextCharFormat::AlignSuperScript) { format.setVerticalAlignment(QTextCharFormat::AlignNormal); } else { format.setVerticalAlignment(QTextCharFormat::AlignSuperScript); } m_textEditorWidget.richTextEdit->mergeCurrentCharFormat(format); } void SvgTextEditor::increaseTextSize() { QTextCharFormat format; int pointSize = m_textEditorWidget.richTextEdit->textCursor().charFormat().font().pointSize(); if (pointSize<0) { pointSize = m_textEditorWidget.richTextEdit->textCursor().charFormat().font().pixelSize(); } qDebug()<mergeCurrentCharFormat(format); } void SvgTextEditor::decreaseTextSize() { QTextCharFormat format; int pointSize = m_textEditorWidget.richTextEdit->textCursor().charFormat().font().pointSize(); if (pointSize<1) { pointSize = m_textEditorWidget.richTextEdit->textCursor().charFormat().font().pixelSize(); } format.setFontPointSize(qMax(pointSize-1.0, 1.0)); m_textEditorWidget.richTextEdit->mergeCurrentCharFormat(format); } void SvgTextEditor::setLineHeight(double lineHeightPercentage) { QTextBlockFormat format = m_textEditorWidget.richTextEdit->textCursor().blockFormat(); format.setLineHeight(lineHeightPercentage, QTextBlockFormat::ProportionalHeight); m_textEditorWidget.richTextEdit->textCursor().mergeBlockFormat(format); } void SvgTextEditor::alignLeft() { QTextBlockFormat format = m_textEditorWidget.richTextEdit->textCursor().blockFormat(); format.setAlignment(Qt::AlignLeft); m_textEditorWidget.richTextEdit->textCursor().mergeBlockFormat(format); } void SvgTextEditor::alignRight() { QTextBlockFormat format = m_textEditorWidget.richTextEdit->textCursor().blockFormat(); format.setAlignment(Qt::AlignRight); m_textEditorWidget.richTextEdit->textCursor().mergeBlockFormat(format); } void SvgTextEditor::alignCenter() { QTextBlockFormat format = m_textEditorWidget.richTextEdit->textCursor().blockFormat(); format.setAlignment(Qt::AlignCenter); m_textEditorWidget.richTextEdit->textCursor().mergeBlockFormat(format); } void SvgTextEditor::alignJustified() { QTextBlockFormat format = m_textEditorWidget.richTextEdit->textCursor().blockFormat(); format.setAlignment(Qt::AlignJustify); m_textEditorWidget.richTextEdit->textCursor().mergeBlockFormat(format); } void SvgTextEditor::setSettings() { - KoDialog settingsDialog; + KoDialog settingsDialog(this); Ui_WdgSvgTextSettings textSettings; QWidget *settingsPage = new QWidget(&settingsDialog, 0); settingsDialog.setMainWidget(settingsPage); textSettings.setupUi(settingsPage); // get the settings and initialize the dialog KConfigGroup cfg(KSharedConfig::openConfig(), "SvgTextTool"); QStringList selectedWritingSystems = cfg.readEntry("selectedWritingSystems", "").split(","); QList scripts = QFontDatabase().writingSystems(); QStandardItemModel *writingSystemsModel = new QStandardItemModel(&settingsDialog); for (int s = 0; s < scripts.size(); s ++) { QString writingSystem = QFontDatabase().writingSystemName(scripts.at(s)); QStandardItem *script = new QStandardItem(writingSystem); script->setCheckable(true); script->setCheckState(selectedWritingSystems.contains(QString::number(scripts.at(s))) ? Qt::Checked : Qt::Unchecked); script->setData((int)scripts.at(s)); writingSystemsModel->appendRow(script); } textSettings.lwScripts->setModel(writingSystemsModel); EditorMode mode = (EditorMode)cfg.readEntry("EditorMode", (int)Both); switch(mode) { case(RichText): textSettings.radioRichText->setChecked(true); break; case(SvgSource): textSettings.radioSvgSource->setChecked(true); break; case(Both): textSettings.radioBoth->setChecked(true); } QColor background = cfg.readEntry("colorEditorBackground", qApp->palette().background().color()); textSettings.colorEditorBackground->setColor(background); textSettings.colorEditorForeground->setColor(cfg.readEntry("colorEditorForeground", qApp->palette().text().color())); textSettings.colorKeyword->setColor(cfg.readEntry("colorKeyword", QColor(background.value() < 100 ? Qt::cyan : Qt::blue))); textSettings.chkBoldKeyword->setChecked(cfg.readEntry("BoldKeyword", true)); textSettings.chkItalicKeyword->setChecked(cfg.readEntry("ItalicKeyword", false)); textSettings.colorElement->setColor(cfg.readEntry("colorElement", QColor(background.value() < 100 ? Qt::magenta : Qt::darkMagenta))); textSettings.chkBoldElement->setChecked(cfg.readEntry("BoldElement", true)); textSettings.chkItalicElement->setChecked(cfg.readEntry("ItalicElement", false)); textSettings.colorAttribute->setColor(cfg.readEntry("colorAttribute", QColor(background.value() < 100 ? Qt::green : Qt::darkGreen))); textSettings.chkBoldAttribute->setChecked(cfg.readEntry("BoldAttribute", true)); textSettings.chkItalicAttribute->setChecked(cfg.readEntry("ItalicAttribute", true)); textSettings.colorValue->setColor(cfg.readEntry("colorValue", QColor(background.value() < 100 ? Qt::red: Qt::darkRed))); textSettings.chkBoldValue->setChecked(cfg.readEntry("BoldValue", true)); textSettings.chkItalicValue->setChecked(cfg.readEntry("ItalicValue", false)); textSettings.colorComment->setColor(cfg.readEntry("colorComment", QColor(background.value() < 100 ? Qt::lightGray : Qt::gray))); textSettings.chkBoldComment->setChecked(cfg.readEntry("BoldComment", false)); textSettings.chkItalicComment->setChecked(cfg.readEntry("ItalicComment", false)); settingsDialog.setButtons(KoDialog::Ok | KoDialog::Cancel); if (settingsDialog.exec() == QDialog::Accepted) { qDebug() << "saving settings"; // save and set the settings QStringList writingSystems; for (int i = 0; i < writingSystemsModel->rowCount(); i++) { QStandardItem *item = writingSystemsModel->item(i); if (item->checkState() == Qt::Checked) { writingSystems.append(QString::number(item->data().toInt())); } } if (!writingSystems.isEmpty()) { cfg.writeEntry("selectedWritingSystems", writingSystems.join(',')); } if (textSettings.radioRichText->isChecked()) { cfg.writeEntry("EditorMode", (int)Richtext); } else if (textSettings.radioSvgSource->isChecked()) { cfg.writeEntry("EditorMode", (int)SvgSource); } else if (textSettings.radioBoth->isChecked()) { cfg.writeEntry("EditorMode", (int)Both); } cfg.writeEntry("colorEditorBackground", textSettings.colorEditorBackground->color()); cfg.writeEntry("colorEditorForeground", textSettings.colorEditorForeground->color()); cfg.writeEntry("colorKeyword", textSettings.colorKeyword->color()); cfg.writeEntry("BoldKeyword", textSettings.chkBoldKeyword->isChecked()); cfg.writeEntry("ItalicKeyWord", textSettings.chkItalicKeyword->isChecked()); cfg.writeEntry("colorElement", textSettings.colorElement->color()); cfg.writeEntry("BoldElement", textSettings.chkBoldElement->isChecked()); cfg.writeEntry("ItalicElement", textSettings.chkItalicElement->isChecked()); cfg.writeEntry("colorAttribute", textSettings.colorAttribute->color()); cfg.writeEntry("BoldAttribute", textSettings.chkBoldAttribute->isChecked()); cfg.writeEntry("ItalicAttribute", textSettings.chkItalicAttribute->isChecked()); cfg.writeEntry("colorValue", textSettings.colorValue->color()); cfg.writeEntry("BoldValue", textSettings.chkBoldValue->isChecked()); cfg.writeEntry("ItalicValue", textSettings.chkItalicValue->isChecked()); cfg.writeEntry("colorComment", textSettings.colorComment->color()); cfg.writeEntry("BoldComment", textSettings.chkBoldComment->isChecked()); cfg.writeEntry("ItalicComment", textSettings.chkItalicComment->isChecked()); applySettings(); } } void SvgTextEditor::slotToolbarToggled(bool) { } void SvgTextEditor::setFontColor(const KoColor &c) { QColor color = c.toQColor(); if (m_textEditorWidget.textTab->currentIndex() == Richtext) { QTextCharFormat format; format.setForeground(QBrush(color)); m_textEditorWidget.richTextEdit->mergeCurrentCharFormat(format); } else { QTextCursor cursor = m_textEditorWidget.svgTextEdit->textCursor(); if (cursor.hasSelection()) { QString selectionModified = "" + cursor.selectedText() + ""; cursor.removeSelectedText(); cursor.insertText(selectionModified); } } } void SvgTextEditor::setBackgroundColor(const KoColor &c) { QColor color = c.toQColor(); QTextCursor cursor = m_textEditorWidget.svgTextEdit->textCursor(); if (cursor.hasSelection()) { QString selectionModified = "" + cursor.selectedText() + ""; cursor.removeSelectedText(); cursor.insertText(selectionModified); } } void SvgTextEditor::setModified(bool modified) { if (modified) { m_textEditorWidget.buttons->setStandardButtons(QDialogButtonBox::Save | QDialogButtonBox::Discard); } else { m_textEditorWidget.buttons->setStandardButtons(QDialogButtonBox::Save | QDialogButtonBox::Close); } } void SvgTextEditor::dialogButtonClicked(QAbstractButton *button) { if (m_textEditorWidget.buttons->standardButton(button) == QDialogButtonBox::Discard) { if (QMessageBox::warning(this, i18nc("@title:window", "Krita"), i18n("You have modified the text. Discard changes?"), QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { close(); } } } void SvgTextEditor::setFont(const QString &fontName) { QFont font; font.fromString(fontName); QTextCharFormat curFormat = m_textEditorWidget.richTextEdit->textCursor().charFormat(); font.setPointSize(curFormat.font().pointSize()); QTextCharFormat format; //This disables the style being set from the font-comboboxes too, so we need to rethink how we use that. format.setFontFamily(font.family()); if (m_textEditorWidget.textTab->currentIndex() == Richtext) { m_textEditorWidget.richTextEdit->mergeCurrentCharFormat(format); } else { QTextCursor cursor = m_textEditorWidget.svgTextEdit->textCursor(); if (cursor.hasSelection()) { QString selectionModified = "" + cursor.selectedText() + ""; cursor.removeSelectedText(); cursor.insertText(selectionModified); } } } void SvgTextEditor::setFontSize(const QString &fontSize) { if (m_textEditorWidget.textTab->currentIndex() == Richtext) { QTextCharFormat format; format.setFontPointSize((qreal)fontSize.toInt()); m_textEditorWidget.richTextEdit->mergeCurrentCharFormat(format); } else { QTextCursor cursor = m_textEditorWidget.svgTextEdit->textCursor(); if (cursor.hasSelection()) { QString selectionModified = "" + cursor.selectedText() + ""; cursor.removeSelectedText(); cursor.insertText(selectionModified); } } } void SvgTextEditor::setBaseline(KoSvgText::BaselineShiftMode) { QTextCursor cursor = m_textEditorWidget.svgTextEdit->textCursor(); if (cursor.hasSelection()) { QString selectionModified = "" + cursor.selectedText() + ""; cursor.removeSelectedText(); cursor.insertText(selectionModified); } } void SvgTextEditor::wheelEvent(QWheelEvent *event) { if (event->modifiers() & Qt::ControlModifier) { int numDegrees = event->delta() / 8; int numSteps = numDegrees / 7; m_textEditorWidget.svgTextEdit->zoomOut(numSteps); event->accept(); } } void SvgTextEditor::applySettings() { KConfigGroup cfg(KSharedConfig::openConfig(), "SvgTextTool"); - EditorMode mode = (EditorMode)cfg.readEntry("EditorMode", (int)RichText); + EditorMode mode = (EditorMode)cfg.readEntry("EditorMode", (int)Both); QWidget *richTab = m_textEditorWidget.richTab; QWidget *svgTab = m_textEditorWidget.svgTab; m_page->setUpdatesEnabled(false); m_textEditorWidget.textTab->clear(); switch(mode) { case(RichText): m_textEditorWidget.textTab->addTab(richTab, i18n("Rich text")); break; case(SvgSource): m_textEditorWidget.textTab->addTab(svgTab, i18n("SVG Source")); break; case(Both): m_textEditorWidget.textTab->addTab(richTab, i18n("Rich text")); m_textEditorWidget.textTab->addTab(svgTab, i18n("SVG Source")); } m_syntaxHighlighter->setFormats(); QPalette palette = m_textEditorWidget.svgTextEdit->palette(); QColor background = cfg.readEntry("colorEditorBackground", qApp->palette().background().color()); palette.setBrush(QPalette::Active, QPalette::Background, QBrush(background)); QColor foreground = cfg.readEntry("colorEditorForeground", qApp->palette().text().color()); palette.setBrush(QPalette::Active, QPalette::Text, QBrush(foreground)); QStringList selectedWritingSystems = cfg.readEntry("selectedWritingSystems", "").split(","); QVector writingSystems; for (int i=0; i(qobject_cast(actionCollection()->action("svg_font"))->defaultWidget())->refillComboBox(writingSystems); m_page->setUpdatesEnabled(true); } QAction *SvgTextEditor::createAction(const QString &name, const char *member) { QAction *action = new QAction(this); KisActionRegistry *actionRegistry = KisActionRegistry::instance(); actionRegistry->propertizeAction(name, action); actionCollection()->addAction(name, action); QObject::connect(action, SIGNAL(triggered(bool)), this, member); return action; } void SvgTextEditor::createActions() { KisActionRegistry *actionRegistry = KisActionRegistry::instance(); // File: new, open, save, save as, close KStandardAction::save(this, SLOT(save()), actionCollection()); KStandardAction::close(this, SLOT(close()), actionCollection()); // Edit KStandardAction::undo(this, SLOT(undo()), actionCollection()); KStandardAction::redo(this, SLOT(redo()), actionCollection()); KStandardAction::cut(this, SLOT(cut()), actionCollection()); KStandardAction::copy(this, SLOT(copy()), actionCollection()); KStandardAction::paste(this, SLOT(paste()), actionCollection()); KStandardAction::selectAll(this, SLOT(selectAll()), actionCollection()); KStandardAction::deselect(this, SLOT(deselect()), actionCollection()); KStandardAction::find(this, SLOT(find()), actionCollection()); KStandardAction::findNext(this, SLOT(findNext()), actionCollection()); KStandardAction::findPrev(this, SLOT(findPrev()), actionCollection()); KStandardAction::replace(this, SLOT(replace()), actionCollection()); // View KStandardAction::zoomOut(this, SLOT(zoomOut()), actionCollection()); KStandardAction::zoomIn(this, SLOT(zoomIn()), actionCollection()); // Insert: QAction * insertAction = createAction("svg_insert_special_character", SLOT(showInsertSpecialCharacterDialog())); insertAction->setCheckable(true); insertAction->setChecked(false); // Format: m_richTextActions << createAction("svg_weight_bold", SLOT(setTextBold())); m_richTextActions << createAction("svg_format_italic", SLOT(setTextItalic())); m_richTextActions << createAction("svg_format_underline", SLOT(setTextUnderline())); m_richTextActions << createAction("svg_format_strike_through", SLOT(setTextStrikethrough())); m_richTextActions << createAction("svg_format_superscript", SLOT(setTextSuperScript())); m_richTextActions << createAction("svg_format_subscript", SLOT(setTextSubscript())); m_richTextActions << createAction("svg_weight_light", SLOT(setTextWeightLight())); m_richTextActions << createAction("svg_weight_normal", SLOT(setTextWeightNormal())); m_richTextActions << createAction("svg_weight_demi", SLOT(setTextWeightDemi())); m_richTextActions << createAction("svg_weight_black", SLOT(setTextWeightBlack())); m_richTextActions << createAction("svg_increase_font_size", SLOT(increaseTextSize())); m_richTextActions << createAction("svg_decrease_font_size", SLOT(decreaseTextSize())); m_richTextActions << createAction("svg_align_left", SLOT(alignLeft())); m_richTextActions << createAction("svg_align_right", SLOT(alignRight())); m_richTextActions << createAction("svg_align_center", SLOT(alignCenter())); // m_richTextActions << createAction("svg_align_justified", // SLOT(alignJustified())); // Settings m_richTextActions << createAction("svg_settings", SLOT(setSettings())); QWidgetAction *fontComboAction = new QWidgetAction(this); fontComboAction->setToolTip(i18n("Font")); KisFontComboBoxes *fontCombo = new KisFontComboBoxes(); connect(fontCombo, SIGNAL(fontChanged(QString)), SLOT(setFont(QString))); fontComboAction->setDefaultWidget(fontCombo); actionCollection()->addAction("svg_font", fontComboAction); m_richTextActions << fontComboAction; actionRegistry->propertizeAction("svg_font", fontComboAction); QWidgetAction *fontSizeAction = new QWidgetAction(this); fontSizeAction->setToolTip(i18n("Size")); QComboBox *fontSizeCombo = new QComboBox(); Q_FOREACH (int size, QFontDatabase::standardSizes()) { fontSizeCombo->addItem(QString::number(size)); } fontSizeCombo->setCurrentIndex(QFontDatabase::standardSizes().indexOf(QApplication::font().pointSize())); connect(fontSizeCombo, SIGNAL(activated(QString)), SLOT(setFontSize(QString))); fontSizeAction->setDefaultWidget(fontSizeCombo); actionCollection()->addAction("svg_font_size", fontSizeAction); m_richTextActions << fontSizeAction; actionRegistry->propertizeAction("svg_font_size", fontSizeAction); KoColorPopupAction *fgColor = new KoColorPopupAction(this); fgColor->setCurrentColor(QColor(Qt::black)); fgColor->setToolTip(i18n("Text Color")); connect(fgColor, SIGNAL(colorChanged(KoColor)), SLOT(setFontColor(KoColor))); actionCollection()->addAction("svg_format_textcolor", fgColor); m_richTextActions << fgColor; actionRegistry->propertizeAction("svg_format_textcolor", fgColor); KoColorPopupAction *bgColor = new KoColorPopupAction(this); bgColor->setCurrentColor(QColor(Qt::white)); bgColor->setToolTip(i18n("Background Color")); connect(bgColor, SIGNAL(colorChanged(KoColor)), SLOT(setBackgroundColor(KoColor))); actionCollection()->addAction("svg_background_color", bgColor); actionRegistry->propertizeAction("svg_background_color", bgColor); m_richTextActions << bgColor; QWidgetAction *lineHeight = new QWidgetAction(this); lineHeight->setToolTip(i18n("Line height")); QDoubleSpinBox *spnLineHeight = new QDoubleSpinBox(); spnLineHeight->setRange(0.0, 1000.0); spnLineHeight->setSingleStep(10.0); spnLineHeight->setSuffix("%"); connect(spnLineHeight, SIGNAL(valueChanged(double)), SLOT(setLineHeight(double))); lineHeight->setDefaultWidget(spnLineHeight); actionCollection()->addAction("svg_line_height", lineHeight); m_richTextActions << lineHeight; actionRegistry->propertizeAction("svg_line_height", lineHeight); } void SvgTextEditor::enableRichTextActions(bool enable) { Q_FOREACH(QAction *action, m_richTextActions) { action->setEnabled(enable); } }