diff --git a/libs/image/brushengine/kis_paintop_config_widget.cpp b/libs/image/brushengine/kis_paintop_config_widget.cpp index c220372b70..ad8fe6411c 100644 --- a/libs/image/brushengine/kis_paintop_config_widget.cpp +++ b/libs/image/brushengine/kis_paintop_config_widget.cpp @@ -1,66 +1,66 @@ /* * Copyright (c) 2008 Boudewijn Rempt * 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_paintop_config_widget.h" #include KisPaintOpConfigWidget::KisPaintOpConfigWidget(QWidget * parent, Qt::WindowFlags f) - : KisConfigWidget(parent, f, 10), + : KisConfigWidget(parent, f, 100), m_isInsideUpdateCall(0) { } KisPaintOpConfigWidget::~KisPaintOpConfigWidget() { } void KisPaintOpConfigWidget::writeConfigurationSafe(KisPropertiesConfigurationSP config) const { if (m_isInsideUpdateCall) return; m_isInsideUpdateCall++; writeConfiguration(config); m_isInsideUpdateCall--; } void KisPaintOpConfigWidget::setConfigurationSafe(const KisPropertiesConfigurationSP config) { if (m_isInsideUpdateCall) return; m_isInsideUpdateCall++; setConfiguration(config); m_isInsideUpdateCall--; } void KisPaintOpConfigWidget::setImage(KisImageWSP image) { m_image = image; } void KisPaintOpConfigWidget::setNode(KisNodeWSP node) { m_node = node; } bool KisPaintOpConfigWidget::presetIsValid() { return true; } bool KisPaintOpConfigWidget::supportScratchBox() { return true; } diff --git a/libs/image/brushengine/kis_paintop_preset.cpp b/libs/image/brushengine/kis_paintop_preset.cpp index 2c424ad5f9..59d3f0aa2c 100644 --- a/libs/image/brushengine/kis_paintop_preset.cpp +++ b/libs/image/brushengine/kis_paintop_preset.cpp @@ -1,425 +1,425 @@ /* This file is part of the KDE project * Copyright (C) Boudewijn Rempt , (C) 2008 * Copyright (C) Sven Langkamp , (C) 2009 * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include #include #include #include #include #include #include #include #include #include "kis_paintop_registry.h" #include "kis_painter.h" #include #include "kis_paint_device.h" #include "kis_image.h" #include "kis_paintop_settings_update_proxy.h" #include #include struct Q_DECL_HIDDEN KisPaintOpPreset::Private { Private() : settings(0), dirtyPreset(false) { } KisPaintOpSettingsSP settings; bool dirtyPreset; QScopedPointer updateProxy; }; KisPaintOpPreset::KisPaintOpPreset() : KoResource(QString()) , m_d(new Private) { } KisPaintOpPreset::KisPaintOpPreset(const QString & fileName) : KoResource(fileName) , m_d(new Private) { } KisPaintOpPreset::~KisPaintOpPreset() { delete m_d; } KisPaintOpPresetSP KisPaintOpPreset::clone() const { KisPaintOpPresetSP preset(new KisPaintOpPreset()); if (settings()) { preset->setSettings(settings()); // the settings are cloned inside! } preset->setDirty(isDirty()); // only valid if we could clone the settings preset->setValid(settings()); preset->setPaintOp(paintOp()); preset->setName(name()); preset->setImage(image()); preset->settings()->setPreset(KisPaintOpPresetWSP(preset)); Q_ASSERT(preset->valid()); return preset; } void KisPaintOpPreset::setDirty(bool value) { m_d->dirtyPreset = value; } bool KisPaintOpPreset::isDirty() const { return m_d->dirtyPreset; } void KisPaintOpPreset::setPaintOp(const KoID & paintOp) { Q_ASSERT(m_d->settings); m_d->settings->setProperty("paintop", paintOp.id()); } KoID KisPaintOpPreset::paintOp() const { Q_ASSERT(m_d->settings); return KoID(m_d->settings->getString("paintop")); } void KisPaintOpPreset::setOptionsWidget(KisPaintOpConfigWidget* widget) { if (m_d->settings) { m_d->settings->setOptionsWidget(widget); if (widget) { widget->setConfigurationSafe(m_d->settings); } } } void KisPaintOpPreset::setSettings(KisPaintOpSettingsSP settings) { Q_ASSERT(settings); Q_ASSERT(!settings->getString("paintop", QString()).isEmpty()); DirtyStateSaver dirtyStateSaver(this); KisPaintOpConfigWidget *oldOptionsWidget = 0; if (m_d->settings) { oldOptionsWidget = m_d->settings->optionsWidget(); m_d->settings->setOptionsWidget(0); m_d->settings->setPreset(0); m_d->settings = 0; } if (settings) { m_d->settings = settings->clone(); m_d->settings->setPreset(KisPaintOpPresetWSP(this)); if (oldOptionsWidget) { - m_d->settings->setOptionsWidget(oldOptionsWidget); oldOptionsWidget->setConfigurationSafe(m_d->settings); + m_d->settings->setOptionsWidget(oldOptionsWidget); } } setValid(m_d->settings); if (m_d->updateProxy) { m_d->updateProxy->notifyUniformPropertiesChanged(); m_d->updateProxy->notifySettingsChanged(); } } KisPaintOpSettingsSP KisPaintOpPreset::settings() const { Q_ASSERT(m_d->settings); Q_ASSERT(!m_d->settings->getString("paintop", QString()).isEmpty()); return m_d->settings; } bool KisPaintOpPreset::load() { dbgImage << "Load preset " << filename(); setValid(false); if (filename().isEmpty()) { return false; } QIODevice *dev = 0; QByteArray ba; if (filename().startsWith("bundle://")) { QString bn = filename().mid(9); int pos = bn.lastIndexOf(":"); QString fn = bn.right(bn.size() - pos - 1); bn = bn.left(pos); QScopedPointer resourceStore(KoStore::createStore(bn, KoStore::Read, "application/x-krita-resourcebundle", KoStore::Zip)); if (!resourceStore || resourceStore->bad()) { warnKrita << "Could not open store on bundle" << bn; return false; } if (resourceStore->isOpen()) resourceStore->close(); if (!resourceStore->open(fn)) { warnKrita << "Could not open preset" << fn << "in bundle" << bn; return false; } ba = resourceStore->device()->readAll(); dev = new QBuffer(&ba); resourceStore->close(); } else { dev = new QFile(filename()); if (dev->size() == 0) { delete dev; return false; } if (!dev->open(QIODevice::ReadOnly)) { warnKrita << "Can't open file " << filename(); delete dev; return false; } } bool res = loadFromDevice(dev); delete dev; setValid(res); setDirty(false); return res; } bool KisPaintOpPreset::loadFromDevice(QIODevice *dev) { QImageReader reader(dev, "PNG"); QString version = reader.text("version"); QString preset = reader.text("preset"); dbgImage << version; if (version != "2.2") { return false; } QImage img; if (!reader.read(&img)) { dbgImage << "Fail to decode PNG"; return false; } //Workaround for broken presets //Presets was saved with nested cdata section preset.replace(""); preset.replace("]]>", ""); QDomDocument doc; if (!doc.setContent(preset)) { return false; } fromXML(doc.documentElement()); if (!m_d->settings) { return false; } setValid(true); setImage(img); return true; } bool KisPaintOpPreset::save() { if (filename().isEmpty()) return false; QString paintopid = m_d->settings->getString("paintop", QString()); if (paintopid.isEmpty()) return false; QFile f(filename()); f.open(QFile::WriteOnly); return saveToDevice(&f); } void KisPaintOpPreset::toXML(QDomDocument& doc, QDomElement& elt) const { QString paintopid = m_d->settings->getString("paintop", QString()); elt.setAttribute("paintopid", paintopid); elt.setAttribute("name", name()); // sanitize the settings bool hasTexture = m_d->settings->getBool("Texture/Pattern/Enabled"); if (!hasTexture) { Q_FOREACH (const QString & key, m_d->settings->getProperties().keys()) { if (key.startsWith("Texture") && key != "Texture/Pattern/Enabled") { m_d->settings->removeProperty(key); } } } m_d->settings->toXML(doc, elt); } void KisPaintOpPreset::fromXML(const QDomElement& presetElt) { setName(presetElt.attribute("name")); QString paintopid = presetElt.attribute("paintopid"); if (paintopid.isEmpty()) { dbgImage << "No paintopid attribute"; setValid(false); return; } if (KisPaintOpRegistry::instance()->get(paintopid) == 0) { dbgImage << "No paintop " << paintopid; setValid(false); return; } KoID id(paintopid, QString()); KisPaintOpSettingsSP settings = KisPaintOpRegistry::instance()->settings(id); if (!settings) { setValid(false); warnKrita << "Could not load settings for preset" << paintopid; return; } settings->fromXML(presetElt); // sanitize the settings bool hasTexture = settings->getBool("Texture/Pattern/Enabled"); if (!hasTexture) { Q_FOREACH (const QString & key, settings->getProperties().keys()) { if (key.startsWith("Texture") && key != "Texture/Pattern/Enabled") { settings->removeProperty(key); } } } setSettings(settings); } bool KisPaintOpPreset::saveToDevice(QIODevice *dev) const { QImageWriter writer(dev, "PNG"); QDomDocument doc; QDomElement root = doc.createElement("Preset"); toXML(doc, root); doc.appendChild(root); writer.setText("version", "2.2"); writer.setText("preset", doc.toString()); QImage img; if (image().isNull()) { img = QImage(1, 1, QImage::Format_RGB32); } else { img = image(); } m_d->dirtyPreset = false; KoResource::saveToDevice(dev); return writer.write(img); } KisPaintopSettingsUpdateProxy* KisPaintOpPreset::updateProxy() const { if (!m_d->updateProxy) { m_d->updateProxy.reset(new KisPaintopSettingsUpdateProxy()); } return m_d->updateProxy.data(); } KisPaintopSettingsUpdateProxy* KisPaintOpPreset::updateProxyNoCreate() const { return m_d->updateProxy.data(); } QList KisPaintOpPreset::uniformProperties() { return m_d->settings->uniformProperties(m_d->settings); } bool KisPaintOpPreset::hasMaskingPreset() const { return m_d->settings && m_d->settings->hasMaskingSettings(); } KisPaintOpPresetSP KisPaintOpPreset::createMaskingPreset() const { KisPaintOpPresetSP result; if (m_d->settings && m_d->settings->hasMaskingSettings()) { result = new KisPaintOpPreset(); result->setSettings(m_d->settings->createMaskingSettings()); if (!result->valid()) { result.clear(); } } return result; } KisPaintOpPreset::UpdatedPostponer::UpdatedPostponer(KisPaintOpPreset *preset) : m_updateProxy(preset->updateProxyNoCreate()) { if (m_updateProxy) { m_updateProxy->postponeSettingsChanges(); } } KisPaintOpPreset::UpdatedPostponer::~UpdatedPostponer() { if (m_updateProxy) { m_updateProxy->unpostponeSettingsChanges(); } } diff --git a/libs/image/kis_edge_detection_kernel.cpp b/libs/image/kis_edge_detection_kernel.cpp index e4df5df300..1d09d3f6da 100644 --- a/libs/image/kis_edge_detection_kernel.cpp +++ b/libs/image/kis_edge_detection_kernel.cpp @@ -1,404 +1,420 @@ /* * Copyright (c) 2017 Wolthera van Hövell tot Westerflier * * 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_edge_detection_kernel.h" #include "kis_global.h" #include "kis_convolution_kernel.h" #include #include #include #include #include #include KisEdgeDetectionKernel::KisEdgeDetectionKernel() { } /* * This code is very similar to the gaussian kernel code, except unlike the gaussian code, * edge-detection kernels DO use the diagonals. * Except for the simple mode. We implement the simple mode because it is an analogue to * the old sobel filter. */ Eigen::Matrix KisEdgeDetectionKernel::createHorizontalMatrix(qreal radius, FilterType type, bool reverse) { int kernelSize = kernelSizeFromRadius(radius); Eigen::Matrix matrix(kernelSize, kernelSize); KIS_ASSERT_RECOVER_NOOP(kernelSize & 0x1); const int center = kernelSize / 2; if (type==Prewit) { for (int x = 0; x < kernelSize; x++) { for (int y=0; y KisEdgeDetectionKernel::createVerticalMatrix(qreal radius, FilterType type, bool reverse) { int kernelSize = kernelSizeFromRadius(radius); Eigen::Matrix matrix(kernelSize, kernelSize); KIS_ASSERT_RECOVER_NOOP(kernelSize & 0x1); const int center = kernelSize / 2; if (type==Prewit) { for (int y = 0; y < kernelSize; y++) { for (int x=0; x matrix = createHorizontalMatrix(radius, type, reverse); if (denormalize) { return KisConvolutionKernel::fromMatrix(matrix, 0.5, 1); } else { return KisConvolutionKernel::fromMatrix(matrix, 0, matrix.sum()); } } KisConvolutionKernelSP KisEdgeDetectionKernel::createVerticalKernel(qreal radius, KisEdgeDetectionKernel::FilterType type, bool denormalize, bool reverse) { Eigen::Matrix matrix = createVerticalMatrix(radius, type, reverse); if (denormalize) { return KisConvolutionKernel::fromMatrix(matrix, 0.5, 1); } else { return KisConvolutionKernel::fromMatrix(matrix, 0, matrix.sum()); } } int KisEdgeDetectionKernel::kernelSizeFromRadius(qreal radius) { return qMax((int)(2 * ceil(sigmaFromRadius(radius)) + 1), 3); } qreal KisEdgeDetectionKernel::sigmaFromRadius(qreal radius) { return 0.3 * radius + 0.3; } void KisEdgeDetectionKernel::applyEdgeDetection(KisPaintDeviceSP device, const QRect &rect, qreal xRadius, qreal yRadius, KisEdgeDetectionKernel::FilterType type, const QBitArray &channelFlags, KoUpdater *progressUpdater, FilterOutput output, bool writeToAlpha) { QPoint srcTopLeft = rect.topLeft(); KisPainter finalPainter(device); finalPainter.setChannelFlags(channelFlags); finalPainter.setProgress(progressUpdater); if (output == pythagorean || output == radian) { KisPaintDeviceSP x_denormalised = new KisPaintDevice(device->colorSpace()); KisPaintDeviceSP y_denormalised = new KisPaintDevice(device->colorSpace()); KisConvolutionKernelSP kernelHorizLeftRight = KisEdgeDetectionKernel::createHorizontalKernel(xRadius, type); KisConvolutionKernelSP kernelVerticalTopBottom = KisEdgeDetectionKernel::createVerticalKernel(yRadius, type); qreal horizontalCenter = qreal(kernelHorizLeftRight->width()) / 2.0; qreal verticalCenter = qreal(kernelVerticalTopBottom->height()) / 2.0; KisConvolutionPainter horizPainterLR(x_denormalised); horizPainterLR.setChannelFlags(channelFlags); horizPainterLR.setProgress(progressUpdater); horizPainterLR.applyMatrix(kernelHorizLeftRight, device, srcTopLeft - QPoint(0, ceil(horizontalCenter)), srcTopLeft - QPoint(0, ceil(horizontalCenter)), rect.size() + QSize(0, 2 * ceil(horizontalCenter)), BORDER_REPEAT); KisConvolutionPainter verticalPainterTB(y_denormalised); verticalPainterTB.setChannelFlags(channelFlags); verticalPainterTB.setProgress(progressUpdater); verticalPainterTB.applyMatrix(kernelVerticalTopBottom, device, srcTopLeft - QPoint(0, ceil(verticalCenter)), srcTopLeft - QPoint(0, ceil(verticalCenter)), rect.size() + QSize(0, 2 * ceil(verticalCenter)), BORDER_REPEAT); KisSequentialIterator yItterator(y_denormalised, rect); KisSequentialIterator xItterator(x_denormalised, rect); KisSequentialIterator finalIt(device, rect); const int pixelSize = device->colorSpace()->pixelSize(); const int channels = device->colorSpace()->channelCount(); + const int alphaPos = device->colorSpace()->alphaPos(); + KIS_SAFE_ASSERT_RECOVER_RETURN(alphaPos >= 0); + QVector yNormalised(channels); QVector xNormalised(channels); QVector finalNorm(channels); while(yItterator.nextPixel() && xItterator.nextPixel() && finalIt.nextPixel()) { device->colorSpace()->normalisedChannelsValue(yItterator.rawData(), yNormalised); device->colorSpace()->normalisedChannelsValue(xItterator.rawData(), xNormalised); device->colorSpace()->normalisedChannelsValue(finalIt.rawData(), finalNorm); if (output == pythagorean) { for (int c = 0; ccolorSpace()); qreal alpha = 0; for (int c = 0; c<(channels-1); c++) { alpha = alpha+finalNorm[c]; } alpha = qMin(alpha/(channels-1), col.opacityF()); col.setOpacity(alpha); memcpy(finalIt.rawData(), col.data(), pixelSize); } else { quint8* f = finalIt.rawData(); + finalNorm[alphaPos] = 1.0; device->colorSpace()->fromNormalisedChannelsValue(f, finalNorm); memcpy(finalIt.rawData(), f, pixelSize); } } } else { KisConvolutionKernelSP kernel; qreal center = 0; bool denormalize = !writeToAlpha; if (output == xGrowth) { kernel = KisEdgeDetectionKernel::createHorizontalKernel(xRadius, type, denormalize); center = qreal(kernel->width()) / 2.0; } else if (output == xFall) { kernel = KisEdgeDetectionKernel::createHorizontalKernel(xRadius, type, denormalize, true); center = qreal(kernel->width()) / 2.0; } else if (output == yGrowth) { kernel = KisEdgeDetectionKernel::createVerticalKernel(yRadius, type, denormalize); center = qreal(kernel->height()) / 2.0; } else { //yFall kernel = KisEdgeDetectionKernel::createVerticalKernel(yRadius, type, denormalize, true); center = qreal(kernel->height()) / 2.0; } if (writeToAlpha) { KisPaintDeviceSP denormalised = new KisPaintDevice(device->colorSpace()); KisConvolutionPainter kernelP(denormalised); kernelP.setChannelFlags(channelFlags); kernelP.setProgress(progressUpdater); kernelP.applyMatrix(kernel, device, srcTopLeft - QPoint(0, ceil(center)), srcTopLeft - QPoint(0, ceil(center)), rect.size() + QSize(0, 2 * ceil(center)), BORDER_REPEAT); KisSequentialIterator itterator(denormalised, rect); KisSequentialIterator finalIt(device, rect); const int pixelSize = device->colorSpace()->pixelSize(); const int channels = device->colorSpace()->colorChannelCount(); QVector normalised(channels); while (itterator.nextPixel() && finalIt.nextPixel()) { device->colorSpace()->normalisedChannelsValue(itterator.rawData(), normalised); KoColor col(finalIt.rawData(), device->colorSpace()); qreal alpha = 0; for (int c = 0; ccolorSpace()->setOpacity(finalIt.rawData(), 1.0, numConseqPixels); + } } } } void KisEdgeDetectionKernel::convertToNormalMap(KisPaintDeviceSP device, const QRect &rect, qreal xRadius, qreal yRadius, KisEdgeDetectionKernel::FilterType type, int channelToConvert, QVector channelOrder, QVector channelFlip, const QBitArray &channelFlags, KoUpdater *progressUpdater) { QPoint srcTopLeft = rect.topLeft(); KisPainter finalPainter(device); finalPainter.setChannelFlags(channelFlags); finalPainter.setProgress(progressUpdater); KisPaintDeviceSP x_denormalised = new KisPaintDevice(device->colorSpace()); KisPaintDeviceSP y_denormalised = new KisPaintDevice(device->colorSpace()); KisConvolutionKernelSP kernelHorizLeftRight = KisEdgeDetectionKernel::createHorizontalKernel(yRadius, type, true, !channelFlip[1]); KisConvolutionKernelSP kernelVerticalTopBottom = KisEdgeDetectionKernel::createVerticalKernel(xRadius, type, true, !channelFlip[0]); qreal horizontalCenter = qreal(kernelHorizLeftRight->width()) / 2.0; qreal verticalCenter = qreal(kernelVerticalTopBottom->height()) / 2.0; KisConvolutionPainter horizPainterLR(y_denormalised); horizPainterLR.setChannelFlags(channelFlags); horizPainterLR.setProgress(progressUpdater); horizPainterLR.applyMatrix(kernelHorizLeftRight, device, srcTopLeft - QPoint(ceil(horizontalCenter), 0), srcTopLeft - QPoint(ceil(horizontalCenter), 0), rect.size() + QSize(2 * ceil(horizontalCenter), 0), BORDER_REPEAT); KisConvolutionPainter verticalPainterTB(x_denormalised); verticalPainterTB.setChannelFlags(channelFlags); verticalPainterTB.setProgress(progressUpdater); verticalPainterTB.applyMatrix(kernelVerticalTopBottom, device, srcTopLeft - QPoint(0, ceil(verticalCenter)), srcTopLeft - QPoint(0, ceil(verticalCenter)), rect.size() + QSize(0, 2 * ceil(verticalCenter)), BORDER_REPEAT); KisSequentialIterator yItterator(y_denormalised, rect); KisSequentialIterator xItterator(x_denormalised, rect); KisSequentialIterator finalIt(device, rect); const int pixelSize = device->colorSpace()->pixelSize(); const int channels = device->colorSpace()->channelCount(); + const int alphaPos = device->colorSpace()->alphaPos(); + KIS_SAFE_ASSERT_RECOVER_RETURN(alphaPos >= 0); + QVector yNormalised(channels); QVector xNormalised(channels); QVector finalNorm(channels); while(yItterator.nextPixel() && xItterator.nextPixel() && finalIt.nextPixel()) { device->colorSpace()->normalisedChannelsValue(yItterator.rawData(), yNormalised); device->colorSpace()->normalisedChannelsValue(xItterator.rawData(), xNormalised); qreal z = 1.0; if (channelFlip[2]==true){ z=-1.0; } QVector3D normal = QVector3D((xNormalised[channelToConvert]-0.5)*2, (yNormalised[channelToConvert]-0.5)*2, z); normal.normalize(); finalNorm.fill(1.0); for (int c = 0; c<3; c++) { finalNorm[device->colorSpace()->channels().at(channelOrder[c])->displayPosition()] = (normal[channelOrder[c]]/2)+0.5; } + finalNorm[alphaPos]= 1.0; + quint8* pixel = finalIt.rawData(); device->colorSpace()->fromNormalisedChannelsValue(pixel, finalNorm); memcpy(finalIt.rawData(), pixel, pixelSize); } } diff --git a/libs/pigment/KoColorSpace.h b/libs/pigment/KoColorSpace.h index fc0d6a5032..036760e00d 100644 --- a/libs/pigment/KoColorSpace.h +++ b/libs/pigment/KoColorSpace.h @@ -1,641 +1,646 @@ /* * Copyright (c) 2005 Boudewijn Rempt * Copyright (c) 2006-2007 Cyrille Berger * * 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; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #ifndef KOCOLORSPACE_H #define KOCOLORSPACE_H #include #include #include #include #include #include #include "KoColorSpaceConstants.h" #include "KoColorConversionTransformation.h" #include "KoColorProofingConversionTransformation.h" #include "KoCompositeOp.h" #include #include "kritapigment_export.h" class QDomDocument; class QDomElement; class KoChannelInfo; class KoColorProfile; class KoColorTransformation; class QBitArray; enum Deletability { OwnedByRegistryDoNotDelete, OwnedByRegistryRegistryDeletes, NotOwnedByRegistry }; enum ColorSpaceIndependence { FULLY_INDEPENDENT, TO_LAB16, TO_RGBA8, TO_RGBA16 }; class KoMixColorsOp; class KoConvolutionOp; /** * A KoColorSpace is the definition of a certain color space. * * A color model and a color space are two related concepts. A color * model is more general in that it describes the channels involved and * how they in broad terms combine to describe a color. Examples are * RGB, HSV, CMYK. * * A color space is more specific in that it also describes exactly how * the channels are combined. So for each color model there can be a * number of specific color spaces. So RGB is the model and sRGB, * adobeRGB, etc are colorspaces. * * In Pigment KoColorSpace acts as both a color model and a color space. * You can think of the class definition as the color model, but the * instance of the class as representing a colorspace. * * A third concept is the profile represented by KoColorProfile. It * represents the info needed to specialize a color model into a color * space. * * KoColorSpace is an abstract class serving as an interface. * * Subclasses implement actual color spaces * Some subclasses implement only some parts and are named Traits * */ class KRITAPIGMENT_EXPORT KoColorSpace : public boost::equality_comparable { friend class KoColorSpaceRegistry; friend class KoColorSpaceFactory; protected: /// Only for use by classes that serve as baseclass for real color spaces KoColorSpace(); public: /// Should be called by real color spaces KoColorSpace(const QString &id, const QString &name, KoMixColorsOp* mixColorsOp, KoConvolutionOp* convolutionOp); virtual bool operator==(const KoColorSpace& rhs) const; protected: virtual ~KoColorSpace(); public: //========== Gamut and other basic info ===================================// /* * @returns QPolygonF with 5*channel samples converted to xyY. * maybe convert to 3d space in future? */ QPolygonF gamutXYY() const; /* * @returns a polygon with 5 samples per channel converted to xyY, but unlike * gamutxyY it focuses on the luminance. This then can be used to visualise * the approximate trc of a given colorspace. */ QPolygonF estimatedTRCXYY() const; QVector lumaCoefficients() const; //========== Channels =====================================================// /// Return a list describing all the channels this color model has. The order /// of the channels in the list is the order of channels in the pixel. To find /// out the preferred display position, use KoChannelInfo::displayPosition. QList channels() const; /** * The total number of channels for a single pixel in this color model */ virtual quint32 channelCount() const = 0; + /** + * Position of the alpha channel in a pixel + */ + virtual quint32 alphaPos() const = 0; + /** * The total number of color channels (excludes alpha) for a single * pixel in this color model. */ virtual quint32 colorChannelCount() const = 0; /** * returns a QBitArray that contains true for the specified * channel types: * * @param color if true, set all color channels to true * @param alpha if true, set all alpha channels to true * * The order of channels is the colorspace descriptive order, * not the pixel order. */ QBitArray channelFlags(bool color = true, bool alpha = false) const; /** * The size in bytes of a single pixel in this color model */ virtual quint32 pixelSize() const = 0; /** * Return a string with the channel's value suitable for display in the gui. */ virtual QString channelValueText(const quint8 *pixel, quint32 channelIndex) const = 0; /** * Return a string with the channel's value with integer * channels normalised to the floating point range 0 to 1, if * appropriate. */ virtual QString normalisedChannelValueText(const quint8 *pixel, quint32 channelIndex) const = 0; /** * Return a QVector of floats with channels' values normalized * to floating point range 0 to 1. */ virtual void normalisedChannelsValue(const quint8 *pixel, QVector &channels) const = 0; /** * Write in the pixel the value from the normalized vector. */ virtual void fromNormalisedChannelsValue(quint8 *pixel, const QVector &values) const = 0; /** * Convert the value of the channel at the specified position into * an 8-bit value. The position is not the number of bytes, but * the position of the channel as defined in the channel info list. */ virtual quint8 scaleToU8(const quint8 * srcPixel, qint32 channelPos) const = 0; /** * Set dstPixel to the pixel containing only the given channel of srcPixel. The remaining channels * should be set to whatever makes sense for 'empty' channels of this color space, * with the intent being that the pixel should look like it only has the given channel. */ virtual void singleChannelPixel(quint8 *dstPixel, const quint8 *srcPixel, quint32 channelIndex) const = 0; //========== Identification ===============================================// /** * ID for use in files and internally: unchanging name. As the id must be unique * it is usually the concatenation of the id of the color model and of the color * depth, for instance "RGBA8" or "CMYKA16" or "XYZA32f". */ QString id() const; /** * User visible name which contains the name of the color model and of the color depth. * For instance "RGBA (8-bits)" or "CMYKA (16-bits)". */ QString name() const; /** * @return a string that identify the color model (for instance "RGB" or "CMYK" ...) * @see KoColorModelStandardIds.h */ virtual KoID colorModelId() const = 0; /** * @return a string that identify the bit depth (for instance "U8" or "F16" ...) * @see KoColorModelStandardIds.h */ virtual KoID colorDepthId() const = 0; /** * @return true if the profile given in argument can be used by this color space */ virtual bool profileIsCompatible(const KoColorProfile* profile) const = 0; /** * If false, images in this colorspace will degrade considerably by * functions, tools and filters that have the given measure of colorspace * independence. * * @param independence the measure to which this colorspace will suffer * from the manipulations of the tool or filter asking * @return false if no degradation will take place, true if degradation will * take place */ virtual bool willDegrade(ColorSpaceIndependence independence) const = 0; //========== Capabilities =================================================// /** * Tests if the colorspace offers the specific composite op. */ virtual bool hasCompositeOp(const QString & id) const; /** * Returns the list of user-visible composite ops supported by this colorspace. */ virtual QList compositeOps() const; /** * Retrieve a single composite op from the ones this colorspace offers. * If the requeste composite op does not exist, COMPOSITE_OVER is returned. */ const KoCompositeOp * compositeOp(const QString & id) const; /** * add a composite op to this colorspace. */ virtual void addCompositeOp(const KoCompositeOp * op); /** * Returns true if the colorspace supports channel values outside the * (normalised) range 0 to 1. */ virtual bool hasHighDynamicRange() const = 0; //========== Display profiles =============================================// /** * Return the profile of this color space. */ virtual const KoColorProfile * profile() const = 0; //================= Conversion functions ==================================// /** * The fromQColor methods take a given color defined as an RGB QColor * and fills a byte array with the corresponding color in the * the colorspace managed by this strategy. * * @param color the QColor that will be used to fill dst * @param dst a pointer to a pixel * @param profile the optional profile that describes the color values of QColor */ virtual void fromQColor(const QColor& color, quint8 *dst, const KoColorProfile * profile = 0) const = 0; /** * The toQColor methods take a byte array that is at least pixelSize() long * and converts the contents to a QColor, using the given profile as a source * profile and the optional profile as a destination profile. * * @param src a pointer to the source pixel * @param c the QColor that will be filled with the color at src * @param profile the optional profile that describes the color in c, for instance the monitor profile */ virtual void toQColor(const quint8 *src, QColor *c, const KoColorProfile * profile = 0) const = 0; /** * Convert the pixels in data to (8-bit BGRA) QImage using the specified profiles. * * @param data A pointer to a contiguous memory region containing width * height pixels * @param width in pixels * @param height in pixels * @param dstProfile destination profile * @param renderingIntent the rendering intent * @param conversionFlags conversion flags */ virtual QImage convertToQImage(const quint8 *data, qint32 width, qint32 height, const KoColorProfile * dstProfile, KoColorConversionTransformation::Intent renderingIntent, KoColorConversionTransformation::ConversionFlags conversionFlags) const; /** * Convert the specified data to Lab (D50). All colorspaces are guaranteed to support this * * @param src the source data * @param dst the destination data * @param nPixels the number of source pixels */ virtual void toLabA16(const quint8 * src, quint8 * dst, quint32 nPixels) const; /** * Convert the specified data from Lab (D50). to this colorspace. All colorspaces are * guaranteed to support this. * * @param src the pixels in 16 bit lab format * @param dst the destination data * @param nPixels the number of pixels in the array */ virtual void fromLabA16(const quint8 * src, quint8 * dst, quint32 nPixels) const; /** * Convert the specified data to sRGB 16 bits. All colorspaces are guaranteed to support this * * @param src the source data * @param dst the destination data * @param nPixels the number of source pixels */ virtual void toRgbA16(const quint8 * src, quint8 * dst, quint32 nPixels) const; /** * Convert the specified data from sRGB 16 bits. to this colorspace. All colorspaces are * guaranteed to support this. * * @param src the pixels in 16 bit rgb format * @param dst the destination data * @param nPixels the number of pixels in the array */ virtual void fromRgbA16(const quint8 * src, quint8 * dst, quint32 nPixels) const; /** * Create a color conversion transformation. */ virtual KoColorConversionTransformation* createColorConverter(const KoColorSpace * dstColorSpace, KoColorConversionTransformation::Intent renderingIntent, KoColorConversionTransformation::ConversionFlags conversionFlags) const; /** * Convert a byte array of srcLen pixels *src to the specified color space * and put the converted bytes into the prepared byte array *dst. * * Returns false if the conversion failed, true if it succeeded * * This function is not thread-safe. If you want to apply multiple conversion * in different threads at the same time, you need to create one color converter * per-thread using createColorConverter. */ virtual bool convertPixelsTo(const quint8 * src, quint8 * dst, const KoColorSpace * dstColorSpace, quint32 numPixels, KoColorConversionTransformation::Intent renderingIntent, KoColorConversionTransformation::ConversionFlags conversionFlags) const; virtual KoColorConversionTransformation *createProofingTransform(const KoColorSpace * dstColorSpace, const KoColorSpace * proofingSpace, KoColorConversionTransformation::Intent renderingIntent, KoColorConversionTransformation::Intent proofingIntent, KoColorConversionTransformation::ConversionFlags conversionFlags, quint8 *gamutWarning, double adaptationState) const; /** * @brief proofPixelsTo * @param src source * @param dst destination * @param numPixels the amount of pixels. * @param proofingTransform the intent used for proofing. * @return */ virtual bool proofPixelsTo(const quint8 * src, quint8 * dst, quint32 numPixels, KoColorConversionTransformation *proofingTransform) const; //============================== Manipulation functions ==========================// // // The manipulation functions have default implementations that _convert_ the pixel // to a QColor and back. Reimplement these methods in your color strategy! // /** * Get the alpha value of the given pixel, downscaled to an 8-bit value. */ virtual quint8 opacityU8(const quint8 * pixel) const = 0; virtual qreal opacityF(const quint8 * pixel) const = 0; /** * Set the alpha channel of the given run of pixels to the given value. * * pixels -- a pointer to the pixels that will have their alpha set to this value * alpha -- a downscaled 8-bit value for opacity * nPixels -- the number of pixels * */ virtual void setOpacity(quint8 * pixels, quint8 alpha, qint32 nPixels) const = 0; virtual void setOpacity(quint8 * pixels, qreal alpha, qint32 nPixels) const = 0; /** * Multiply the alpha channel of the given run of pixels by the given value. * * pixels -- a pointer to the pixels that will have their alpha set to this value * alpha -- a downscaled 8-bit value for opacity * nPixels -- the number of pixels * */ virtual void multiplyAlpha(quint8 * pixels, quint8 alpha, qint32 nPixels) const = 0; /** * Applies the specified 8-bit alpha mask to the pixels. We assume that there are just * as many alpha values as pixels but we do not check this; the alpha values * are assumed to be 8-bits. */ virtual void applyAlphaU8Mask(quint8 * pixels, const quint8 * alpha, qint32 nPixels) const = 0; /** * Applies the inverted 8-bit alpha mask to the pixels. We assume that there are just * as many alpha values as pixels but we do not check this; the alpha values * are assumed to be 8-bits. */ virtual void applyInverseAlphaU8Mask(quint8 * pixels, const quint8 * alpha, qint32 nPixels) const = 0; /** * Applies the specified float alpha mask to the pixels. We assume that there are just * as many alpha values as pixels but we do not check this; alpha values have to be between 0.0 and 1.0 */ virtual void applyAlphaNormedFloatMask(quint8 * pixels, const float * alpha, qint32 nPixels) const = 0; /** * Applies the inverted specified float alpha mask to the pixels. We assume that there are just * as many alpha values as pixels but we do not check this; alpha values have to be between 0.0 and 1.0 */ virtual void applyInverseNormedFloatMask(quint8 * pixels, const float * alpha, qint32 nPixels) const = 0; /** * Create an adjustment object for adjusting the brightness and contrast * transferValues is a 256 bins array with values from 0 to 0xFFFF * This function is thread-safe, but you need to create one KoColorTransformation per thread. */ virtual KoColorTransformation *createBrightnessContrastAdjustment(const quint16 *transferValues) const = 0; /** * Create an adjustment object for adjusting individual channels * transferValues is an array of colorChannelCount number of 256 bins array with values from 0 to 0xFFFF * This function is thread-safe, but you need to create one KoColorTransformation per thread. * * The layout of the channels must be the following: * * 0..N-2 - color channels of the pixel; * N-1 - alpha channel of the pixel (if exists) */ virtual KoColorTransformation *createPerChannelAdjustment(const quint16 * const* transferValues) const = 0; /** * Darken all color channels with the given amount. If compensate is true, * the compensation factor will be used to limit the darkening. * */ virtual KoColorTransformation *createDarkenAdjustment(qint32 shade, bool compensate, qreal compensation) const = 0; /** * Invert color channels of the given pixels * This function is thread-safe, but you need to create one KoColorTransformation per thread. */ virtual KoColorTransformation *createInvertTransformation() const = 0; /** * Get the difference between 2 colors, normalized in the range (0,255). Only completely * opaque and completely transparent are taken into account when computing the difference; * other transparency levels are not regarded when finding the difference. */ virtual quint8 difference(const quint8* src1, const quint8* src2) const = 0; /** * Get the difference between 2 colors, normalized in the range (0,255). This function * takes the Alpha channel of the pixel into account. Alpha channel has the same * weight as Lightness channel. */ virtual quint8 differenceA(const quint8* src1, const quint8* src2) const = 0; /** * @return the mix color operation of this colorspace (do not delete it locally, it's deleted by the colorspace). */ virtual KoMixColorsOp* mixColorsOp() const; /** * @return the convolution operation of this colorspace (do not delete it locally, it's deleted by the colorspace). */ virtual KoConvolutionOp* convolutionOp() const; /** * Calculate the intensity of the given pixel, scaled down to the range 0-255. XXX: Maybe this should be more flexible */ virtual quint8 intensity8(const quint8 * src) const = 0; /* *increase luminosity by step */ virtual void increaseLuminosity(quint8 * pixel, qreal step) const; virtual void decreaseLuminosity(quint8 * pixel, qreal step) const; virtual void increaseSaturation(quint8 * pixel, qreal step) const; virtual void decreaseSaturation(quint8 * pixel, qreal step) const; virtual void increaseHue(quint8 * pixel, qreal step) const; virtual void decreaseHue(quint8 * pixel, qreal step) const; virtual void increaseRed(quint8 * pixel, qreal step) const; virtual void increaseGreen(quint8 * pixel, qreal step) const; virtual void increaseBlue(quint8 * pixel, qreal step) const; virtual void increaseYellow(quint8 * pixel, qreal step) const; virtual void toHSY(const QVector &channelValues, qreal *hue, qreal *sat, qreal *luma) const = 0; virtual QVector fromHSY(qreal *hue, qreal *sat, qreal *luma) const = 0; virtual void toYUV(const QVector &channelValues, qreal *y, qreal *u, qreal *v) const = 0; virtual QVector fromYUV(qreal *y, qreal *u, qreal *v) const = 0; /** * Compose two arrays of pixels together. If source and target * are not the same color model, the source pixels will be * converted to the target model. We're "dst" -- "dst" pixels are always in _this_ * colorspace. * * @param srcSpace the colorspace of the source pixels that will be composited onto "us" * @param params the information needed for blitting e.g. the source and destination pixel data, * the opacity and flow, ... * @param op the composition operator to use, e.g. COPY_OVER * @param renderingIntent the rendering intent * @param conversionFlags the conversion flags. * */ virtual void bitBlt(const KoColorSpace* srcSpace, const KoCompositeOp::ParameterInfo& params, const KoCompositeOp* op, KoColorConversionTransformation::Intent renderingIntent, KoColorConversionTransformation::ConversionFlags conversionFlags) const; /** * Serialize this color following Create's swatch color specification available * at http://create.freedesktop.org/wiki/index.php/Swatches_-_colour_file_format * * This function doesn't create the \ element but rather the \, * \, \ ... elements. It is assumed that colorElt is the \ * element. * * @param pixel buffer to serialized * @param colorElt root element for the serialization, it is assumed that this * element is \ * @param doc is the document containing colorElt */ virtual void colorToXML(const quint8* pixel, QDomDocument& doc, QDomElement& colorElt) const = 0; /** * Unserialize a color following Create's swatch color specification available * at http://create.freedesktop.org/wiki/index.php/Swatches_-_colour_file_format * * @param pixel buffer where the color will be unserialized * @param elt the element to unserialize (\, \, \) * @return the unserialize color, or an empty color object if the function failed * to unserialize the color */ virtual void colorFromXML(quint8* pixel, const QDomElement& elt) const = 0; KoColorTransformation* createColorTransformation(const QString & id, const QHash & parameters) const; protected: /** * Use this function in the constructor of your colorspace to add the information about a channel. * @param ci a pointer to the information about a channel */ virtual void addChannel(KoChannelInfo * ci); const KoColorConversionTransformation* toLabA16Converter() const; const KoColorConversionTransformation* fromLabA16Converter() const; const KoColorConversionTransformation* toRgbA16Converter() const; const KoColorConversionTransformation* fromRgbA16Converter() const; /** * Returns the thread-local conversion cache. If it doesn't exist * yet, it is created. If it is currently too small, it is resized. */ QVector * threadLocalConversionCache(quint32 size) const; /** * This function defines the behavior of the bitBlt function * when the composition of pixels in different colorspaces is * requested, that is in case: * * srcCS == any * dstCS == this * * 1) preferCompositionInSourceColorSpace() == false, * * the source pixels are first converted to *this color space * and then composition is performed. * * 2) preferCompositionInSourceColorSpace() == true, * * the destination pixels are first converted into *srcCS color * space, then the composition is done, and the result is finally * converted into *this colorspace. * * This is used by alpha8() color space mostly, because it has * weaker representation of the color, so the composition * should be done in CS with richer functionality. */ virtual bool preferCompositionInSourceColorSpace() const; struct Private; Private * const d; }; inline QDebug operator<<(QDebug dbg, const KoColorSpace *cs) { if (cs) { dbg.nospace() << cs->name() << " (" << cs->colorModelId().id() << "," << cs->colorDepthId().id() << " )"; } else { dbg.nospace() << "0x0"; } return dbg.space(); } #endif // KOCOLORSPACE_H diff --git a/libs/pigment/KoColorSpaceAbstract.h b/libs/pigment/KoColorSpaceAbstract.h index 877808fb31..54f4519e63 100644 --- a/libs/pigment/KoColorSpaceAbstract.h +++ b/libs/pigment/KoColorSpaceAbstract.h @@ -1,211 +1,216 @@ /* * Copyright (c) 2006 Cyrille Berger * Copyright (c) 2007 Emanuele Tamponi * * 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; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #ifndef KOCOLORSPACEABSTRACT_H #define KOCOLORSPACEABSTRACT_H #include #include #include #include #include #include #include "KoFallBackColorTransformation.h" #include "KoLabDarkenColorTransformation.h" #include "KoMixColorsOpImpl.h" #include "KoConvolutionOpImpl.h" #include "KoInvertColorTransformation.h" /** * This in an implementation of KoColorSpace which can be used as a base for colorspaces with as many * different channels of the same type. * * The template parameters must be a class which inherits KoColorSpaceTrait (or a class with the same signature). * * SOMETYPE is the type of the channel for instance (quint8, quint32...), * SOMENBOFCHANNELS is the number of channels including the alpha channel * SOMEALPHAPOS is the position of the alpha channel in the pixel (can be equal to -1 if no alpha channel). */ template class KoColorSpaceAbstract : public KoColorSpace { public: typedef _CSTrait ColorSpaceTraits; public: KoColorSpaceAbstract(const QString &id, const QString &name) : KoColorSpace(id, name, new KoMixColorsOpImpl< _CSTrait>(), new KoConvolutionOpImpl< _CSTrait>()) { } quint32 colorChannelCount() const override { if (_CSTrait::alpha_pos == -1) return _CSTrait::channels_nb; else return _CSTrait::channels_nb - 1; } quint32 channelCount() const override { return _CSTrait::channels_nb; } + quint32 alphaPos() const override { + return _CSTrait::alpha_pos; + } + + quint32 pixelSize() const override { return _CSTrait::pixelSize; } QString channelValueText(const quint8 *pixel, quint32 channelIndex) const override { return _CSTrait::channelValueText(pixel, channelIndex); } QString normalisedChannelValueText(const quint8 *pixel, quint32 channelIndex) const override { return _CSTrait::normalisedChannelValueText(pixel, channelIndex); } void normalisedChannelsValue(const quint8 *pixel, QVector &channels) const override { return _CSTrait::normalisedChannelsValue(pixel, channels); } void fromNormalisedChannelsValue(quint8 *pixel, const QVector &values) const override { return _CSTrait::fromNormalisedChannelsValue(pixel, values); } quint8 scaleToU8(const quint8 * srcPixel, qint32 channelIndex) const override { typename _CSTrait::channels_type c = _CSTrait::nativeArray(srcPixel)[channelIndex]; return KoColorSpaceMaths::scaleToA(c); } void singleChannelPixel(quint8 *dstPixel, const quint8 *srcPixel, quint32 channelIndex) const override { _CSTrait::singleChannelPixel(dstPixel, srcPixel, channelIndex); } quint8 opacityU8(const quint8 * U8_pixel) const override { return _CSTrait::opacityU8(U8_pixel); } qreal opacityF(const quint8 * U8_pixel) const override { return _CSTrait::opacityF(U8_pixel); } void setOpacity(quint8 * pixels, quint8 alpha, qint32 nPixels) const override { _CSTrait::setOpacity(pixels, alpha, nPixels); } void setOpacity(quint8 * pixels, qreal alpha, qint32 nPixels) const override { _CSTrait::setOpacity(pixels, alpha, nPixels); } void multiplyAlpha(quint8 * pixels, quint8 alpha, qint32 nPixels) const override { _CSTrait::multiplyAlpha(pixels, alpha, nPixels); } void applyAlphaU8Mask(quint8 * pixels, const quint8 * alpha, qint32 nPixels) const override { _CSTrait::applyAlphaU8Mask(pixels, alpha, nPixels); } void applyInverseAlphaU8Mask(quint8 * pixels, const quint8 * alpha, qint32 nPixels) const override { _CSTrait::applyInverseAlphaU8Mask(pixels, alpha, nPixels); } void applyAlphaNormedFloatMask(quint8 * pixels, const float * alpha, qint32 nPixels) const override { _CSTrait::applyAlphaNormedFloatMask(pixels, alpha, nPixels); } void applyInverseNormedFloatMask(quint8 * pixels, const float * alpha, qint32 nPixels) const override { _CSTrait::applyInverseAlphaNormedFloatMask(pixels, alpha, nPixels); } quint8 intensity8(const quint8 * src) const override { QColor c; const_cast *>(this)->toQColor(src, &c); return static_cast(c.red() * 0.30 + c.green() * 0.59 + c.blue() * 0.11); } KoColorTransformation* createInvertTransformation() const override { return KoInvertColorTransformation::getTransformator(this); } KoColorTransformation *createDarkenAdjustment(qint32 shade, bool compensate, qreal compensation) const override { return new KoFallBackColorTransformation(this, KoColorSpaceRegistry::instance()->lab16(""), new KoLabDarkenColorTransformation(shade, compensate, compensation, KoColorSpaceRegistry::instance()->lab16(""))); } bool convertPixelsTo(const quint8 *src, quint8 *dst, const KoColorSpace *dstColorSpace, quint32 numPixels, KoColorConversionTransformation::Intent renderingIntent, KoColorConversionTransformation::ConversionFlags conversionFlags) const override { // check whether we have the same profile and color model, but only a different bit // depth; in that case we don't convert as such, but scale bool scaleOnly = false; // Note: getting the id() is really, really expensive, so only do that if // we are sure there is a difference between the colorspaces if (!(*this == *dstColorSpace)) { scaleOnly = dstColorSpace->colorModelId().id() == colorModelId().id() && dstColorSpace->colorDepthId().id() != colorDepthId().id() && dstColorSpace->profile()->name() == profile()->name(); } if (scaleOnly && dynamic_cast(dstColorSpace)) { typedef typename _CSTrait::channels_type channels_type; switch(dstColorSpace->channels()[0]->channelValueType()) { case KoChannelInfo::UINT8: scalePixels<_CSTrait::pixelSize, 1, channels_type, quint8>(src, dst, numPixels); return true; // case KoChannelInfo::INT8: // scalePixels<_CSTrait::pixelSize, 1, channels_type, qint8>(src, dst, numPixels); // return true; case KoChannelInfo::UINT16: scalePixels<_CSTrait::pixelSize, 2, channels_type, quint16>(src, dst, numPixels); return true; case KoChannelInfo::INT16: scalePixels<_CSTrait::pixelSize, 2, channels_type, qint16>(src, dst, numPixels); return true; case KoChannelInfo::UINT32: scalePixels<_CSTrait::pixelSize, 4, channels_type, quint32>(src, dst, numPixels); return true; default: break; } } return KoColorSpace::convertPixelsTo(src, dst, dstColorSpace, numPixels, renderingIntent, conversionFlags); } private: template void scalePixels(const quint8* src, quint8* dst, quint32 numPixels) const { qint32 dstPixelSize = dstChannelSize * _CSTrait::channels_nb; for(quint32 i=0; i(src + i * srcPixelSize); TDstChannel* dstPixel = reinterpret_cast(dst + i * dstPixelSize); for(quint32 c=0; c<_CSTrait::channels_nb; ++c) dstPixel[c] = Arithmetic::scale(srcPixel[c]); } } }; #endif // KOCOLORSPACEABSTRACT_H diff --git a/libs/ui/KisMainWindow.cpp b/libs/ui/KisMainWindow.cpp index c29cbb36eb..4cd84d9a03 100644 --- a/libs/ui/KisMainWindow.cpp +++ b/libs/ui/KisMainWindow.cpp @@ -1,2754 +1,2762 @@ /* This file is part of the KDE project Copyright (C) 1998, 1999 Torben Weis Copyright (C) 2000-2006 David Faure Copyright (C) 2007, 2009 Thomas zander Copyright (C) 2010 Benjamin Port This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "KisMainWindow.h" #include // qt includes #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kis_selection_manager.h" #include "kis_icon_utils.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include "KoDockFactoryBase.h" #include "KoDocumentInfoDlg.h" #include "KoDocumentInfo.h" #include "KoFileDialog.h" #include #include #include #include #include #include "KoToolDocker.h" #include "KoToolBoxDocker_p.h" #include #include #include #include #include #include #ifdef Q_OS_ANDROID #include #endif #include #include "dialogs/kis_about_application.h" #include "dialogs/kis_delayed_save_dialog.h" #include "dialogs/kis_dlg_preferences.h" #include "kis_action_manager.h" #include "KisApplication.h" #include "kis_canvas2.h" #include "kis_canvas_controller.h" #include "kis_canvas_resource_provider.h" #include "kis_clipboard.h" #include "kis_config.h" #include "kis_config_notifier.h" #include "kis_custom_image_widget.h" #include #include "kis_group_layer.h" #include "kis_image_from_clipboard_widget.h" #include "kis_image.h" #include #include "KisImportExportManager.h" #include "kis_mainwindow_observer.h" #include "kis_memory_statistics_server.h" #include "kis_node.h" #include "KisOpenPane.h" #include "kis_paintop_box.h" #include "KisPart.h" #include "KisPrintJob.h" #include "KisResourceServerProvider.h" #include "kis_signal_compressor_with_param.h" #include "kis_statusbar.h" #include "KisView.h" #include "KisViewManager.h" #include "thememanager.h" #include "kis_animation_importer.h" #include "dialogs/kis_dlg_import_image_sequence.h" #include #include "KisWindowLayoutManager.h" #include #include "KisWelcomePageWidget.h" #include #include #include "KisCanvasWindow.h" #include "kis_action.h" #include class ToolDockerFactory : public KoDockFactoryBase { public: ToolDockerFactory() : KoDockFactoryBase() { } QString id() const override { return "sharedtooldocker"; } QDockWidget* createDockWidget() override { KoToolDocker* dockWidget = new KoToolDocker(); return dockWidget; } DockPosition defaultDockPosition() const override { return DockRight; } }; class Q_DECL_HIDDEN KisMainWindow::Private { public: Private(KisMainWindow *parent, QUuid id) : q(parent) , id(id) , dockWidgetMenu(new KActionMenu(i18nc("@action:inmenu", "&Dockers"), parent)) , windowMenu(new KActionMenu(i18nc("@action:inmenu", "&Window"), parent)) , documentMenu(new KActionMenu(i18nc("@action:inmenu", "New &View"), parent)) , workspaceMenu(new KActionMenu(i18nc("@action:inmenu", "Wor&kspace"), parent)) , welcomePage(new KisWelcomePageWidget(parent)) , widgetStack(new QStackedWidget(parent)) , mdiArea(new QMdiArea(parent)) , windowMapper(new QSignalMapper(parent)) , documentMapper(new QSignalMapper(parent)) #ifdef Q_OS_ANDROID , fileManager(new KisAndroidFileManager) #endif { if (id.isNull()) this->id = QUuid::createUuid(); welcomeScroller = new QScrollArea(); welcomeScroller->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded); welcomeScroller->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); welcomeScroller->setWidget(welcomePage); welcomeScroller->setWidgetResizable(true); widgetStack->addWidget(welcomeScroller); widgetStack->addWidget(mdiArea); mdiArea->setTabsMovable(true); mdiArea->setActivationOrder(QMdiArea::ActivationHistoryOrder); } ~Private() { qDeleteAll(toolbarList); } KisMainWindow *q {0}; QUuid id; KisViewManager *viewManager {0}; QPointer activeView; QList toolbarList; bool firstTime {true}; bool windowSizeDirty {false}; bool readOnly {false}; KisAction *showDocumentInfo {0}; KisAction *saveAction {0}; KisAction *saveActionAs {0}; // KisAction *printAction; // KisAction *printActionPreview; // KisAction *exportPdf {0}; KisAction *importAnimation {0}; KisAction *closeAll {0}; // KisAction *reloadFile; KisAction *importFile {0}; KisAction *exportFile {0}; KisAction *undo {0}; KisAction *redo {0}; KisAction *newWindow {0}; KisAction *close {0}; KisAction *mdiCascade {0}; KisAction *mdiTile {0}; KisAction *mdiNextWindow {0}; KisAction *mdiPreviousWindow {0}; KisAction *toggleDockers {0}; KisAction *toggleDockerTitleBars {0}; KisAction *toggleDetachCanvas {0}; KisAction *fullScreenMode {0}; KisAction *showSessionManager {0}; KisAction *expandingSpacers[2]; KActionMenu *dockWidgetMenu; KActionMenu *windowMenu; KActionMenu *documentMenu; KActionMenu *workspaceMenu; KHelpMenu *helpMenu {0}; KRecentFilesAction *recentFiles {0}; KoResourceModel *workspacemodel {0}; QScopedPointer undoActionsUpdateManager; QString lastExportLocation; QMap dockWidgetsMap; QByteArray dockerStateBeforeHiding; KoToolDocker *toolOptionsDocker {0}; QCloseEvent *deferredClosingEvent {0}; Digikam::ThemeManager *themeManager {0}; QScrollArea *welcomeScroller {0}; KisWelcomePageWidget *welcomePage {0}; QStackedWidget *widgetStack {0}; QMdiArea *mdiArea; QMdiSubWindow *activeSubWindow {0}; QSignalMapper *windowMapper; QSignalMapper *documentMapper; KisCanvasWindow *canvasWindow {0}; QByteArray lastExportedFormat; QScopedPointer > tabSwitchCompressor; QMutex savingEntryMutex; KConfigGroup windowStateConfig; QUuid workspaceBorrowedBy; KisSignalAutoConnectionsStore screenConnectionsStore; #ifdef Q_OS_ANDROID KisAndroidFileManager *fileManager; #endif KisActionManager * actionManager() { return viewManager->actionManager(); } QTabBar* findTabBarHACK() { QObjectList objects = mdiArea->children(); Q_FOREACH (QObject *object, objects) { QTabBar *bar = qobject_cast(object); if (bar) { return bar; } } return 0; } }; KisMainWindow::KisMainWindow(QUuid uuid) : KXmlGuiWindow() , d(new Private(this, uuid)) { auto rserver = KisResourceServerProvider::instance()->workspaceServer(); QSharedPointer adapter(new KoResourceServerAdapter(rserver)); d->workspacemodel = new KoResourceModel(adapter, this); connect(d->workspacemodel, &KoResourceModel::afterResourcesLayoutReset, this, [&]() { updateWindowMenu(); }); d->viewManager = new KisViewManager(this, actionCollection()); KConfigGroup group( KSharedConfig::openConfig(), "theme"); d->themeManager = new Digikam::ThemeManager(group.readEntry("Theme", "Krita dark"), this); d->windowStateConfig = KSharedConfig::openConfig()->group("MainWindow"); setStandardToolBarMenuEnabled(true); setTabPosition(Qt::AllDockWidgetAreas, QTabWidget::North); setDockNestingEnabled(true); qApp->setStartDragDistance(25); // 25 px is a distance that works well for Tablet and Mouse events #ifdef Q_OS_MACOS setUnifiedTitleAndToolBarOnMac(true); #endif connect(this, SIGNAL(restoringDone()), this, SLOT(forceDockTabFonts())); connect(this, SIGNAL(themeChanged()), d->viewManager, SLOT(updateIcons())); connect(KisPart::instance(), SIGNAL(documentClosed(QString)), SLOT(updateWindowMenu())); connect(KisPart::instance(), SIGNAL(documentOpened(QString)), SLOT(updateWindowMenu())); connect(KisConfigNotifier::instance(), SIGNAL(configChanged()), this, SLOT(configChanged())); actionCollection()->addAssociatedWidget(this); KoPluginLoader::instance()->load("Krita/ViewPlugin", "Type == 'Service' and ([X-Krita-Version] == 28)", KoPluginLoader::PluginsConfig(), d->viewManager, false); // Load the per-application plugins (Right now, only Python) We do this only once, when the first mainwindow is being created. KoPluginLoader::instance()->load("Krita/ApplicationPlugin", "Type == 'Service' and ([X-Krita-Version] == 28)", KoPluginLoader::PluginsConfig(), qApp, true); KoToolBoxFactory toolBoxFactory; QDockWidget *toolbox = createDockWidget(&toolBoxFactory); toolbox->setFeatures(QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetFloatable | QDockWidget::DockWidgetClosable); KisConfig cfg(true); if (cfg.toolOptionsInDocker()) { ToolDockerFactory toolDockerFactory; d->toolOptionsDocker = qobject_cast(createDockWidget(&toolDockerFactory)); d->toolOptionsDocker->toggleViewAction()->setEnabled(true); } QMap dockwidgetActions; dockwidgetActions[toolbox->toggleViewAction()->text()] = toolbox->toggleViewAction(); Q_FOREACH (const QString & docker, KoDockRegistry::instance()->keys()) { KoDockFactoryBase *factory = KoDockRegistry::instance()->value(docker); QDockWidget *dw = createDockWidget(factory); dockwidgetActions[dw->toggleViewAction()->text()] = dw->toggleViewAction(); } if (d->toolOptionsDocker) { dockwidgetActions[d->toolOptionsDocker->toggleViewAction()->text()] = d->toolOptionsDocker->toggleViewAction(); } connect(KoToolManager::instance(), SIGNAL(toolOptionWidgetsChanged(KoCanvasController*,QList >)), this, SLOT(newOptionWidgets(KoCanvasController*,QList >))); Q_FOREACH (QString title, dockwidgetActions.keys()) { d->dockWidgetMenu->addAction(dockwidgetActions[title]); } Q_FOREACH (QDockWidget *wdg, dockWidgets()) { if ((wdg->features() & QDockWidget::DockWidgetClosable) == 0) { wdg->setVisible(true); } } Q_FOREACH (KoCanvasObserverBase* observer, canvasObservers()) { observer->setObservedCanvas(0); KisMainwindowObserver* mainwindowObserver = dynamic_cast(observer); if (mainwindowObserver) { mainwindowObserver->setViewManager(d->viewManager); } } // Load all the actions from the tool plugins Q_FOREACH(KoToolFactoryBase *toolFactory, KoToolRegistry::instance()->values()) { toolFactory->createActions(actionCollection()); } d->mdiArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded); d->mdiArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); d->mdiArea->setTabPosition(QTabWidget::North); d->mdiArea->setTabsClosable(true); // Tab close button override // Windows just has a black X, and Ubuntu has a dark x that is hard to read // just switch this icon out for all OSs so it is easier to see d->mdiArea->setStyleSheet("QTabBar::close-button { image: url(:/pics/broken-preset.png) }"); setCentralWidget(d->widgetStack); d->widgetStack->setCurrentIndex(0); connect(d->mdiArea, SIGNAL(subWindowActivated(QMdiSubWindow*)), this, SLOT(subWindowActivated())); connect(d->windowMapper, SIGNAL(mapped(QWidget*)), this, SLOT(setActiveSubWindow(QWidget*))); connect(d->documentMapper, SIGNAL(mapped(QObject*)), this, SLOT(newView(QObject*))); d->canvasWindow = new KisCanvasWindow(this); actionCollection()->addAssociatedWidget(d->canvasWindow); createActions(); // the welcome screen needs to grab actions...so make sure this line goes after the createAction() so they exist d->welcomePage->setMainWindow(this); setAutoSaveSettings(d->windowStateConfig, false); subWindowActivated(); updateWindowMenu(); if (isHelpMenuEnabled() && !d->helpMenu) { // workaround for KHelpMenu (or rather KAboutData::applicationData()) internally // not using the Q*Application metadata ATM, which results e.g. in the bugreport wizard // not having the app version preset // fixed hopefully in KF5 5.22.0, patch pending QGuiApplication *app = qApp; KAboutData aboutData(app->applicationName(), app->applicationDisplayName(), app->applicationVersion()); aboutData.setOrganizationDomain(app->organizationDomain().toUtf8()); d->helpMenu = new KHelpMenu(this, aboutData, false); // workaround-less version: // d->helpMenu = new KHelpMenu(this, QString()/*unused*/, false); // The difference between using KActionCollection->addAction() is that // these actions do not get tied to the MainWindow. What does this all do? KActionCollection *actions = d->viewManager->actionCollection(); QAction *helpContentsAction = d->helpMenu->action(KHelpMenu::menuHelpContents); QAction *whatsThisAction = d->helpMenu->action(KHelpMenu::menuWhatsThis); QAction *reportBugAction = d->helpMenu->action(KHelpMenu::menuReportBug); QAction *switchLanguageAction = d->helpMenu->action(KHelpMenu::menuSwitchLanguage); QAction *aboutAppAction = d->helpMenu->action(KHelpMenu::menuAboutApp); QAction *aboutKdeAction = d->helpMenu->action(KHelpMenu::menuAboutKDE); if (helpContentsAction) { actions->addAction(helpContentsAction->objectName(), helpContentsAction); } if (whatsThisAction) { actions->addAction(whatsThisAction->objectName(), whatsThisAction); } if (reportBugAction) { actions->addAction(reportBugAction->objectName(), reportBugAction); } if (switchLanguageAction) { actions->addAction(switchLanguageAction->objectName(), switchLanguageAction); } if (aboutAppAction) { actions->addAction(aboutAppAction->objectName(), aboutAppAction); } if (aboutKdeAction) { actions->addAction(aboutKdeAction->objectName(), aboutKdeAction); } connect(d->helpMenu, SIGNAL(showAboutApplication()), SLOT(showAboutApplication())); } // KDE' libs 4''s help contents action is broken outside kde, for some reason... We can handle it just as easily ourselves QAction *helpAction = actionCollection()->action("help_contents"); helpAction->disconnect(); connect(helpAction, SIGNAL(triggered()), this, SLOT(showManual())); #if 0 //check for colliding shortcuts QSet existingShortcuts; Q_FOREACH (QAction* action, actionCollection()->actions()) { if(action->shortcut() == QKeySequence(0)) { continue; } dbgKrita << "shortcut " << action->text() << " " << action->shortcut(); Q_ASSERT(!existingShortcuts.contains(action->shortcut())); existingShortcuts.insert(action->shortcut()); } #endif configChanged(); // If we have customized the toolbars, load that first setLocalXMLFile(KoResourcePaths::locateLocal("data", "krita4.xmlgui")); setXMLFile(":/kxmlgui5/krita4.xmlgui"); guiFactory()->addClient(this); + connect(guiFactory(), SIGNAL(makingChanges(bool)), SLOT(slotXmlGuiMakingChanges(bool))); // Create and plug toolbar list for Settings menu QList toolbarList; Q_FOREACH (QWidget* it, guiFactory()->containers("ToolBar")) { KToolBar * toolBar = ::qobject_cast(it); toolBar->setMovable(KisConfig(true).readEntry("LockAllDockerPanels", false)); if (toolBar) { if (toolBar->objectName() == "BrushesAndStuff") { toolBar->setEnabled(false); } 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); } else { warnUI << "Toolbar list contains a " << it->metaObject()->className() << " which is not a toolbar!"; } } KToolBar::setToolBarsLocked(KisConfig(true).readEntry("LockAllDockerPanels", false)); plugActionList("toolbarlist", toolbarList); d->toolbarList = toolbarList; applyToolBarLayout(); d->viewManager->updateGUI(); d->viewManager->updateIcons(); QTimer::singleShot(1000, this, SLOT(checkSanity())); { using namespace std::placeholders; // For _1 placeholder std::function callback( std::bind(&KisMainWindow::switchTab, this, _1)); d->tabSwitchCompressor.reset( new KisSignalCompressorWithParam(500, callback, KisSignalCompressor::FIRST_INACTIVE)); } if (cfg.readEntry("CanvasOnlyActive", false)) { QString currentWorkspace = cfg.readEntry("CurrentWorkspace", "Default"); KoResourceServer * rserver = KisResourceServerProvider::instance()->workspaceServer(); KisWorkspaceResource* workspace = rserver->resourceByName(currentWorkspace); if (workspace) { restoreWorkspace(workspace); } cfg.writeEntry("CanvasOnlyActive", false); menuBar()->setVisible(true); } this->winId(); // Ensures the native window has been created. QWindow *window = this->windowHandle(); connect(window, SIGNAL(screenChanged(QScreen *)), this, SLOT(windowScreenChanged(QScreen *))); } KisMainWindow::~KisMainWindow() { // Q_FOREACH (QAction *ac, actionCollection()->actions()) { // QAction *action = qobject_cast(ac); // if (action) { // qDebug() << "", "").replace("", "") // << "\n\ticonText=" << action->iconText().replace("&", "&") // << "\n\tshortcut=" << action->shortcut().toString() // << "\n\tisCheckable=" << QString((action->isChecked() ? "true" : "false")) // << "\n\tstatusTip=" << action->statusTip() // << "\n/>\n" ; // } // else { // dbgKrita << "Got a non-qaction:" << ac->objectName(); // } // } // The doc and view might still exist (this is the case when closing the window) KisPart::instance()->removeMainWindow(this); delete d->viewManager; delete d; } QUuid KisMainWindow::id() const { return d->id; } void KisMainWindow::addView(KisView *view) { if (d->activeView == view) return; if (d->activeView) { d->activeView->disconnect(this); } // register the newly created view in the input manager viewManager()->inputManager()->addTrackedCanvas(view->canvasBase()); showView(view); updateCaption(); emit restoringDone(); if (d->activeView) { connect(d->activeView, SIGNAL(titleModified(QString,bool)), SLOT(slotDocumentTitleModified())); connect(d->viewManager->statusBar(), SIGNAL(memoryStatusUpdated()), this, SLOT(updateCaption())); } } void KisMainWindow::notifyChildViewDestroyed(KisView *view) { /** * If we are the last view of the window, Qt will not activate another tab * before destroying tab/window. In ths case we should clear oll the dangling * pointers manually by setting the current view to null */ viewManager()->inputManager()->removeTrackedCanvas(view->canvasBase()); if (view->canvasBase() == viewManager()->canvasBase()) { viewManager()->setCurrentView(0); } } void KisMainWindow::showView(KisView *imageView) { if (imageView && activeView() != imageView) { // XXX: find a better way to initialize this! imageView->setViewManager(d->viewManager); imageView->canvasBase()->setFavoriteResourceManager(d->viewManager->paintOpBox()->favoriteResourcesManager()); imageView->slotLoadingFinished(); QMdiSubWindow *subwin = d->mdiArea->addSubWindow(imageView); imageView->setSubWindow(subwin); subwin->setAttribute(Qt::WA_DeleteOnClose, true); connect(subwin, SIGNAL(destroyed()), SLOT(updateWindowMenu())); KisConfig cfg(true); subwin->setOption(QMdiSubWindow::RubberBandMove, cfg.readEntry("mdi_rubberband", cfg.useOpenGL())); subwin->setOption(QMdiSubWindow::RubberBandResize, cfg.readEntry("mdi_rubberband", cfg.useOpenGL())); subwin->setWindowIcon(qApp->windowIcon()); /** * Hack alert! * * Here we explicitly request KoToolManager to emit all the tool * activation signals, to reinitialize the tool options docker. * * That is needed due to a design flaw we have in the * initialization procedure. The tool in the KoToolManager is * initialized in KisView::setViewManager() calls, which * happens early enough. During this call the tool manager * requests KoCanvasControllerWidget to emit the signal to * update the widgets in the tool docker. *But* at that moment * of time the view is not yet connected to the main window, * because it happens in KisViewManager::setCurrentView a bit * later. This fact makes the widgets updating signals be lost * and never reach the tool docker. * * So here we just explicitly call the tool activation stub. */ KoToolManager::instance()->initializeCurrentToolForCanvas(); if (d->mdiArea->subWindowList().size() == 1) { imageView->showMaximized(); } else { imageView->show(); } // No, no, no: do not try to call this _before_ the show() has // been called on the view; only when that has happened is the // opengl context active, and very bad things happen if we tell // the dockers to update themselves with a view if the opengl // context is not active. setActiveView(imageView); updateWindowMenu(); updateCaption(); } } void KisMainWindow::slotPreferences() { if (KisDlgPreferences::editPreferences()) { KisConfigNotifier::instance()->notifyConfigChanged(); KisConfigNotifier::instance()->notifyPixelGridModeChanged(); KisImageConfigNotifier::instance()->notifyConfigChanged(); // XXX: should this be changed for the views in other windows as well? Q_FOREACH (QPointer koview, KisPart::instance()->views()) { KisViewManager *view = qobject_cast(koview); if (view) { // Update the settings for all nodes -- they don't query // KisConfig directly because they need the settings during // compositing, and they don't connect to the config notifier // because nodes are not QObjects (because only one base class // can be a QObject). KisNode* node = dynamic_cast(view->image()->rootLayer().data()); node->updateSettings(); } } updateWindowMenu(); d->viewManager->showHideScrollbars(); } } void KisMainWindow::slotThemeChanged() { // save theme changes instantly KConfigGroup group( KSharedConfig::openConfig(), "theme"); group.writeEntry("Theme", d->themeManager->currentThemeName()); // reload action icons! Q_FOREACH (QAction *action, actionCollection()->actions()) { KisIconUtils::updateIcon(action); } if (d->mdiArea) { d->mdiArea->setPalette(qApp->palette()); for (int i=0; imdiArea->subWindowList().size(); i++) { QMdiSubWindow *window = d->mdiArea->subWindowList().at(i); if (window) { window->setPalette(qApp->palette()); KisView *view = qobject_cast(window->widget()); if (view) { view->slotThemeChanged(qApp->palette()); } } } } emit themeChanged(); } bool KisMainWindow::canvasDetached() const { return centralWidget() != d->widgetStack; } void KisMainWindow::setCanvasDetached(bool detach) { if (detach == canvasDetached()) return; QWidget *outgoingWidget = centralWidget() ? takeCentralWidget() : nullptr; QWidget *incomingWidget = d->canvasWindow->swapMainWidget(outgoingWidget); if (incomingWidget) { setCentralWidget(incomingWidget); } if (detach) { d->canvasWindow->show(); } else { d->canvasWindow->hide(); } } void KisMainWindow::slotFileSelected(QString path) { QString url = path; if (!url.isEmpty()) { bool res = openDocument(QUrl::fromLocalFile(url), Import); if (!res) { warnKrita << "Loading" << url << "failed"; } } } void KisMainWindow::slotEmptyFilePath() { QMessageBox::critical(0, i18nc("@title:window", "Krita"), i18n("The chosen file's location could not be found. Does it exist?")); } QWidget * KisMainWindow::canvasWindow() const { return d->canvasWindow; } void KisMainWindow::updateReloadFileAction(KisDocument *doc) { Q_UNUSED(doc); // d->reloadFile->setEnabled(doc && !doc->url().isEmpty()); } void KisMainWindow::setReadWrite(bool readwrite) { d->saveAction->setEnabled(readwrite); d->importFile->setEnabled(readwrite); d->readOnly = !readwrite; updateCaption(); } void KisMainWindow::addRecentURL(const QUrl &url) { // Add entry to recent documents list // (call coming from KisDocument because it must work with cmd line, template dlg, file/open, etc.) if (!url.isEmpty()) { bool ok = true; if (url.isLocalFile()) { QString path = url.adjusted(QUrl::StripTrailingSlash).toLocalFile(); const QStringList tmpDirs = KoResourcePaths::resourceDirs("tmp"); for (QStringList::ConstIterator it = tmpDirs.begin() ; ok && it != tmpDirs.end() ; ++it) { if (path.contains(*it)) { ok = false; // it's in the tmp resource } } const QStringList templateDirs = KoResourcePaths::findDirs("templates"); for (QStringList::ConstIterator it = templateDirs.begin() ; ok && it != templateDirs.end() ; ++it) { if (path.contains(*it)) { ok = false; // it's in the templates directory. break; } } } if (ok) { d->recentFiles->addUrl(url); } saveRecentFiles(); } } void KisMainWindow::saveRecentFiles() { // Save list of recent files KSharedConfigPtr config = KSharedConfig::openConfig(); d->recentFiles->saveEntries(config->group("RecentFiles")); config->sync(); // Tell all windows to reload their list, after saving // Doesn't work multi-process, but it's a start Q_FOREACH (KisMainWindow *mw, KisPart::instance()->mainWindows()) { if (mw != this) { mw->reloadRecentFileList(); } } } QList KisMainWindow::recentFilesUrls() { return d->recentFiles->urls(); } void KisMainWindow::clearRecentFiles() { d->recentFiles->clear(); d->welcomePage->slotClearRecentFiles(); } void KisMainWindow::removeRecentUrl(const QUrl &url) { d->recentFiles->removeUrl(url); KSharedConfigPtr config = KSharedConfig::openConfig(); d->recentFiles->saveEntries(config->group("RecentFiles")); config->sync(); } void KisMainWindow::reloadRecentFileList() { d->recentFiles->loadEntries(KSharedConfig::openConfig()->group("RecentFiles")); } void KisMainWindow::updateCaption() { if (!d->mdiArea->activeSubWindow()) { updateCaption(QString(), false); } else if (d->activeView && d->activeView->document() && d->activeView->image()){ KisDocument *doc = d->activeView->document(); QString caption(doc->caption()); if (d->readOnly) { caption += " [" + i18n("Write Protected") + "] "; } if (doc->isRecovered()) { caption += " [" + i18n("Recovered") + "] "; } // show the file size for the document KisMemoryStatisticsServer::Statistics m_fileSizeStats = KisMemoryStatisticsServer::instance()->fetchMemoryStatistics(d->activeView ? d->activeView->image() : 0); if (m_fileSizeStats.imageSize) { caption += QString(" (").append( KFormat().formatByteSize(m_fileSizeStats.imageSize)).append( ")"); } updateCaption(caption, doc->isModified()); if (!doc->url().fileName().isEmpty()) { d->saveAction->setToolTip(i18n("Save as %1", doc->url().fileName())); } else { d->saveAction->setToolTip(i18n("Save")); } } } void KisMainWindow::updateCaption(const QString &caption, bool modified) { QString versionString = KritaVersionWrapper::versionString(true); QString title = caption; if (!title.contains(QStringLiteral("[*]"))) { // append the placeholder so that the modified mechanism works title.append(QStringLiteral(" [*]")); } if (d->mdiArea->activeSubWindow()) { #if defined(KRITA_ALPHA) || defined (KRITA_BETA) || defined (KRITA_RC) d->mdiArea->activeSubWindow()->setWindowTitle(QString("%1: %2").arg(versionString).arg(title)); #else d->mdiArea->activeSubWindow()->setWindowTitle(title); #endif d->mdiArea->activeSubWindow()->setWindowModified(modified); } else { #if defined(KRITA_ALPHA) || defined (KRITA_BETA) || defined (KRITA_RC) setWindowTitle(QString("%1: %2").arg(versionString).arg(title)); #else setWindowTitle(title); #endif } setWindowModified(modified); } KisView *KisMainWindow::activeView() const { if (d->activeView) { return d->activeView; } return 0; } bool KisMainWindow::openDocument(const QUrl &url, OpenFlags flags) { if (!QFile(url.toLocalFile()).exists()) { if (!(flags & BatchMode)) { QMessageBox::critical(0, i18nc("@title:window", "Krita"), i18n("The file %1 does not exist.", url.url())); } d->recentFiles->removeUrl(url); //remove the file from the recent-opened-file-list saveRecentFiles(); return false; } return openDocumentInternal(url, flags); } bool KisMainWindow::openDocumentInternal(const QUrl &url, OpenFlags flags) { if (!url.isLocalFile()) { qWarning() << "KisMainWindow::openDocumentInternal. Not a local file:" << url; return false; } KisDocument *newdoc = KisPart::instance()->createDocument(); if (flags & BatchMode) { newdoc->setFileBatchMode(true); } d->firstTime = true; connect(newdoc, SIGNAL(completed()), this, SLOT(slotLoadCompleted())); connect(newdoc, SIGNAL(canceled(QString)), this, SLOT(slotLoadCanceled(QString))); KisDocument::OpenFlags openFlags = KisDocument::None; if (flags & RecoveryFile) { openFlags |= KisDocument::RecoveryFile; } bool openRet = !(flags & Import) ? newdoc->openUrl(url, openFlags) : newdoc->importDocument(url); if (!openRet) { delete newdoc; return false; } KisPart::instance()->addDocument(newdoc); updateReloadFileAction(newdoc); if (!QFileInfo(url.toLocalFile()).isWritable()) { setReadWrite(false); } return true; } void KisMainWindow::showDocument(KisDocument *document) { Q_FOREACH(QMdiSubWindow *subwindow, d->mdiArea->subWindowList()) { KisView *view = qobject_cast(subwindow->widget()); KIS_SAFE_ASSERT_RECOVER_NOOP(view); if (view) { if (view->document() == document) { setActiveSubWindow(subwindow); return; } } } addViewAndNotifyLoadingCompleted(document); } KisView* KisMainWindow::addViewAndNotifyLoadingCompleted(KisDocument *document) { showWelcomeScreen(false); // see workaround in function header KisView *view = KisPart::instance()->createView(document, d->viewManager, this); addView(view); emit guiLoadingFinished(); return view; } QStringList KisMainWindow::showOpenFileDialog(bool isImporting) { KoFileDialog dialog(this, KoFileDialog::ImportFiles, "OpenDocument"); dialog.setDefaultDir(QStandardPaths::writableLocation(QStandardPaths::PicturesLocation)); dialog.setMimeTypeFilters(KisImportExportManager::supportedMimeTypes(KisImportExportManager::Import)); dialog.setCaption(isImporting ? i18n("Import Images") : i18n("Open Images")); return dialog.filenames(); } // Separate from openDocument to handle async loading (remote URLs) void KisMainWindow::slotLoadCompleted() { KisDocument *newdoc = qobject_cast(sender()); if (newdoc && newdoc->image()) { addViewAndNotifyLoadingCompleted(newdoc); disconnect(newdoc, SIGNAL(completed()), this, SLOT(slotLoadCompleted())); disconnect(newdoc, SIGNAL(canceled(QString)), this, SLOT(slotLoadCanceled(QString))); emit loadCompleted(); } } void KisMainWindow::slotLoadCanceled(const QString & errMsg) { dbgUI << "KisMainWindow::slotLoadCanceled"; if (!errMsg.isEmpty()) // empty when canceled by user QMessageBox::critical(this, i18nc("@title:window", "Krita"), errMsg); // ... can't delete the document, it's the one who emitted the signal... KisDocument* doc = qobject_cast(sender()); Q_ASSERT(doc); disconnect(doc, SIGNAL(completed()), this, SLOT(slotLoadCompleted())); disconnect(doc, SIGNAL(canceled(QString)), this, SLOT(slotLoadCanceled(QString))); } void KisMainWindow::slotSaveCanceled(const QString &errMsg) { dbgUI << "KisMainWindow::slotSaveCanceled"; if (!errMsg.isEmpty()) // empty when canceled by user QMessageBox::critical(this, i18nc("@title:window", "Krita"), errMsg); slotSaveCompleted(); } void KisMainWindow::slotSaveCompleted() { dbgUI << "KisMainWindow::slotSaveCompleted"; KisDocument* doc = qobject_cast(sender()); Q_ASSERT(doc); disconnect(doc, SIGNAL(completed()), this, SLOT(slotSaveCompleted())); disconnect(doc, SIGNAL(canceled(QString)), this, SLOT(slotSaveCanceled(QString))); if (d->deferredClosingEvent) { KXmlGuiWindow::closeEvent(d->deferredClosingEvent); } } bool KisMainWindow::hackIsSaving() const { StdLockableWrapper wrapper(&d->savingEntryMutex); std::unique_lock> l(wrapper, std::try_to_lock); return !l.owns_lock(); } bool KisMainWindow::installBundle(const QString &fileName) const { QFileInfo from(fileName); QFileInfo to(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/bundles/" + from.fileName()); if (to.exists()) { QFile::remove(to.canonicalFilePath()); } return QFile::copy(fileName, QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/bundles/" + from.fileName()); } bool KisMainWindow::saveDocument(KisDocument *document, bool saveas, bool isExporting) { if (!document) { return true; } /** * Make sure that we cannot enter this method twice! * * The lower level functions may call processEvents() so * double-entry is quite possible to achieve. Here we try to lock * the mutex, and if it is failed, just cancel saving. */ StdLockableWrapper wrapper(&d->savingEntryMutex); std::unique_lock> l(wrapper, std::try_to_lock); if (!l.owns_lock()) return false; // no busy wait for saving because it is dangerous! KisDelayedSaveDialog dlg(document->image(), KisDelayedSaveDialog::SaveDialog, 0, this); dlg.blockIfImageIsBusy(); if (dlg.result() == KisDelayedSaveDialog::Rejected) { return false; } else if (dlg.result() == KisDelayedSaveDialog::Ignored) { QMessageBox::critical(0, i18nc("@title:window", "Krita"), i18n("You are saving a file while the image is " "still rendering. The saved file may be " "incomplete or corrupted.\n\n" "Please select a location where the original " "file will not be overridden!")); saveas = true; } if (document->isRecovered()) { saveas = true; } if (document->url().isEmpty()) { saveas = true; } connect(document, SIGNAL(completed()), this, SLOT(slotSaveCompleted())); connect(document, SIGNAL(canceled(QString)), this, SLOT(slotSaveCanceled(QString))); QByteArray nativeFormat = document->nativeFormatMimeType(); QByteArray oldMimeFormat = document->mimeType(); QUrl suggestedURL = document->url(); QStringList mimeFilter = KisImportExportManager::supportedMimeTypes(KisImportExportManager::Export); mimeFilter = KisImportExportManager::supportedMimeTypes(KisImportExportManager::Export); if (!mimeFilter.contains(oldMimeFormat)) { dbgUI << "KisMainWindow::saveDocument no export filter for" << oldMimeFormat; // --- don't setOutputMimeType in case the user cancels the Save As // dialog and then tries to just plain Save --- // suggest a different filename extension (yes, we fortunately don't all live in a world of magic :)) QString suggestedFilename = QFileInfo(suggestedURL.toLocalFile()).completeBaseName(); if (!suggestedFilename.isEmpty()) { // ".kra" looks strange for a name suggestedFilename = suggestedFilename + "." + KisMimeDatabase::suffixesForMimeType(KIS_MIME_TYPE).first(); suggestedURL = suggestedURL.adjusted(QUrl::RemoveFilename); suggestedURL.setPath(suggestedURL.path() + suggestedFilename); } // force the user to choose outputMimeType saveas = true; } bool ret = false; if (document->url().isEmpty() || isExporting || saveas) { // if you're just File/Save As'ing to change filter options you // don't want to be reminded about overwriting files etc. bool justChangingFilterOptions = false; KoFileDialog dialog(this, KoFileDialog::SaveFile, "SaveAs"); dialog.setCaption(isExporting ? i18n("Exporting") : i18n("Saving As")); //qDebug() << ">>>>>" << isExporting << d->lastExportLocation << d->lastExportedFormat << QString::fromLatin1(document->mimeType()); if (isExporting && !d->lastExportLocation.isEmpty() && !d->lastExportLocation.contains(QDir::tempPath())) { // Use the location where we last exported to, if it's set, as the opening location for the file dialog QString proposedPath = QFileInfo(d->lastExportLocation).absolutePath(); // If the document doesn't have a filename yet, use the title QString proposedFileName = suggestedURL.isEmpty() ? document->documentInfo()->aboutInfo("title") : QFileInfo(suggestedURL.toLocalFile()).completeBaseName(); // Use the last mimetype we exported to by default QString proposedMimeType = d->lastExportedFormat.isEmpty() ? "" : d->lastExportedFormat; QString proposedExtension = KisMimeDatabase::suffixesForMimeType(proposedMimeType).first().remove("*,"); // Set the default dir: this overrides the one loaded from the config file, since we're exporting and the lastExportLocation is not empty dialog.setDefaultDir(proposedPath + "/" + proposedFileName + "." + proposedExtension, true); dialog.setMimeTypeFilters(mimeFilter, proposedMimeType); } else { // Get the last used location for saving KConfigGroup group = KSharedConfig::openConfig()->group("File Dialogs"); QString proposedPath = group.readEntry("SaveAs", ""); // if that is empty, get the last used location for loading if (proposedPath.isEmpty()) { proposedPath = group.readEntry("OpenDocument", ""); } // If that is empty, too, use the Pictures location. if (proposedPath.isEmpty()) { proposedPath = QStandardPaths::writableLocation(QStandardPaths::PicturesLocation); } // But only use that if the suggestedUrl, that is, the document's own url is empty, otherwise // open the location where the document currently is. dialog.setDefaultDir(suggestedURL.isEmpty() ? proposedPath : suggestedURL.toLocalFile(), true); // If exporting, default to all supported file types if user is exporting QByteArray default_mime_type = ""; if (!isExporting) { // otherwise use the document's mimetype, or if that is empty, kra, which is the savest. default_mime_type = document->mimeType().isEmpty() ? nativeFormat : document->mimeType(); } dialog.setMimeTypeFilters(mimeFilter, QString::fromLatin1(default_mime_type)); } QUrl newURL = QUrl::fromUserInput(dialog.filename()); if (newURL.isLocalFile()) { QString fn = newURL.toLocalFile(); if (QFileInfo(fn).completeSuffix().isEmpty()) { fn.append(KisMimeDatabase::suffixesForMimeType(nativeFormat).first()); newURL = QUrl::fromLocalFile(fn); } } if (document->documentInfo()->aboutInfo("title") == i18n("Unnamed")) { QString fn = newURL.toLocalFile(); QFileInfo info(fn); document->documentInfo()->setAboutInfo("title", info.completeBaseName()); } QByteArray outputFormat = nativeFormat; QString outputFormatString = KisMimeDatabase::mimeTypeForFile(newURL.toLocalFile(), false); outputFormat = outputFormatString.toLatin1(); if (!isExporting) { justChangingFilterOptions = (newURL == document->url()) && (outputFormat == document->mimeType()); } else { QString path = QFileInfo(d->lastExportLocation).absolutePath(); QString filename = QFileInfo(document->url().toLocalFile()).completeBaseName(); justChangingFilterOptions = (QFileInfo(newURL.toLocalFile()).absolutePath() == path) && (QFileInfo(newURL.toLocalFile()).completeBaseName() == filename) && (outputFormat == d->lastExportedFormat); } bool bOk = true; if (newURL.isEmpty()) { bOk = false; } if (bOk) { bool wantToSave = true; // don't change this line unless you know what you're doing :) if (!justChangingFilterOptions) { if (!document->isNativeFormat(outputFormat)) wantToSave = true; } if (wantToSave) { if (!isExporting) { // Save As ret = document->saveAs(newURL, outputFormat, true); if (ret) { dbgUI << "Successful Save As!"; KisPart::instance()->addRecentURLToAllMainWindows(newURL); setReadWrite(true); } else { dbgUI << "Failed Save As!"; } } else { // Export ret = document->exportDocument(newURL, outputFormat); if (ret) { d->lastExportLocation = newURL.toLocalFile(); d->lastExportedFormat = outputFormat; } } } // if (wantToSave) { else ret = false; } // if (bOk) { else ret = false; } else { // saving // We cannot "export" into the currently // opened document. We are not Gimp. KIS_ASSERT_RECOVER_NOOP(!isExporting); // be sure document has the correct outputMimeType! if (document->isModified()) { ret = document->save(true, 0); } if (!ret) { dbgUI << "Failed Save!"; } } updateReloadFileAction(document); updateCaption(); return ret; } void KisMainWindow::undo() { if (activeView()) { activeView()->document()->undoStack()->undo(); } } void KisMainWindow::redo() { if (activeView()) { activeView()->document()->undoStack()->redo(); } } void KisMainWindow::closeEvent(QCloseEvent *e) { if (hackIsSaving()) { e->setAccepted(false); return; } if (!KisPart::instance()->closingSession()) { QAction *action= d->viewManager->actionCollection()->action("view_show_canvas_only"); if ((action) && (action->isChecked())) { action->setChecked(false); } // Save session when last window is closed if (KisPart::instance()->mainwindowCount() == 1) { bool closeAllowed = KisPart::instance()->closeSession(); if (!closeAllowed) { e->setAccepted(false); return; } } } d->mdiArea->closeAllSubWindows(); QList childrenList = d->mdiArea->subWindowList(); if (childrenList.isEmpty()) { d->deferredClosingEvent = e; saveWindowState(true); d->canvasWindow->close(); } else { e->setAccepted(false); } } void KisMainWindow::saveWindowSettings() { KSharedConfigPtr config = KSharedConfig::openConfig(); if (d->windowSizeDirty ) { dbgUI << "KisMainWindow::saveWindowSettings"; KConfigGroup group = d->windowStateConfig; KWindowConfig::saveWindowSize(windowHandle(), group); config->sync(); d->windowSizeDirty = false; } if (!d->activeView || d->activeView->document()) { // Save toolbar position into the config file of the app, under the doc's component name KConfigGroup group = d->windowStateConfig; saveMainWindowSettings(group); // Save collapsible state of dock widgets for (QMap::const_iterator i = d->dockWidgetsMap.constBegin(); i != d->dockWidgetsMap.constEnd(); ++i) { if (i.value()->widget()) { KConfigGroup dockGroup = group.group(QString("DockWidget ") + i.key()); dockGroup.writeEntry("Collapsed", i.value()->widget()->isHidden()); dockGroup.writeEntry("Locked", i.value()->property("Locked").toBool()); dockGroup.writeEntry("DockArea", (int) dockWidgetArea(i.value())); dockGroup.writeEntry("xPosition", (int) i.value()->widget()->x()); dockGroup.writeEntry("yPosition", (int) i.value()->widget()->y()); dockGroup.writeEntry("width", (int) i.value()->widget()->width()); dockGroup.writeEntry("height", (int) i.value()->widget()->height()); } } } KSharedConfig::openConfig()->sync(); resetAutoSaveSettings(); // Don't let KMainWindow override the good stuff we wrote down } void KisMainWindow::resizeEvent(QResizeEvent * e) { d->windowSizeDirty = true; KXmlGuiWindow::resizeEvent(e); } void KisMainWindow::setActiveView(KisView* view) { d->activeView = view; updateCaption(); if (d->undoActionsUpdateManager) { d->undoActionsUpdateManager->setCurrentDocument(view ? view->document() : 0); } d->viewManager->setCurrentView(view); KisWindowLayoutManager::instance()->activeDocumentChanged(view->document()); } void KisMainWindow::dragMove(QDragMoveEvent * event) { QTabBar *tabBar = d->findTabBarHACK(); if (!tabBar && d->mdiArea->viewMode() == QMdiArea::TabbedView) { qWarning() << "WARNING!!! Cannot find QTabBar in the main window! Looks like Qt has changed behavior. Drag & Drop between multiple tabs might not work properly (tabs will not switch automatically)!"; } if (tabBar && tabBar->isVisible()) { QPoint pos = tabBar->mapFromGlobal(mapToGlobal(event->pos())); if (tabBar->rect().contains(pos)) { const int tabIndex = tabBar->tabAt(pos); if (tabIndex >= 0 && tabBar->currentIndex() != tabIndex) { d->tabSwitchCompressor->start(tabIndex); } } else if (d->tabSwitchCompressor->isActive()) { d->tabSwitchCompressor->stop(); } } } void KisMainWindow::dragLeave() { if (d->tabSwitchCompressor->isActive()) { d->tabSwitchCompressor->stop(); } } void KisMainWindow::switchTab(int index) { QTabBar *tabBar = d->findTabBarHACK(); if (!tabBar) return; tabBar->setCurrentIndex(index); } void KisMainWindow::showWelcomeScreen(bool show) { d->widgetStack->setCurrentIndex(!show); } void KisMainWindow::slotFileNew() { const QStringList mimeFilter = KisImportExportManager::supportedMimeTypes(KisImportExportManager::Import); KisOpenPane *startupWidget = new KisOpenPane(this, mimeFilter, QStringLiteral("templates/")); startupWidget->setWindowModality(Qt::WindowModal); startupWidget->setWindowTitle(i18n("Create new document")); KisConfig cfg(true); int w = cfg.defImageWidth(); int h = cfg.defImageHeight(); const double resolution = cfg.defImageResolution(); const QString colorModel = cfg.defColorModel(); const QString colorDepth = cfg.defaultColorDepth(); const QString colorProfile = cfg.defColorProfile(); CustomDocumentWidgetItem item; item.widget = new KisCustomImageWidget(startupWidget, w, h, resolution, colorModel, colorDepth, colorProfile, i18n("Unnamed")); item.icon = "document-new"; item.title = i18n("Custom Document"); startupWidget->addCustomDocumentWidget(item.widget, item.title, "Custom Document", item.icon); QSize sz = KisClipboard::instance()->clipSize(); if (sz.isValid() && sz.width() != 0 && sz.height() != 0) { w = sz.width(); h = sz.height(); } item.widget = new KisImageFromClipboard(startupWidget, w, h, resolution, colorModel, colorDepth, colorProfile, i18n("Unnamed")); item.title = i18n("Create from Clipboard"); item.icon = "tab-new"; startupWidget->addCustomDocumentWidget(item.widget, item.title, "Create from ClipBoard", item.icon); // calls deleteLater connect(startupWidget, SIGNAL(documentSelected(KisDocument*)), KisPart::instance(), SLOT(startCustomDocument(KisDocument*))); // calls deleteLater connect(startupWidget, SIGNAL(openTemplate(QUrl)), KisPart::instance(), SLOT(openTemplate(QUrl))); startupWidget->exec(); // Cancel calls deleteLater... } void KisMainWindow::slotImportFile() { dbgUI << "slotImportFile()"; slotFileOpen(true); } void KisMainWindow::slotFileOpen(bool isImporting) { #ifndef Q_OS_ANDROID QStringList urls = showOpenFileDialog(isImporting); if (urls.isEmpty()) return; Q_FOREACH (const QString& url, urls) { if (!url.isEmpty()) { OpenFlags flags = isImporting ? Import : None; bool res = openDocument(QUrl::fromLocalFile(url), flags); if (!res) { warnKrita << "Loading" << url << "failed"; } } } #else Q_UNUSED(isImporting) d->fileManager->openImportFile(); connect(d->fileManager, SIGNAL(sigFileSelected(QString)), this, SLOT(slotFileSelected(QString))); connect(d->fileManager, SIGNAL(sigEmptyFilePath()), this, SLOT(slotEmptyFilePath())); #endif } void KisMainWindow::slotFileOpenRecent(const QUrl &url) { (void) openDocument(QUrl::fromLocalFile(url.toLocalFile()), None); } void KisMainWindow::slotFileSave() { if (saveDocument(d->activeView->document(), false, false)) { emit documentSaved(); } } void KisMainWindow::slotFileSaveAs() { if (saveDocument(d->activeView->document(), true, false)) { emit documentSaved(); } } void KisMainWindow::slotExportFile() { if (saveDocument(d->activeView->document(), true, true)) { emit documentSaved(); } } void KisMainWindow::slotShowSessionManager() { KisPart::instance()->showSessionManager(); } KoCanvasResourceProvider *KisMainWindow::resourceManager() const { return d->viewManager->canvasResourceProvider()->resourceManager(); } int KisMainWindow::viewCount() const { return d->mdiArea->subWindowList().size(); } const KConfigGroup &KisMainWindow::windowStateConfig() const { return d->windowStateConfig; } void KisMainWindow::saveWindowState(bool restoreNormalState) { if (restoreNormalState) { QAction *showCanvasOnly = d->viewManager->actionCollection()->action("view_show_canvas_only"); if (showCanvasOnly && showCanvasOnly->isChecked()) { showCanvasOnly->setChecked(false); } d->windowStateConfig.writeEntry("ko_geometry", saveGeometry().toBase64()); d->windowStateConfig.writeEntry("State", saveState().toBase64()); if (!d->dockerStateBeforeHiding.isEmpty()) { restoreState(d->dockerStateBeforeHiding); } statusBar()->setVisible(true); menuBar()->setVisible(true); saveWindowSettings(); } else { saveMainWindowSettings(d->windowStateConfig); } } bool KisMainWindow::restoreWorkspaceState(const QByteArray &state) { QByteArray oldState = saveState(); // needed because otherwise the layout isn't correctly restored in some situations Q_FOREACH (QDockWidget *dock, dockWidgets()) { dock->toggleViewAction()->setEnabled(true); dock->hide(); } bool success = KXmlGuiWindow::restoreState(state); if (!success) { KXmlGuiWindow::restoreState(oldState); return false; } return success; } bool KisMainWindow::restoreWorkspace(KisWorkspaceResource *workspace) { bool success = restoreWorkspaceState(workspace->dockerState()); if (activeKisView()) { activeKisView()->resourceProvider()->notifyLoadingWorkspace(workspace); } return success; } QByteArray KisMainWindow::borrowWorkspace(KisMainWindow *other) { QByteArray currentWorkspace = saveState(); if (!d->workspaceBorrowedBy.isNull()) { if (other->id() == d->workspaceBorrowedBy) { // We're swapping our original workspace back d->workspaceBorrowedBy = QUuid(); return currentWorkspace; } else { // Get our original workspace back before swapping with a third window KisMainWindow *borrower = KisPart::instance()->windowById(d->workspaceBorrowedBy); if (borrower) { QByteArray originalLayout = borrower->borrowWorkspace(this); borrower->restoreWorkspaceState(currentWorkspace); d->workspaceBorrowedBy = other->id(); return originalLayout; } } } d->workspaceBorrowedBy = other->id(); return currentWorkspace; } void KisMainWindow::swapWorkspaces(KisMainWindow *a, KisMainWindow *b) { QByteArray workspaceA = a->borrowWorkspace(b); QByteArray workspaceB = b->borrowWorkspace(a); a->restoreWorkspaceState(workspaceB); b->restoreWorkspaceState(workspaceA); } KisViewManager *KisMainWindow::viewManager() const { return d->viewManager; } void KisMainWindow::slotDocumentInfo() { if (!d->activeView->document()) return; KoDocumentInfo *docInfo = d->activeView->document()->documentInfo(); if (!docInfo) return; KoDocumentInfoDlg *dlg = d->activeView->document()->createDocumentInfoDialog(this, docInfo); if (dlg->exec()) { if (dlg->isDocumentSaved()) { d->activeView->document()->setModified(false); } else { d->activeView->document()->setModified(true); } d->activeView->document()->setTitleModified(); } delete dlg; } bool KisMainWindow::slotFileCloseAll() { Q_FOREACH (QMdiSubWindow *subwin, d->mdiArea->subWindowList()) { if (subwin) { if(!subwin->close()) return false; } } updateCaption(); return true; } void KisMainWindow::slotFileQuit() { // Do not close while KisMainWindow has the savingEntryMutex locked, bug409395. // After the background saving job is initiated, KisDocument blocks closing // while it saves itself. if (hackIsSaving()) { return; } KisPart::instance()->closeSession(); } void KisMainWindow::slotFilePrint() { if (!activeView()) return; KisPrintJob *printJob = activeView()->createPrintJob(); if (printJob == 0) return; applyDefaultSettings(printJob->printer()); QPrintDialog *printDialog = activeView()->createPrintDialog( printJob, this ); if (printDialog && printDialog->exec() == QDialog::Accepted) { printJob->printer().setPageMargins(0.0, 0.0, 0.0, 0.0, QPrinter::Point); printJob->printer().setPaperSize(QSizeF(activeView()->image()->width() / (72.0 * activeView()->image()->xRes()), activeView()->image()->height()/ (72.0 * activeView()->image()->yRes())), QPrinter::Inch); printJob->startPrinting(KisPrintJob::DeleteWhenDone); } else { delete printJob; } delete printDialog; } void KisMainWindow::slotFilePrintPreview() { if (!activeView()) return; KisPrintJob *printJob = activeView()->createPrintJob(); if (printJob == 0) return; /* Sets the startPrinting() slot to be blocking. The Qt print-preview dialog requires the printing to be completely blocking and only return when the full document has been printed. By default the KisPrintingDialog is non-blocking and multithreading, setting blocking to true will allow it to be used in the preview dialog */ printJob->setProperty("blocking", true); QPrintPreviewDialog *preview = new QPrintPreviewDialog(&printJob->printer(), this); printJob->setParent(preview); // will take care of deleting the job connect(preview, SIGNAL(paintRequested(QPrinter*)), printJob, SLOT(startPrinting())); preview->exec(); delete preview; } KisPrintJob* KisMainWindow::exportToPdf(QString pdfFileName) { if (!activeView()) return 0; if (!activeView()->document()) return 0; KoPageLayout pageLayout; pageLayout.width = 0; pageLayout.height = 0; pageLayout.topMargin = 0; pageLayout.bottomMargin = 0; pageLayout.leftMargin = 0; pageLayout.rightMargin = 0; if (pdfFileName.isEmpty()) { KConfigGroup group = KSharedConfig::openConfig()->group("File Dialogs"); QString defaultDir = group.readEntry("SavePdfDialog"); if (defaultDir.isEmpty()) defaultDir = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation); QUrl startUrl = QUrl::fromLocalFile(defaultDir); KisDocument* pDoc = d->activeView->document(); /** if document has a file name, take file name and replace extension with .pdf */ if (pDoc && pDoc->url().isValid()) { startUrl = pDoc->url(); QString fileName = startUrl.toLocalFile(); fileName = fileName.replace( QRegExp( "\\.\\w{2,5}$", Qt::CaseInsensitive ), ".pdf" ); startUrl = startUrl.adjusted(QUrl::RemoveFilename); startUrl.setPath(startUrl.path() + fileName ); } QPointer layoutDlg(new KoPageLayoutDialog(this, pageLayout)); layoutDlg->setWindowModality(Qt::WindowModal); if (layoutDlg->exec() != QDialog::Accepted || !layoutDlg) { delete layoutDlg; return 0; } pageLayout = layoutDlg->pageLayout(); delete layoutDlg; KoFileDialog dialog(this, KoFileDialog::SaveFile, "OpenDocument"); dialog.setCaption(i18n("Export as PDF")); dialog.setDefaultDir(startUrl.toLocalFile()); dialog.setMimeTypeFilters(QStringList() << "application/pdf"); QUrl url = QUrl::fromUserInput(dialog.filename()); pdfFileName = url.toLocalFile(); if (pdfFileName.isEmpty()) return 0; } KisPrintJob *printJob = activeView()->createPrintJob(); if (printJob == 0) return 0; if (isHidden()) { printJob->setProperty("noprogressdialog", true); } applyDefaultSettings(printJob->printer()); // TODO for remote files we have to first save locally and then upload. printJob->printer().setOutputFileName(pdfFileName); printJob->printer().setDocName(pdfFileName); printJob->printer().setColorMode(QPrinter::Color); if (pageLayout.format == KoPageFormat::CustomSize) { printJob->printer().setPaperSize(QSizeF(pageLayout.width, pageLayout.height), QPrinter::Millimeter); } else { printJob->printer().setPaperSize(KoPageFormat::printerPageSize(pageLayout.format)); } printJob->printer().setPageMargins(pageLayout.leftMargin, pageLayout.topMargin, pageLayout.rightMargin, pageLayout.bottomMargin, QPrinter::Millimeter); switch (pageLayout.orientation) { case KoPageFormat::Portrait: printJob->printer().setOrientation(QPrinter::Portrait); break; case KoPageFormat::Landscape: printJob->printer().setOrientation(QPrinter::Landscape); break; } //before printing check if the printer can handle printing if (!printJob->canPrint()) { QMessageBox::critical(this, i18nc("@title:window", "Krita"), i18n("Cannot export to the specified file")); } printJob->startPrinting(KisPrintJob::DeleteWhenDone); return printJob; } void KisMainWindow::importAnimation() { if (!activeView()) return; KisDocument *document = activeView()->document(); if (!document) return; KisDlgImportImageSequence dlg(this, document); if (dlg.exec() == QDialog::Accepted) { QStringList files = dlg.files(); int firstFrame = dlg.firstFrame(); int step = dlg.step(); KoUpdaterPtr updater = !document->fileBatchMode() ? viewManager()->createUnthreadedUpdater(i18n("Import frames")) : 0; KisAnimationImporter importer(document->image(), updater); KisImportExportErrorCode status = importer.import(files, firstFrame, step); if (!status.isOk() && !status.isInternalError()) { QString msg = status.errorMessage(); if (!msg.isEmpty()) QMessageBox::critical(0, i18nc("@title:window", "Krita"), i18n("Could not finish import animation:\n%1", msg)); } activeView()->canvasBase()->refetchDataFromImage(); } } void KisMainWindow::slotConfigureToolbars() { saveWindowState(); KEditToolBar edit(factory(), this); connect(&edit, SIGNAL(newToolBarConfig()), this, SLOT(slotNewToolbarConfig())); (void) edit.exec(); applyToolBarLayout(); } void KisMainWindow::slotNewToolbarConfig() { applyMainWindowSettings(d->windowStateConfig); KXMLGUIFactory *factory = guiFactory(); Q_UNUSED(factory); // Check if there's an active view if (!d->activeView) return; plugActionList("toolbarlist", d->toolbarList); applyToolBarLayout(); } void KisMainWindow::slotToolbarToggled(bool toggle) { //dbgUI <<"KisMainWindow::slotToolbarToggled" << sender()->name() <<" toggle=" << true; // The action (sender) and the toolbar have the same name KToolBar * bar = toolBar(sender()->objectName()); if (bar) { if (toggle) { bar->show(); } else { bar->hide(); } if (d->activeView && d->activeView->document()) { saveWindowState(); } } else warnUI << "slotToolbarToggled : Toolbar " << sender()->objectName() << " not found!"; } void KisMainWindow::viewFullscreen(bool fullScreen) { KisConfig cfg(false); cfg.setFullscreenMode(fullScreen); if (fullScreen) { setWindowState(windowState() | Qt::WindowFullScreen); // set } else { setWindowState(windowState() & ~Qt::WindowFullScreen); // reset } } void KisMainWindow::setMaxRecentItems(uint _number) { d->recentFiles->setMaxItems(_number); } void KisMainWindow::slotReloadFile() { KisDocument* document = d->activeView->document(); if (!document || document->url().isEmpty()) return; if (document->isModified()) { bool ok = QMessageBox::question(this, i18nc("@title:window", "Krita"), i18n("You will lose all changes made since your last save\n" "Do you want to continue?"), QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes) == QMessageBox::Yes; if (!ok) return; } QUrl url = document->url(); saveWindowSettings(); if (!document->reload()) { QMessageBox::critical(this, i18nc("@title:window", "Krita"), i18n("Error: Could not reload this document")); } return; } QDockWidget* KisMainWindow::createDockWidget(KoDockFactoryBase* factory) { QDockWidget* dockWidget = 0; bool lockAllDockers = KisConfig(true).readEntry("LockAllDockerPanels", false); if (!d->dockWidgetsMap.contains(factory->id())) { dockWidget = factory->createDockWidget(); // It is quite possible that a dock factory cannot create the dock; don't // do anything in that case. if (!dockWidget) { warnKrita << "Could not create docker for" << factory->id(); return 0; } dockWidget->setFont(KoDockRegistry::dockFont()); dockWidget->setObjectName(factory->id()); dockWidget->setParent(this); if (lockAllDockers) { if (dockWidget->titleBarWidget()) { dockWidget->titleBarWidget()->setVisible(false); } dockWidget->setFeatures(QDockWidget::NoDockWidgetFeatures); } if (dockWidget->widget() && dockWidget->widget()->layout()) dockWidget->widget()->layout()->setContentsMargins(1, 1, 1, 1); Qt::DockWidgetArea side = Qt::RightDockWidgetArea; bool visible = true; switch (factory->defaultDockPosition()) { case KoDockFactoryBase::DockTornOff: dockWidget->setFloating(true); // position nicely? break; case KoDockFactoryBase::DockTop: side = Qt::TopDockWidgetArea; break; case KoDockFactoryBase::DockLeft: side = Qt::LeftDockWidgetArea; break; case KoDockFactoryBase::DockBottom: side = Qt::BottomDockWidgetArea; break; case KoDockFactoryBase::DockRight: side = Qt::RightDockWidgetArea; break; case KoDockFactoryBase::DockMinimized: default: side = Qt::RightDockWidgetArea; visible = false; } KConfigGroup group = d->windowStateConfig.group("DockWidget " + factory->id()); side = static_cast(group.readEntry("DockArea", static_cast(side))); if (side == Qt::NoDockWidgetArea) side = Qt::RightDockWidgetArea; addDockWidget(side, dockWidget); if (!visible) { dockWidget->hide(); } d->dockWidgetsMap.insert(factory->id(), dockWidget); } else { dockWidget = d->dockWidgetsMap[factory->id()]; } #ifdef Q_OS_MACOS dockWidget->setAttribute(Qt::WA_MacSmallSize, true); #endif dockWidget->setFont(KoDockRegistry::dockFont()); connect(dockWidget, SIGNAL(dockLocationChanged(Qt::DockWidgetArea)), this, SLOT(forceDockTabFonts())); return dockWidget; } void KisMainWindow::forceDockTabFonts() { Q_FOREACH (QObject *child, children()) { if (child->inherits("QTabBar")) { ((QTabBar *)child)->setFont(KoDockRegistry::dockFont()); } } } QList KisMainWindow::dockWidgets() const { return d->dockWidgetsMap.values(); } QDockWidget* KisMainWindow::dockWidget(const QString &id) { if (!d->dockWidgetsMap.contains(id)) return 0; return d->dockWidgetsMap[id]; } QList KisMainWindow::canvasObservers() const { QList observers; Q_FOREACH (QDockWidget *docker, dockWidgets()) { KoCanvasObserverBase *observer = dynamic_cast(docker); if (observer) { observers << observer; } else { warnKrita << docker << "is not a canvas observer"; } } return observers; } void KisMainWindow::toggleDockersVisibility(bool visible) { if (!visible) { d->dockerStateBeforeHiding = saveState(); Q_FOREACH (QObject* widget, children()) { if (widget->inherits("QDockWidget")) { QDockWidget* dw = static_cast(widget); if (dw->isVisible()) { dw->hide(); } } } } else { restoreState(d->dockerStateBeforeHiding); } } void KisMainWindow::slotDocumentTitleModified() { updateCaption(); updateReloadFileAction(d->activeView ? d->activeView->document() : 0); } void KisMainWindow::subWindowActivated() { bool enabled = (activeKisView() != 0); d->mdiCascade->setEnabled(enabled); d->mdiNextWindow->setEnabled(enabled); d->mdiPreviousWindow->setEnabled(enabled); d->mdiTile->setEnabled(enabled); d->close->setEnabled(enabled); d->closeAll->setEnabled(enabled); setActiveSubWindow(d->mdiArea->activeSubWindow()); Q_FOREACH (QToolBar *tb, toolBars()) { if (tb->objectName() == "BrushesAndStuff") { tb->setEnabled(enabled); } } /** * Qt has a weirdness, it has hardcoded shortcuts added to an action * in the window menu. We need to reset the shortcuts for that menu * to nothing, otherwise the shortcuts cannot be made configurable. * * See: https://bugs.kde.org/show_bug.cgi?id=352205 * https://bugs.kde.org/show_bug.cgi?id=375524 * https://bugs.kde.org/show_bug.cgi?id=398729 */ QMdiSubWindow *subWindow = d->mdiArea->currentSubWindow(); if (subWindow) { QMenu *menu = subWindow->systemMenu(); if (menu && menu->actions().size() == 8) { Q_FOREACH (QAction *action, menu->actions()) { action->setShortcut(QKeySequence()); } menu->actions().last()->deleteLater(); } } updateCaption(); d->actionManager()->updateGUI(); } void KisMainWindow::windowFocused() { /** * Notify selection manager so that it could update selection mask overlay */ if (viewManager() && viewManager()->selectionManager()) { viewManager()->selectionManager()->selectionChanged(); } KisPart *kisPart = KisPart::instance(); KisWindowLayoutManager *layoutManager = KisWindowLayoutManager::instance(); if (!layoutManager->primaryWorkspaceFollowsFocus()) return; QUuid primary = layoutManager->primaryWindowId(); if (primary.isNull()) return; if (d->id == primary) { if (!d->workspaceBorrowedBy.isNull()) { KisMainWindow *borrower = kisPart->windowById(d->workspaceBorrowedBy); if (!borrower) return; swapWorkspaces(this, borrower); } } else { if (d->workspaceBorrowedBy == primary) return; KisMainWindow *primaryWindow = kisPart->windowById(primary); if (!primaryWindow) return; swapWorkspaces(this, primaryWindow); } } void KisMainWindow::updateWindowMenu() { QMenu *menu = d->windowMenu->menu(); menu->clear(); menu->addAction(d->newWindow); menu->addAction(d->documentMenu); QMenu *docMenu = d->documentMenu->menu(); docMenu->clear(); QFontMetrics fontMetrics = docMenu->fontMetrics(); int fileStringWidth = int(QApplication::desktop()->screenGeometry(this).width() * .40f); Q_FOREACH (QPointer doc, KisPart::instance()->documents()) { if (doc) { QString title = fontMetrics.elidedText(doc->url().toDisplayString(QUrl::PreferLocalFile), Qt::ElideMiddle, fileStringWidth); if (title.isEmpty() && doc->image()) { title = doc->image()->objectName(); } QAction *action = docMenu->addAction(title); action->setIcon(qApp->windowIcon()); connect(action, SIGNAL(triggered()), d->documentMapper, SLOT(map())); d->documentMapper->setMapping(action, doc); } } menu->addAction(d->workspaceMenu); QMenu *workspaceMenu = d->workspaceMenu->menu(); workspaceMenu->clear(); auto workspaces = KisResourceServerProvider::instance()->workspaceServer()->resources(); auto m_this = this; for (auto &w : workspaces) { auto action = workspaceMenu->addAction(w->name()); connect(action, &QAction::triggered, this, [=]() { m_this->restoreWorkspace(w); }); } workspaceMenu->addSeparator(); connect(workspaceMenu->addAction(i18nc("@action:inmenu", "&Import Workspace...")), &QAction::triggered, this, [&]() { QString extensions = d->workspacemodel->extensions(); QStringList mimeTypes; for(const QString &suffix : extensions.split(":")) { mimeTypes << KisMimeDatabase::mimeTypeForSuffix(suffix); } KoFileDialog dialog(0, KoFileDialog::OpenFile, "OpenDocument"); dialog.setMimeTypeFilters(mimeTypes); dialog.setCaption(i18nc("@title:window", "Choose File to Add")); QString filename = dialog.filename(); d->workspacemodel->importResourceFile(filename); }); connect(workspaceMenu->addAction(i18nc("@action:inmenu", "&New Workspace...")), &QAction::triggered, [=]() { QString name = QInputDialog::getText(this, i18nc("@title:window", "New Workspace..."), i18nc("@label:textbox", "Name:")); if (name.isEmpty()) return; auto rserver = KisResourceServerProvider::instance()->workspaceServer(); KisWorkspaceResource* workspace = new KisWorkspaceResource(""); workspace->setDockerState(m_this->saveState()); d->viewManager->canvasResourceProvider()->notifySavingWorkspace(workspace); workspace->setValid(true); QString saveLocation = rserver->saveLocation(); bool newName = false; if(name.isEmpty()) { newName = true; name = i18n("Workspace"); } QFileInfo fileInfo(saveLocation + name + workspace->defaultFileExtension()); int i = 1; while (fileInfo.exists()) { fileInfo.setFile(saveLocation + name + QString("%1").arg(i) + workspace->defaultFileExtension()); i++; } workspace->setFilename(fileInfo.filePath()); if(newName) { name = i18n("Workspace %1", i); } workspace->setName(name); rserver->addResource(workspace); }); // TODO: What to do about delete? // workspaceMenu->addAction(i18nc("@action:inmenu", "&Delete Workspace...")); menu->addSeparator(); menu->addAction(d->close); menu->addAction(d->closeAll); if (d->mdiArea->viewMode() == QMdiArea::SubWindowView) { menu->addSeparator(); menu->addAction(d->mdiTile); menu->addAction(d->mdiCascade); } menu->addSeparator(); menu->addAction(d->mdiNextWindow); menu->addAction(d->mdiPreviousWindow); menu->addSeparator(); QList windows = d->mdiArea->subWindowList(); for (int i = 0; i < windows.size(); ++i) { QPointerchild = qobject_cast(windows.at(i)->widget()); if (child && child->document()) { QString text; if (i < 9) { text = i18n("&%1 %2", i + 1, fontMetrics.elidedText(child->document()->url().toDisplayString(QUrl::PreferLocalFile), Qt::ElideMiddle, fileStringWidth)); } else { text = i18n("%1 %2", i + 1, fontMetrics.elidedText(child->document()->url().toDisplayString(QUrl::PreferLocalFile), Qt::ElideMiddle, fileStringWidth)); } QAction *action = menu->addAction(text); action->setIcon(qApp->windowIcon()); action->setCheckable(true); action->setChecked(child == activeKisView()); connect(action, SIGNAL(triggered()), d->windowMapper, SLOT(map())); d->windowMapper->setMapping(action, windows.at(i)); } } bool showMdiArea = windows.count( ) > 0; if (!showMdiArea) { showWelcomeScreen(true); // see workaround in function in header // keep the recent file list updated when going back to welcome screen reloadRecentFileList(); d->welcomePage->populateRecentDocuments(); } // enable/disable the toolbox docker if there are no documents open Q_FOREACH (QObject* widget, children()) { if (widget->inherits("QDockWidget")) { QDockWidget* dw = static_cast(widget); if ( dw->objectName() == "ToolBox") { dw->setEnabled(showMdiArea); } } } updateCaption(); } void KisMainWindow::setActiveSubWindow(QWidget *window) { if (!window) return; QMdiSubWindow *subwin = qobject_cast(window); //dbgKrita << "setActiveSubWindow();" << subwin << d->activeSubWindow; if (subwin && subwin != d->activeSubWindow) { KisView *view = qobject_cast(subwin->widget()); //dbgKrita << "\t" << view << activeView(); if (view && view != activeView()) { d->mdiArea->setActiveSubWindow(subwin); setActiveView(view); } d->activeSubWindow = subwin; } updateWindowMenu(); d->actionManager()->updateGUI(); } void KisMainWindow::configChanged() { KisConfig cfg(true); QMdiArea::ViewMode viewMode = (QMdiArea::ViewMode)cfg.readEntry("mdi_viewmode", (int)QMdiArea::TabbedView); d->mdiArea->setViewMode(viewMode); Q_FOREACH (QMdiSubWindow *subwin, d->mdiArea->subWindowList()) { subwin->setOption(QMdiSubWindow::RubberBandMove, cfg.readEntry("mdi_rubberband", cfg.useOpenGL())); subwin->setOption(QMdiSubWindow::RubberBandResize, cfg.readEntry("mdi_rubberband", cfg.useOpenGL())); /** * Dirty workaround for a bug in Qt (checked on Qt 5.6.1): * * If you make a window "Show on top" and then switch to the tabbed mode * the window will contiue to be painted in its initial "mid-screen" * position. It will persist here until you explicitly switch to its tab. */ if (viewMode == QMdiArea::TabbedView) { Qt::WindowFlags oldFlags = subwin->windowFlags(); Qt::WindowFlags flags = oldFlags; flags &= ~Qt::WindowStaysOnTopHint; flags &= ~Qt::WindowStaysOnBottomHint; if (flags != oldFlags) { subwin->setWindowFlags(flags); subwin->showMaximized(); } } } KConfigGroup group( KSharedConfig::openConfig(), "theme"); d->themeManager->setCurrentTheme(group.readEntry("Theme", "Krita dark")); d->actionManager()->updateGUI(); QString s = cfg.getMDIBackgroundColor(); KoColor c = KoColor::fromXML(s); QBrush brush(c.toQColor()); d->mdiArea->setBackground(brush); QString backgroundImage = cfg.getMDIBackgroundImage(); if (backgroundImage != "") { QImage image(backgroundImage); QBrush brush(image); d->mdiArea->setBackground(brush); } d->mdiArea->update(); } KisView* KisMainWindow::newView(QObject *document) { KisDocument *doc = qobject_cast(document); KisView *view = addViewAndNotifyLoadingCompleted(doc); d->actionManager()->updateGUI(); return view; } void KisMainWindow::newWindow() { KisMainWindow *mainWindow = KisPart::instance()->createMainWindow(); mainWindow->initializeGeometry(); mainWindow->show(); } void KisMainWindow::closeCurrentWindow() { if (d->mdiArea->currentSubWindow()) { d->mdiArea->currentSubWindow()->close(); d->actionManager()->updateGUI(); } } void KisMainWindow::checkSanity() { // print error if the lcms engine is not available if (!KoColorSpaceEngineRegistry::instance()->contains("icc")) { // need to wait 1 event since exiting here would not work. m_errorMessage = i18n("The Krita LittleCMS color management plugin is not installed. Krita will quit now."); m_dieOnError = true; QTimer::singleShot(0, this, SLOT(showErrorAndDie())); return; } KisPaintOpPresetResourceServer * rserver = KisResourceServerProvider::instance()->paintOpPresetServer(); if (rserver->resources().isEmpty()) { m_errorMessage = i18n("Krita cannot find any brush presets! Krita will quit now."); m_dieOnError = true; QTimer::singleShot(0, this, SLOT(showErrorAndDie())); return; } } void KisMainWindow::showErrorAndDie() { QMessageBox::critical(0, i18nc("@title:window", "Installation error"), m_errorMessage); if (m_dieOnError) { exit(10); } } void KisMainWindow::showAboutApplication() { KisAboutApplication dlg(this); dlg.exec(); } QPointer KisMainWindow::activeKisView() { if (!d->mdiArea) return 0; QMdiSubWindow *activeSubWindow = d->mdiArea->activeSubWindow(); //dbgKrita << "activeKisView" << activeSubWindow; if (!activeSubWindow) return 0; return qobject_cast(activeSubWindow->widget()); } void KisMainWindow::newOptionWidgets(KoCanvasController *controller, const QList > &optionWidgetList) { KIS_ASSERT_RECOVER_NOOP(controller == KoToolManager::instance()->activeCanvasController()); bool isOurOwnView = false; Q_FOREACH (QPointer view, KisPart::instance()->views()) { if (view && view->canvasController() == controller) { isOurOwnView = view->mainWindow() == this; } } if (!isOurOwnView) return; Q_FOREACH (QWidget *w, optionWidgetList) { #ifdef Q_OS_MACOS w->setAttribute(Qt::WA_MacSmallSize, true); #endif w->setFont(KoDockRegistry::dockFont()); } if (d->toolOptionsDocker) { d->toolOptionsDocker->setOptionWidgets(optionWidgetList); } else { d->viewManager->paintOpBox()->newOptionWidgets(optionWidgetList); } } void KisMainWindow::applyDefaultSettings(QPrinter &printer) { if (!d->activeView) return; QString title = d->activeView->document()->documentInfo()->aboutInfo("title"); if (title.isEmpty()) { QFileInfo info(d->activeView->document()->url().fileName()); title = info.completeBaseName(); } if (title.isEmpty()) { // #139905 title = i18n("%1 unsaved document (%2)", qApp->applicationDisplayName(), QLocale().toString(QDate::currentDate(), QLocale::ShortFormat)); } printer.setDocName(title); } void KisMainWindow::createActions() { KisActionManager *actionManager = d->actionManager(); actionManager->createStandardAction(KStandardAction::New, this, SLOT(slotFileNew())); actionManager->createStandardAction(KStandardAction::Open, this, SLOT(slotFileOpen())); actionManager->createStandardAction(KStandardAction::Quit, this, SLOT(slotFileQuit())); actionManager->createStandardAction(KStandardAction::ConfigureToolbars, this, SLOT(slotConfigureToolbars())); d->fullScreenMode = actionManager->createStandardAction(KStandardAction::FullScreen, this, SLOT(viewFullscreen(bool))); d->recentFiles = KStandardAction::openRecent(this, SLOT(slotFileOpenRecent(QUrl)), actionCollection()); connect(d->recentFiles, SIGNAL(recentListCleared()), this, SLOT(saveRecentFiles())); KSharedConfigPtr configPtr = KSharedConfig::openConfig(); d->recentFiles->loadEntries(configPtr->group("RecentFiles")); d->saveAction = actionManager->createStandardAction(KStandardAction::Save, this, SLOT(slotFileSave())); d->saveAction->setActivationFlags(KisAction::ACTIVE_IMAGE); d->saveActionAs = actionManager->createStandardAction(KStandardAction::SaveAs, this, SLOT(slotFileSaveAs())); d->saveActionAs->setActivationFlags(KisAction::ACTIVE_IMAGE); // d->printAction = actionManager->createStandardAction(KStandardAction::Print, this, SLOT(slotFilePrint())); // d->printAction->setActivationFlags(KisAction::ACTIVE_IMAGE); // d->printActionPreview = actionManager->createStandardAction(KStandardAction::PrintPreview, this, SLOT(slotFilePrintPreview())); // d->printActionPreview->setActivationFlags(KisAction::ACTIVE_IMAGE); d->undo = actionManager->createStandardAction(KStandardAction::Undo, this, SLOT(undo())); d->undo->setActivationFlags(KisAction::ACTIVE_IMAGE); d->redo = actionManager->createStandardAction(KStandardAction::Redo, this, SLOT(redo())); d->redo->setActivationFlags(KisAction::ACTIVE_IMAGE); d->undoActionsUpdateManager.reset(new KisUndoActionsUpdateManager(d->undo, d->redo)); d->undoActionsUpdateManager->setCurrentDocument(d->activeView ? d->activeView->document() : 0); // d->exportPdf = actionManager->createAction("file_export_pdf"); // connect(d->exportPdf, SIGNAL(triggered()), this, SLOT(exportToPdf())); d->importAnimation = actionManager->createAction("file_import_animation"); connect(d->importAnimation, SIGNAL(triggered()), this, SLOT(importAnimation())); d->closeAll = actionManager->createAction("file_close_all"); connect(d->closeAll, SIGNAL(triggered()), this, SLOT(slotFileCloseAll())); // d->reloadFile = actionManager->createAction("file_reload_file"); // d->reloadFile->setActivationFlags(KisAction::CURRENT_IMAGE_MODIFIED); // connect(d->reloadFile, SIGNAL(triggered(bool)), this, SLOT(slotReloadFile())); d->importFile = actionManager->createAction("file_import_file"); connect(d->importFile, SIGNAL(triggered(bool)), this, SLOT(slotImportFile())); d->exportFile = actionManager->createAction("file_export_file"); connect(d->exportFile, SIGNAL(triggered(bool)), this, SLOT(slotExportFile())); /* The following entry opens the document information dialog. Since the action is named so it intends to show data this entry should not have a trailing ellipses (...). */ d->showDocumentInfo = actionManager->createAction("file_documentinfo"); connect(d->showDocumentInfo, SIGNAL(triggered(bool)), this, SLOT(slotDocumentInfo())); d->themeManager->setThemeMenuAction(new KActionMenu(i18nc("@action:inmenu", "&Themes"), this)); d->themeManager->registerThemeActions(actionCollection()); connect(d->themeManager, SIGNAL(signalThemeChanged()), this, SLOT(slotThemeChanged())); connect(d->themeManager, SIGNAL(signalThemeChanged()), d->welcomePage, SLOT(slotUpdateThemeColors())); d->toggleDockers = actionManager->createAction("view_toggledockers"); KisConfig(true).showDockers(true); d->toggleDockers->setChecked(true); connect(d->toggleDockers, SIGNAL(toggled(bool)), SLOT(toggleDockersVisibility(bool))); d->toggleDetachCanvas = actionManager->createAction("view_detached_canvas"); d->toggleDetachCanvas->setChecked(false); connect(d->toggleDetachCanvas, SIGNAL(toggled(bool)), SLOT(setCanvasDetached(bool))); setCanvasDetached(false); actionCollection()->addAction("settings_dockers_menu", d->dockWidgetMenu); actionCollection()->addAction("window", d->windowMenu); d->mdiCascade = actionManager->createAction("windows_cascade"); connect(d->mdiCascade, SIGNAL(triggered()), d->mdiArea, SLOT(cascadeSubWindows())); d->mdiTile = actionManager->createAction("windows_tile"); connect(d->mdiTile, SIGNAL(triggered()), d->mdiArea, SLOT(tileSubWindows())); d->mdiNextWindow = actionManager->createAction("windows_next"); connect(d->mdiNextWindow, SIGNAL(triggered()), d->mdiArea, SLOT(activateNextSubWindow())); d->mdiPreviousWindow = actionManager->createAction("windows_previous"); connect(d->mdiPreviousWindow, SIGNAL(triggered()), d->mdiArea, SLOT(activatePreviousSubWindow())); d->newWindow = actionManager->createAction("view_newwindow"); connect(d->newWindow, SIGNAL(triggered(bool)), this, SLOT(newWindow())); d->close = actionManager->createStandardAction(KStandardAction::Close, this, SLOT(closeCurrentWindow())); d->showSessionManager = actionManager->createAction("file_sessions"); connect(d->showSessionManager, SIGNAL(triggered(bool)), this, SLOT(slotShowSessionManager())); actionManager->createStandardAction(KStandardAction::Preferences, this, SLOT(slotPreferences())); for (int i = 0; i < 2; i++) { d->expandingSpacers[i] = new KisAction(i18n("Expanding Spacer")); d->expandingSpacers[i]->setDefaultWidget(new QWidget(this)); d->expandingSpacers[i]->defaultWidget()->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); actionManager->addAction(QString("expanding_spacer_%1").arg(i), d->expandingSpacers[i]); } } void KisMainWindow::applyToolBarLayout() { const bool isPlastiqueStyle = style()->objectName() == "plastique"; Q_FOREACH (KToolBar *toolBar, toolBars()) { toolBar->layout()->setSpacing(4); if (isPlastiqueStyle) { toolBar->setContentsMargins(0, 0, 0, 2); } //Hide text for buttons with an icon in the toolbar Q_FOREACH (QAction *ac, toolBar->actions()){ if (ac->icon().pixmap(QSize(1,1)).isNull() == false){ ac->setPriority(QAction::LowPriority); }else { ac->setIcon(QIcon()); } } } } void KisMainWindow::initializeGeometry() { // if the user didn's specify the geometry on the command line (does anyone do that still?), // we first figure out some good default size and restore the x,y position. See bug 285804Z. KConfigGroup cfg = d->windowStateConfig; QByteArray geom = QByteArray::fromBase64(cfg.readEntry("ko_geometry", QByteArray())); if (!restoreGeometry(geom)) { const int scnum = QApplication::desktop()->screenNumber(parentWidget()); QRect desk = QApplication::desktop()->availableGeometry(scnum); // if the desktop is virtual then use virtual screen size if (QApplication::desktop()->isVirtualDesktop()) { desk = QApplication::desktop()->availableGeometry(QApplication::desktop()->screen(scnum)); } quint32 x = desk.x(); quint32 y = desk.y(); quint32 w = 0; quint32 h = 0; // Default size -- maximize on small screens, something useful on big screens const int deskWidth = desk.width(); if (deskWidth > 1024) { // a nice width, and slightly less than total available // height to compensate for the window decs w = (deskWidth / 3) * 2; h = (desk.height() / 3) * 2; } else { w = desk.width(); h = desk.height(); } x += (desk.width() - w) / 2; y += (desk.height() - h) / 2; move(x,y); setGeometry(geometry().x(), geometry().y(), w, h); } d->fullScreenMode->setChecked(isFullScreen()); } void KisMainWindow::showManual() { QDesktopServices::openUrl(QUrl("https://docs.krita.org")); } void KisMainWindow::windowScreenChanged(QScreen *screen) { emit screenChanged(); d->screenConnectionsStore.clear(); d->screenConnectionsStore.addConnection(screen, SIGNAL(physicalDotsPerInchChanged(qreal)), this, SIGNAL(screenChanged())); } +void KisMainWindow::slotXmlGuiMakingChanges(bool finished) +{ + if (finished) { + subWindowActivated(); + } +} + #include diff --git a/libs/ui/KisMainWindow.h b/libs/ui/KisMainWindow.h index 78fd64aa31..19c77d538a 100644 --- a/libs/ui/KisMainWindow.h +++ b/libs/ui/KisMainWindow.h @@ -1,516 +1,519 @@ /* This file is part of the KDE project Copyright (C) 1998, 1999 Torben Weis Copyright (C) 2000-2004 David Faure This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KIS_MAIN_WINDOW_H #define KIS_MAIN_WINDOW_H #include "kritaui_export.h" #include #include #include #include #include #include #include #include "KisView.h" class QCloseEvent; class QMoveEvent; struct KoPageLayout; class KoCanvasResourceProvider; class KisDocument; class KisPrintJob; class KoDockFactoryBase; class QDockWidget; class KisView; class KisViewManager; class KoCanvasController; class KisWorkspaceResource; /** * @brief Main window for Krita * * This class is used to represent a main window within a Krita session. Each * main window contains a menubar and some toolbars, and potentially several * views of several canvases. * */ class KRITAUI_EXPORT KisMainWindow : public KXmlGuiWindow, public KoCanvasSupervisor { Q_OBJECT public: enum OpenFlag { None = 0, Import = 0x1, BatchMode = 0x2, RecoveryFile = 0x4 }; Q_DECLARE_FLAGS(OpenFlags, OpenFlag) public: /** * Constructor. * * Initializes a Calligra main window (with its basic GUI etc.). */ explicit KisMainWindow(QUuid id = QUuid()); /** * Destructor. */ ~KisMainWindow() override; QUuid id() const; /** * @brief showView shows the given view. Override this if you want to show * the view in a different way than by making it the central widget, for instance * as an QMdiSubWindow */ virtual void showView(KisView *view); /** * @returns the currently active view */ KisView *activeView() const; /** * Sets the maximum number of recent documents entries. */ void setMaxRecentItems(uint _number); /** * The document opened a URL -> store into recent documents list. */ void addRecentURL(const QUrl &url); /** * get list of URL strings for recent files */ QList recentFilesUrls(); /** * clears the list of the recent files */ void clearRecentFiles(); /** * removes the given url from the list of recent files */ void removeRecentUrl(const QUrl &url); /** * Load the desired document and show it. * @param url the URL to open * * @return TRUE on success. */ bool openDocument(const QUrl &url, OpenFlags flags); /** * Activate a view containing the document in this window, creating one if needed. */ void showDocument(KisDocument *document); /** * Toggles between showing the welcome screen and the MDI area * * hack: There seems to be a bug that prevents events happening to the MDI area if it * isn't actively displayed (set in the widgetStack). This can cause things like the title bar * not to update correctly Before doing any actions related to opening or creating documents, * make sure to switch this first to make sure everything can communicate to the MDI area correctly */ void showWelcomeScreen(bool show); /** * Saves the document, asking for a filename if necessary. * * @param saveas if set to TRUE the user is always prompted for a filename * @param silent if set to TRUE rootDocument()->setTitleModified will not be called. * * @return TRUE on success, false on error or cancel * (don't display anything in this case, the error dialog box is also implemented here * but restore the original URL in slotFileSaveAs) */ bool saveDocument(KisDocument *document, bool saveas, bool isExporting); void setReadWrite(bool readwrite); /// Return the list of dock widgets belonging to this main window. QList dockWidgets() const; QDockWidget* dockWidget(const QString &id); QList canvasObservers() const override; KoCanvasResourceProvider *resourceManager() const; int viewCount() const; void saveWindowState(bool restoreNormalState =false); const KConfigGroup &windowStateConfig() const; /** * A wrapper around restoreState * @param state the saved state * @return TRUE on success */ bool restoreWorkspace(KisWorkspaceResource *workspace); bool restoreWorkspaceState(const QByteArray &state); static void swapWorkspaces(KisMainWindow *a, KisMainWindow *b); KisViewManager *viewManager() const; KisView *addViewAndNotifyLoadingCompleted(KisDocument *document); QStringList showOpenFileDialog(bool isImporting); /** * The top-level window used for a detached canvas. */ QWidget *canvasWindow() const; bool canvasDetached() const; /** * Shows if the main window is saving anything right now. If the * user presses Ctrl+W too fast, then the document can be close * before the saving is completed. I'm not sure if it is fixable * in any way without avoiding using porcessEvents() * everywhere (DK) * * Don't use it unless you have no option. */ bool hackIsSaving() const; /// Copy the given file into the bundle directory. bool installBundle(const QString &fileName) const; Q_SIGNALS: /** * This signal is emitted if the document has been saved successfully. */ void documentSaved(); /// This signal is emitted when this windows has finished loading of a /// document. The document may be opened in another window in the end. /// In this case, the signal means there is no link between the window /// and the document anymore. void loadCompleted(); /// This signal is emitted right after the docker states have been succefully restored from config void restoringDone(); /// This signal is emitted when the color theme changes void themeChanged(); /// This signal is emitted when the shortcut key configuration has changed void keyBindingsChanged(); void guiLoadingFinished(); /// emitted when the window is migrated among different screens void screenChanged(); public Q_SLOTS: /** * Slot for opening a new document. * * If the current document is empty, the new document replaces it. * If not, a new mainwindow will be opened for showing the document. */ void slotFileNew(); /** * Slot for opening a saved file. * * If the current document is empty, the opened document replaces it. * If not a new mainwindow will be opened for showing the opened file. */ void slotFileOpen(bool isImporting = false); /** * Slot for opening a file among the recently opened files. * * If the current document is empty, the opened document replaces it. * If not a new mainwindow will be opened for showing the opened file. */ void slotFileOpenRecent(const QUrl &); /** * @brief slotPreferences open the preferences dialog */ void slotPreferences(); /** * Update caption from document info - call when document info * (title in the about page) changes. */ void updateCaption(); /** * Saves the current document with the current name. */ void slotFileSave(); void slotShowSessionManager(); // XXX: disabled KisPrintJob* exportToPdf(QString pdfFileName = QString()); /** * Update the option widgets to the argument ones, removing the currently set widgets. */ void newOptionWidgets(KoCanvasController *controller, const QList > & optionWidgetList); KisView *newView(QObject *document); void notifyChildViewDestroyed(KisView *view); /// Set the active view, this will update the undo/redo actions void setActiveView(KisView *view); void subWindowActivated(); void windowFocused(); /** * Reloads the recent documents list. */ void reloadRecentFileList(); /** * Detach canvas onto a separate window, or restore it back to to main window. */ void setCanvasDetached(bool detached); void slotFileSelected(QString path); void slotEmptyFilePath(); private Q_SLOTS: /** * Save the list of recent files. */ void saveRecentFiles(); void slotLoadCompleted(); void slotLoadCanceled(const QString &); void slotSaveCompleted(); void slotSaveCanceled(const QString &); void forceDockTabFonts(); /** * @internal */ void slotDocumentTitleModified(); /** * Prints the actual document. */ void slotFilePrint(); /** * Saves the current document with a new name. */ void slotFileSaveAs(); void slotFilePrintPreview(); void importAnimation(); /** * Show a dialog with author and document information. */ void slotDocumentInfo(); /** * Closes all open documents. */ bool slotFileCloseAll(); /** * @brief showAboutApplication show the about box */ virtual void showAboutApplication(); /** * Closes the mainwindow. */ void slotFileQuit(); /** * Configure toolbars. */ void slotConfigureToolbars(); /** * Post toolbar config. * (Plug action lists back in, etc.) */ void slotNewToolbarConfig(); /** * Shows or hides a toolbar */ void slotToolbarToggled(bool toggle); /** * Toggle full screen on/off. */ void viewFullscreen(bool fullScreen); /** * Reload file */ void slotReloadFile(); /** * File --> Import * * This will call slotFileOpen(). */ void slotImportFile(); /** * File --> Export * * This will call slotFileSaveAs(). */ void slotExportFile(); /** * Hide the dockers */ void toggleDockersVisibility(bool visible); /** * Handle theme changes from theme manager */ void slotThemeChanged(); void undo(); void redo(); void updateWindowMenu(); void setActiveSubWindow(QWidget *window); void configChanged(); void newWindow(); void closeCurrentWindow(); void checkSanity(); /// Quits Krita with error message from m_errorMessage. void showErrorAndDie(); void initializeGeometry(); void showManual(); void switchTab(int index); void windowScreenChanged(QScreen *screen); + + void slotXmlGuiMakingChanges(bool finished); + protected: void closeEvent(QCloseEvent * e) override; void resizeEvent(QResizeEvent * e) override; // QWidget overrides private: friend class KisWelcomePageWidget; void dragMove(QDragMoveEvent *event); void dragLeave(); private: /** * Add a the given view to the list of views of this mainwindow. * This is a private implementation. For public usage please use * newView() and addViewAndNotifyLoadingCompleted(). */ void addView(KisView *view); friend class KisPart; /** * Returns the dockwidget specified by the @p factory. If the dock widget doesn't exist yet it's created. * Add a "view_palette_action_menu" action to your view menu if you want to use closable dock widgets. * @param factory the factory used to create the dock widget if needed * @return the dock widget specified by @p factory (may be 0) */ QDockWidget* createDockWidget(KoDockFactoryBase* factory); bool openDocumentInternal(const QUrl &url, KisMainWindow::OpenFlags flags = 0); /** * Updates the window caption based on the document info and path. */ void updateCaption(const QString & caption, bool modified); void updateReloadFileAction(KisDocument *doc); void saveWindowSettings(); QPointer activeKisView(); void applyDefaultSettings(QPrinter &printer); void createActions(); void applyToolBarLayout(); QByteArray borrowWorkspace(KisMainWindow *borrower); private: /** * Struct used in the list created by createCustomDocumentWidgets() */ struct CustomDocumentWidgetItem { /// Pointer to the custom document widget QWidget *widget; /// title used in the sidebar. If left empty it will be displayed as "Custom Document" QString title; /// icon used in the sidebar. If left empty it will use the unknown icon QString icon; }; class Private; Private * const d; QString m_errorMessage; bool m_dieOnError; }; Q_DECLARE_OPERATORS_FOR_FLAGS(KisMainWindow::OpenFlags) #endif diff --git a/libs/ui/KisTemplateCreateDia.cpp b/libs/ui/KisTemplateCreateDia.cpp index f8dc808f88..4c921fee65 100644 --- a/libs/ui/KisTemplateCreateDia.cpp +++ b/libs/ui/KisTemplateCreateDia.cpp @@ -1,529 +1,529 @@ /* This file is part of the KDE project Copyright (C) 1998, 1999 Reginald Stadlbauer 2000 Werner Trobin Copyright (C) 2004 Nicolas GOUTTE This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // ODF thumbnail extent static const int thumbnailExtent = 128; class KisTemplateCreateDiaPrivate { public: KisTemplateCreateDiaPrivate(const QString &templatesResourcePath, const QString &filePath, const QPixmap &thumbnail) : m_tree(templatesResourcePath, true) , m_filePath(filePath) , m_thumbnail(thumbnail) { } KisTemplateTree m_tree; QLineEdit *m_name; QRadioButton *m_default; QRadioButton *m_custom; QPushButton *m_select; QLabel *m_preview; QString m_customFile; QPixmap m_customPixmap; QTreeWidget *m_groups; QPushButton *m_add; QPushButton *m_remove; QCheckBox *m_defaultTemplate; QString m_filePath; QPixmap m_thumbnail; bool m_changed; }; /**************************************************************************** * * Class: KisTemplateCreateDia * ****************************************************************************/ KisTemplateCreateDia::KisTemplateCreateDia(const QString &templatesResourcePath, const QString &filePath, const QPixmap &thumbnail, QWidget *parent) : KoDialog(parent) , d(new KisTemplateCreateDiaPrivate(templatesResourcePath, filePath, thumbnail)) { setButtons( KoDialog::Ok|KoDialog::Cancel ); setDefaultButton( KoDialog::Ok ); setCaption( i18n( "Create Template" ) ); setModal( true ); setObjectName( "template create dia" ); QWidget *mainwidget = mainWidget(); QHBoxLayout *mbox=new QHBoxLayout( mainwidget ); QVBoxLayout* leftbox = new QVBoxLayout(); mbox->addLayout( leftbox ); QLabel *label=new QLabel(i18nc("Template name", "Name:"), mainwidget); QHBoxLayout *namefield=new QHBoxLayout(); leftbox->addLayout( namefield ); namefield->addWidget(label); d->m_name=new QLineEdit(mainwidget); d->m_name->setFocus(); connect(d->m_name, SIGNAL(textChanged(QString)), this, SLOT(slotNameChanged(QString))); namefield->addWidget(d->m_name); label=new QLabel(i18n("Group:"), mainwidget); leftbox->addWidget(label); d->m_groups = new QTreeWidget(mainwidget); leftbox->addWidget(d->m_groups); d->m_groups->setColumnCount(1); d->m_groups->setHeaderHidden(true); d->m_groups->setRootIsDecorated(true); d->m_groups->setSortingEnabled(true); fillGroupTree(); d->m_groups->sortItems(0, Qt::AscendingOrder); QHBoxLayout *bbox=new QHBoxLayout(); leftbox->addLayout( bbox ); d->m_add=new QPushButton(i18n("&Add Group..."), mainwidget); connect(d->m_add, SIGNAL(clicked()), this, SLOT(slotAddGroup())); bbox->addWidget(d->m_add); d->m_remove=new QPushButton(i18n("&Remove"), mainwidget); connect(d->m_remove, SIGNAL(clicked()), this, SLOT(slotRemove())); bbox->addWidget(d->m_remove); QVBoxLayout *rightbox=new QVBoxLayout(); mbox->addLayout( rightbox ); QGroupBox *pixbox = new QGroupBox(i18n("Picture"), mainwidget); rightbox->addWidget(pixbox); QVBoxLayout *pixlayout=new QVBoxLayout(pixbox ); d->m_default=new QRadioButton(i18n("&Preview"), pixbox); d->m_default->setChecked(true); connect(d->m_default, SIGNAL(clicked()), this, SLOT(slotDefault())); pixlayout->addWidget(d->m_default); QHBoxLayout *custombox=new QHBoxLayout(); d->m_custom=new QRadioButton(i18n("Custom:"), pixbox); d->m_custom->setChecked(false); connect(d->m_custom, SIGNAL(clicked()), this, SLOT(slotCustom())); custombox->addWidget(d->m_custom); d->m_select=new QPushButton(i18n("&Select..."), pixbox); connect(d->m_select, SIGNAL(clicked()), this, SLOT(slotSelect())); custombox->addWidget(d->m_select); custombox->addStretch(1); pixlayout->addLayout(custombox); d->m_preview=new QLabel(pixbox); // setPixmap() -> auto resize? pixlayout->addWidget(d->m_preview, 0, Qt::AlignCenter); pixlayout->addStretch(1); d->m_defaultTemplate = new QCheckBox( i18n("Use the new template as default"), mainwidget ); d->m_defaultTemplate->setChecked( true ); d->m_defaultTemplate->setVisible( false ); d->m_defaultTemplate->setToolTip(i18n("Use the new template every time Krita starts")); rightbox->addWidget( d->m_defaultTemplate ); enableButtonOk(false); d->m_changed=false; updatePixmap(); connect(d->m_groups, SIGNAL(itemSelectionChanged()), this, SLOT(slotSelectionChanged())); d->m_remove->setEnabled(d->m_groups->currentItem()); connect(this, SIGNAL(okClicked()), this, SLOT(slotOk())); } KisTemplateCreateDia::~KisTemplateCreateDia() { delete d; } void KisTemplateCreateDia::slotSelectionChanged() { const QTreeWidgetItem* item = d->m_groups->currentItem(); d->m_remove->setEnabled( item ); if ( ! item ) return; if ( item->parent() != 0 ) { d->m_name->setText( item->text( 0 ) ); } } void KisTemplateCreateDia::createTemplate(const QString &templatesResourcePath, const char *suffix, KisDocument *document, QWidget *parent) { Q_UNUSED(suffix); QString fileName; { QTemporaryFile tempFile; if (!tempFile.open()) { qWarning("Creation of temporary file to store template failed."); return; } fileName = tempFile.fileName(); } bool retval = document->exportDocumentSync(QUrl::fromLocalFile(fileName), KisDocument::nativeFormatMimeType()); if (!retval) { qWarning("Could not save template"); return; } const QPixmap thumbnail = document->generatePreview(QSize(thumbnailExtent, thumbnailExtent)); KisTemplateCreateDia *dia = new KisTemplateCreateDia(templatesResourcePath, fileName, thumbnail, parent); dia->exec(); delete dia; QDir d; d.remove(fileName); } static void saveAsQuadraticPng(const QPixmap &pixmap, const QString &fileName) { QImage icon = pixmap.toImage(); icon = icon.convertToFormat(QImage::Format_ARGB32); const int iconExtent = qMax(icon.width(), icon.height()); icon = icon.copy((icon.width() - iconExtent) / 2, (icon.height() - iconExtent) / 2, iconExtent, iconExtent); icon.save(fileName, "PNG"); } void KisTemplateCreateDia::slotOk() { // get the current item, if there is one... QTreeWidgetItem *item = d->m_groups->currentItem(); if (!item) item = d->m_groups->topLevelItem(0); if (!item) { // safe :) d->m_tree.writeTemplateTree(); slotButtonClicked( KoDialog::Cancel ); return; } // is it a group or a template? anyway - get the group :) if (item->parent() != 0) item=item->parent(); if (!item) { // *very* safe :P d->m_tree.writeTemplateTree(); slotButtonClicked( KoDialog::Cancel ); return; } KisTemplateGroup *group=d->m_tree.find(item->text(0)); if (!group) { // even safer d->m_tree.writeTemplateTree(); slotButtonClicked( KoDialog::Cancel ); return; } if (d->m_name->text().isEmpty()) { d->m_tree.writeTemplateTree(); slotButtonClicked( KoDialog::Cancel ); return; } // copy the tmp file and the picture the app provides QString dir = KoResourcePaths::saveLocation("data", d->m_tree.templatesResourcePath()); dir += group->name(); QString templateDir = dir+"/.source/"; QString iconDir = dir+"/.icon/"; QString file = KisTemplates::trimmed(d->m_name->text()); QString tmpIcon = ".icon/"+file; tmpIcon += ".png"; QString icon=iconDir+file; icon += ".png"; QString ext = ".kra"; QString dest = templateDir + file + ext; if (QFile::exists(dest)) { do { file = file.prepend( '_' ); dest = templateDir + file + ext; tmpIcon=".icon/" + file + ".png"; icon=iconDir + file + ".png"; } while (QFile(dest).exists()); } bool ignore = false; KisTemplate *t = new KisTemplate(d->m_name->text(), QString(), ".source/"+ file + ext, tmpIcon, "", "", false, true); if (!group->add(t)) { KisTemplate *existingTemplate=group->find(d->m_name->text()); if (existingTemplate && !existingTemplate->isHidden()) { if (QMessageBox::warning(this, i18nc("@title:window", "Krita"), i18n("Do you really want to overwrite the existing '%1' template?", existingTemplate->name()), QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes) == QMessageBox::Yes) { group->add(t, true); } else { delete t; return; } } else { ignore = true; } } QDir path; if (!path.mkpath(templateDir) || !path.mkpath(iconDir)) { d->m_tree.writeTemplateTree(); slotButtonClicked( KoDialog::Cancel ); return; } QString orig; orig = d->m_filePath; // don't overwrite the hidden template file with a new non-hidden one if (!ignore) { if (!QFile::copy(d->m_filePath, dest)) { qWarning() << "Could not copy" << d->m_filePath << "to" << dest; } // save the picture as icon if (d->m_default->isChecked() && !d->m_thumbnail.isNull()) { saveAsQuadraticPng(d->m_thumbnail, icon); } else if (!d->m_customPixmap.isNull()) { saveAsQuadraticPng(d->m_customPixmap, icon); } else { warnUI << "Could not save the preview picture!"; } } // if there's a .directory file, we copy this one, too bool ready=false; QStringList tmp=group->dirs(); for(QStringList::ConstIterator it=tmp.constBegin(); it!=tmp.constEnd() && !ready; ++it) { if ((*it).contains(dir)==0) { orig = (*it) + ".directory"; // Check if we can read the file if (QFile(orig).exists()) { dest = dir + "/.directory"; // We copy the file with overwrite if (!QFile(orig).copy(dest)) { warnKrita << "Failed to copy from" << orig << "to" << dest; } ready = true; } } } d->m_tree.writeTemplateTree(); if ( d->m_defaultTemplate->isChecked() ) { KConfigGroup grp( KSharedConfig::openConfig(), "TemplateChooserDialog"); grp.writeEntry( "LastReturnType", "Template" ); grp.writePathEntry( "FullTemplateName", dir + '/' + t->file() ); grp.writePathEntry( "AlwaysUseTemplate", dir + '/' + t->file() ); } } void KisTemplateCreateDia::slotDefault() { d->m_default->setChecked(true); d->m_custom->setChecked(false); updatePixmap(); } void KisTemplateCreateDia::slotCustom() { d->m_default->setChecked(false); d->m_custom->setChecked(true); if (d->m_customFile.isEmpty()) slotSelect(); else updatePixmap(); } void KisTemplateCreateDia::slotSelect() { d->m_default->setChecked(false); d->m_custom->setChecked(true); KoFileDialog dlg(this, KoFileDialog::OpenFile, "TemplateImages"); dlg.setDefaultDir(QStandardPaths::writableLocation(QStandardPaths::PicturesLocation)); dlg.setImageFilters(); dlg.setCaption(i18n("Select an image")); QString fn = dlg.filename(); if (fn.isEmpty()) { if (d->m_customFile.isEmpty()) { d->m_default->setChecked(true); d->m_custom->setChecked(false); } return; } QImage image(fn); if (image.isNull()) { QMessageBox::warning(this, i18nc("@title:window", "Krita"), i18n("%1 is not a valid image file!", fn)); } d->m_customFile = fn; d->m_customPixmap = QPixmap(); updatePixmap(); } void KisTemplateCreateDia::slotNameChanged(const QString &name) { if ( ( name.trimmed().isEmpty() || !d->m_groups->topLevelItem(0) ) && !d->m_changed ) enableButtonOk(false); else enableButtonOk(true); } void KisTemplateCreateDia::slotAddGroup() { const QString name = QInputDialog::getText(this, i18n("Add Group"), i18n("Enter group name:")); KisTemplateGroup *group = d->m_tree.find(name); if (group && !group->isHidden()) { QMessageBox::information( this, i18n("This name is already used."), i18n("Add Group") ); return; } QString dir = KoResourcePaths::saveLocation("data", d->m_tree.templatesResourcePath()); dir+=name; KisTemplateGroup *newGroup=new KisTemplateGroup(name, dir, 0, true); d->m_tree.add(newGroup); QTreeWidgetItem *item = new QTreeWidgetItem(d->m_groups, QStringList() << name); d->m_groups->setCurrentItem(item); d->m_groups->sortItems(0, Qt::AscendingOrder); d->m_name->setFocus(); enableButtonOk(true); d->m_changed=true; } void KisTemplateCreateDia::slotRemove() { QTreeWidgetItem *item = d->m_groups->currentItem(); if (!item) return; QString what; QString removed; if (item->parent() == 0) { what = i18n("Do you really want to remove that group?"); removed = i18nc("@title:window", "Remove Group"); } else { what = i18n("Do you really want to remove that template?"); removed = i18nc("@title:window", "Remove Template"); } if (QMessageBox::warning(this, removed, what, QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes) == QMessageBox:: No) { d->m_name->setFocus(); return; } if (item->parent() == 0) { KisTemplateGroup *group=d->m_tree.find(item->text(0)); if (group) group->setHidden(true); } else { bool done=false; QList groups = d->m_tree.groups(); QList::const_iterator it = groups.constBegin(); for(; it != groups.constEnd() && !done; ++it) { KisTemplate *t = (*it)->find(item->text(0)); if (t) { t->setHidden(true); done=true; } } } delete item; item=0; - enableButtonOk(true); d->m_name->setFocus(); d->m_changed=true; + d->m_tree.writeTemplateTree(); } void KisTemplateCreateDia::updatePixmap() { if (d->m_default->isChecked() && !d->m_thumbnail.isNull()) { d->m_preview->setPixmap(d->m_thumbnail); } else if (d->m_custom->isChecked() && !d->m_customFile.isEmpty()) { if (d->m_customPixmap.isNull()) { dbgUI <<"Trying to load picture" << d->m_customFile; // use the code in KisTemplate to load the image... hacky, I know :) KisTemplate t("foo", "bar", QString(), d->m_customFile); d->m_customPixmap = t.loadPicture(); } else { warnUI << "Trying to load picture"; } if (!d->m_customPixmap.isNull()) { d->m_preview->setPixmap(d->m_customPixmap); } else { d->m_preview->setText(i18n("Could not load picture.")); } } else { d->m_preview->setText(i18n("No picture available.")); } } void KisTemplateCreateDia::fillGroupTree() { Q_FOREACH (KisTemplateGroup *group, d->m_tree.groups()) { if (group->isHidden()) continue; QTreeWidgetItem *groupItem=new QTreeWidgetItem(d->m_groups, QStringList() << group->name()); Q_FOREACH (KisTemplate *t, group->templates()) { if (t->isHidden()) continue; (void)new QTreeWidgetItem(groupItem, QStringList() << t->name()); } } } diff --git a/libs/ui/kis_painting_assistant.cc b/libs/ui/kis_painting_assistant.cc index 8537c4d273..9dfff8731e 100644 --- a/libs/ui/kis_painting_assistant.cc +++ b/libs/ui/kis_painting_assistant.cc @@ -1,925 +1,934 @@ /* * Copyright (c) 2008,2011 Cyrille Berger * Copyright (c) 2010 Geoffry Song * Copyright (c) 2017 Scott Petrovic * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include #include "kis_painting_assistant.h" #include "kis_coordinates_converter.h" #include "kis_debug.h" #include "kis_dom_utils.h" #include #include "kis_tool.h" #include "kis_config.h" #include #include #include #include #include #include #include Q_GLOBAL_STATIC(KisPaintingAssistantFactoryRegistry, s_instance) struct KisPaintingAssistantHandle::Private { QList assistants; char handle_type; }; KisPaintingAssistantHandle::KisPaintingAssistantHandle(double x, double y) : QPointF(x, y), d(new Private) { } KisPaintingAssistantHandle::KisPaintingAssistantHandle(QPointF p) : QPointF(p), d(new Private) { } KisPaintingAssistantHandle::KisPaintingAssistantHandle(const KisPaintingAssistantHandle& rhs) : QPointF(rhs) , KisShared() , d(new Private) { dbgUI << "KisPaintingAssistantHandle ctor"; } KisPaintingAssistantHandle& KisPaintingAssistantHandle::operator=(const QPointF & pt) { setX(pt.x()); setY(pt.y()); return *this; } void KisPaintingAssistantHandle::setType(char type) { d->handle_type = type; } char KisPaintingAssistantHandle::handleType() const { return d->handle_type; } +KisPaintingAssistant *KisPaintingAssistantHandle::chiefAssistant() const +{ + return !d->assistants.isEmpty() ? d->assistants.first() : 0; +} + KisPaintingAssistantHandle::~KisPaintingAssistantHandle() { Q_ASSERT(d->assistants.empty()); delete d; } void KisPaintingAssistantHandle::registerAssistant(KisPaintingAssistant* assistant) { Q_ASSERT(!d->assistants.contains(assistant)); d->assistants.append(assistant); } void KisPaintingAssistantHandle::unregisterAssistant(KisPaintingAssistant* assistant) { d->assistants.removeOne(assistant); Q_ASSERT(!d->assistants.contains(assistant)); } bool KisPaintingAssistantHandle::containsAssistant(KisPaintingAssistant* assistant) const { return d->assistants.contains(assistant); } void KisPaintingAssistantHandle::mergeWith(KisPaintingAssistantHandleSP handle) { if(this->handleType()== HandleType::NORMAL || handle.data()->handleType()== HandleType::SIDE) { return; } Q_FOREACH (KisPaintingAssistant* assistant, handle->d->assistants) { if (!assistant->handles().contains(this)) { assistant->replaceHandle(handle, this); } } } void KisPaintingAssistantHandle::uncache() { Q_FOREACH (KisPaintingAssistant* assistant, d->assistants) { assistant->uncache(); } } struct KisPaintingAssistant::Private { Private(); explicit Private(const Private &rhs); KisPaintingAssistantHandleSP reuseOrCreateHandle(QMap &handleMap, KisPaintingAssistantHandleSP origHandle, KisPaintingAssistant *q, bool registerAssistant = true); QList handles, sideHandles; KisPaintingAssistantHandleSP topLeft, bottomLeft, topRight, bottomRight, topMiddle, bottomMiddle, rightMiddle, leftMiddle; // share everything except handles between the clones struct SharedData { QString id; QString name; bool isSnappingActive; bool outlineVisible; KisCanvas2* m_canvas = 0; QPixmapCache::Key cached; QRect cachedRect; // relative to boundingRect().topLeft() struct TranslationInvariantTransform { qreal m11, m12, m21, m22; TranslationInvariantTransform() { } TranslationInvariantTransform(const QTransform& t) : m11(t.m11()), m12(t.m12()), m21(t.m21()), m22(t.m22()) { } bool operator==(const TranslationInvariantTransform& b) { return m11 == b.m11 && m12 == b.m12 && m21 == b.m21 && m22 == b.m22; } } cachedTransform; QColor assistantGlobalColorCache = QColor(Qt::red); // color to paint with if a custom color is not set bool useCustomColor = false; QColor assistantCustomColor = KisConfig(true).defaultAssistantsColor(); }; QSharedPointer s; }; KisPaintingAssistant::Private::Private() : s(new SharedData) { } KisPaintingAssistant::Private::Private(const Private &rhs) : s(rhs.s) { } KisPaintingAssistantHandleSP KisPaintingAssistant::Private::reuseOrCreateHandle(QMap &handleMap, KisPaintingAssistantHandleSP origHandle, KisPaintingAssistant *q, bool registerAssistant) { KisPaintingAssistantHandleSP mappedHandle = handleMap.value(origHandle); if (!mappedHandle) { if (origHandle) { dbgUI << "handle not found in the map, creating a new one..."; mappedHandle = KisPaintingAssistantHandleSP(new KisPaintingAssistantHandle(*origHandle)); dbgUI << "done"; mappedHandle->setType(origHandle->handleType()); handleMap.insert(origHandle, mappedHandle); } else { dbgUI << "orig handle is null, not doing anything"; mappedHandle = KisPaintingAssistantHandleSP(); } } if (mappedHandle && registerAssistant) { mappedHandle->registerAssistant(q); } return mappedHandle; } bool KisPaintingAssistant::useCustomColor() { return d->s->useCustomColor; } void KisPaintingAssistant::setUseCustomColor(bool useCustomColor) { d->s->useCustomColor = useCustomColor; } void KisPaintingAssistant::setAssistantCustomColor(QColor color) { d->s->assistantCustomColor = color; } QColor KisPaintingAssistant::assistantCustomColor() { return d->s->assistantCustomColor; } void KisPaintingAssistant::setAssistantGlobalColorCache(const QColor &color) { d->s->assistantGlobalColorCache = color; } QColor KisPaintingAssistant::effectiveAssistantColor() const { return d->s->useCustomColor ? d->s->assistantCustomColor : d->s->assistantGlobalColorCache; } KisPaintingAssistant::KisPaintingAssistant(const QString& id, const QString& name) : d(new Private) { d->s->id = id; d->s->name = name; d->s->isSnappingActive = true; d->s->outlineVisible = true; } KisPaintingAssistant::KisPaintingAssistant(const KisPaintingAssistant &rhs, QMap &handleMap) : d(new Private(*(rhs.d))) { dbgUI << "creating handles..."; Q_FOREACH (const KisPaintingAssistantHandleSP origHandle, rhs.d->handles) { d->handles << d->reuseOrCreateHandle(handleMap, origHandle, this); } Q_FOREACH (const KisPaintingAssistantHandleSP origHandle, rhs.d->sideHandles) { d->sideHandles << d->reuseOrCreateHandle(handleMap, origHandle, this); } #define _REUSE_H(name) d->name = d->reuseOrCreateHandle(handleMap, rhs.d->name, this, /* registerAssistant = */ false) _REUSE_H(topLeft); _REUSE_H(bottomLeft); _REUSE_H(topRight); _REUSE_H(bottomRight); _REUSE_H(topMiddle); _REUSE_H(bottomMiddle); _REUSE_H(rightMiddle); _REUSE_H(leftMiddle); #undef _REUSE_H dbgUI << "done"; } bool KisPaintingAssistant::isSnappingActive() const { return d->s->isSnappingActive; } void KisPaintingAssistant::setSnappingActive(bool set) { d->s->isSnappingActive = set; } void KisPaintingAssistant::drawPath(QPainter& painter, const QPainterPath &path, bool isSnappingOn) { QColor paintingColor = effectiveAssistantColor(); if (!isSnappingOn) { paintingColor.setAlpha(0.2 * paintingColor.alpha()); } painter.save(); QPen pen_a(paintingColor, 2); pen_a.setCosmetic(true); painter.setPen(pen_a); painter.drawPath(path); painter.restore(); } void KisPaintingAssistant::drawPreview(QPainter& painter, const QPainterPath &path) { painter.save(); QPen pen_a(effectiveAssistantColor(), 1); pen_a.setStyle(Qt::SolidLine); pen_a.setCosmetic(true); painter.setPen(pen_a); painter.drawPath(path); painter.restore(); } void KisPaintingAssistant::initHandles(QList _handles) { Q_ASSERT(d->handles.isEmpty()); d->handles = _handles; Q_FOREACH (KisPaintingAssistantHandleSP handle, _handles) { handle->registerAssistant(this); } } KisPaintingAssistant::~KisPaintingAssistant() { Q_FOREACH (KisPaintingAssistantHandleSP handle, d->handles) { handle->unregisterAssistant(this); } if(!d->sideHandles.isEmpty()) { Q_FOREACH (KisPaintingAssistantHandleSP handle, d->sideHandles) { handle->unregisterAssistant(this); } } delete d; } const QString& KisPaintingAssistant::id() const { return d->s->id; } const QString& KisPaintingAssistant::name() const { return d->s->name; } void KisPaintingAssistant::replaceHandle(KisPaintingAssistantHandleSP _handle, KisPaintingAssistantHandleSP _with) { Q_ASSERT(d->handles.contains(_handle)); d->handles.replace(d->handles.indexOf(_handle), _with); Q_ASSERT(!d->handles.contains(_handle)); _handle->unregisterAssistant(this); _with->registerAssistant(this); } void KisPaintingAssistant::addHandle(KisPaintingAssistantHandleSP handle, HandleType type) { Q_ASSERT(!d->handles.contains(handle)); if (HandleType::SIDE == type) { d->sideHandles.append(handle); } else { d->handles.append(handle); } handle->registerAssistant(this); handle.data()->setType(type); } QPointF KisPaintingAssistant::viewportConstrainedEditorPosition(const KisCoordinatesConverter* converter, const QSize editorSize) { QPointF editorDocumentPos = getEditorPosition(); QPointF editorWidgetPos = converter->documentToWidgetTransform().map(editorDocumentPos); QSizeF canvasSize = converter->getCanvasWidgetSize(); const int padding = 16; editorWidgetPos.rx() = qBound(0.0, editorWidgetPos.x(), canvasSize.width() - (editorSize.width() + padding)); editorWidgetPos.ry() = qBound(0.0, editorWidgetPos.y(), canvasSize.height() - (editorSize.height() + padding)); return converter->widgetToDocument(editorWidgetPos); } void KisPaintingAssistant::drawAssistant(QPainter& gc, const QRectF& updateRect, const KisCoordinatesConverter* converter, bool useCache, KisCanvas2* canvas, bool assistantVisible, bool previewVisible) { Q_UNUSED(updateRect); Q_UNUSED(previewVisible); findPerspectiveAssistantHandleLocation(); if (!useCache) { gc.save(); drawCache(gc, converter, assistantVisible); gc.restore(); return; } const QRect bound = boundingRect(); if (bound.isEmpty()) { return; } const QTransform transform = converter->documentToWidgetTransform(); const QRect widgetBound = transform.mapRect(bound); const QRect paintRect = transform.mapRect(bound).intersected(gc.viewport()); if (paintRect.isEmpty()) return; QPixmap cached; bool found = QPixmapCache::find(d->s->cached, &cached); if (!(found && d->s->cachedTransform == transform && d->s->cachedRect.translated(widgetBound.topLeft()).contains(paintRect))) { const QRect cacheRect = gc.viewport().adjusted(-100, -100, 100, 100).intersected(widgetBound); Q_ASSERT(!cacheRect.isEmpty()); if (cached.isNull() || cached.size() != cacheRect.size()) { cached = QPixmap(cacheRect.size()); } cached.fill(Qt::transparent); QPainter painter(&cached); painter.setRenderHint(QPainter::Antialiasing); painter.setWindow(cacheRect); drawCache(painter, converter, assistantVisible); painter.end(); d->s->cachedTransform = transform; d->s->cachedRect = cacheRect.translated(-widgetBound.topLeft()); d->s->cached = QPixmapCache::insert(cached); } gc.drawPixmap(paintRect, cached, paintRect.translated(-widgetBound.topLeft() - d->s->cachedRect.topLeft())); if (canvas) { d->s->m_canvas = canvas; } } void KisPaintingAssistant::uncache() { d->s->cached = QPixmapCache::Key(); } QRect KisPaintingAssistant::boundingRect() const { QRectF r; Q_FOREACH (KisPaintingAssistantHandleSP h, handles()) { r = r.united(QRectF(*h, QSizeF(1,1))); } return r.adjusted(-2, -2, 2, 2).toAlignedRect(); } bool KisPaintingAssistant::isAssistantComplete() const { return true; } void KisPaintingAssistant::transform(const QTransform &transform) { Q_FOREACH(KisPaintingAssistantHandleSP handle, handles()) { + if (handle->chiefAssistant() != this) continue; + *handle = transform.map(*handle); } Q_FOREACH(KisPaintingAssistantHandleSP handle, sideHandles()) { + if (handle->chiefAssistant() != this) continue; + *handle = transform.map(*handle); } uncache(); } QByteArray KisPaintingAssistant::saveXml(QMap &handleMap) { QByteArray data; QXmlStreamWriter xml(&data); xml.writeStartDocument(); xml.writeStartElement("assistant"); xml.writeAttribute("type",d->s->id); xml.writeAttribute("active", QString::number(d->s->isSnappingActive)); xml.writeAttribute("useCustomColor", QString::number(d->s->useCustomColor)); xml.writeAttribute("customColor", KisDomUtils::qColorToQString(d->s->assistantCustomColor)); saveCustomXml(&xml); // if any specific assistants have custom XML data to save to // write individual handle data xml.writeStartElement("handles"); Q_FOREACH (const KisPaintingAssistantHandleSP handle, d->handles) { int id = handleMap.size(); if (!handleMap.contains(handle)){ handleMap.insert(handle, id); } id = handleMap.value(handle); xml.writeStartElement("handle"); xml.writeAttribute("id", QString::number(id)); xml.writeAttribute("x", QString::number(double(handle->x()), 'f', 3)); xml.writeAttribute("y", QString::number(double(handle->y()), 'f', 3)); xml.writeEndElement(); } xml.writeEndElement(); xml.writeEndElement(); xml.writeEndDocument(); return data; } void KisPaintingAssistant::saveCustomXml(QXmlStreamWriter* xml) { Q_UNUSED(xml); } void KisPaintingAssistant::loadXml(KoStore* store, QMap &handleMap, QString path) { int id = 0; double x = 0.0, y = 0.0; store->open(path); QByteArray data = store->read(store->size()); QXmlStreamReader xml(data); while (!xml.atEnd()) { switch (xml.readNext()) { case QXmlStreamReader::StartElement: if (xml.name() == "assistant") { QStringRef active = xml.attributes().value("active"); setSnappingActive( (active != "0") ); // load custom shared assistant properties if ( xml.attributes().hasAttribute("useCustomColor")) { QStringRef useCustomColor = xml.attributes().value("useCustomColor"); bool usingColor = false; if (useCustomColor.toString() == "1") { usingColor = true; } setUseCustomColor(usingColor); } if ( xml.attributes().hasAttribute("customColor")) { QStringRef customColor = xml.attributes().value("customColor"); setAssistantCustomColor( KisDomUtils::qStringToQColor(customColor.toString()) ); } } loadCustomXml(&xml); if (xml.name() == "handle") { QString strId = xml.attributes().value("id").toString(), strX = xml.attributes().value("x").toString(), strY = xml.attributes().value("y").toString(); if (!strId.isEmpty() && !strX.isEmpty() && !strY.isEmpty()) { id = strId.toInt(); x = strX.toDouble(); y = strY.toDouble(); if (!handleMap.contains(id)) { handleMap.insert(id, new KisPaintingAssistantHandle(x, y)); } } addHandle(handleMap.value(id), HandleType::NORMAL); } break; default: break; } } store->close(); } bool KisPaintingAssistant::loadCustomXml(QXmlStreamReader* xml) { Q_UNUSED(xml); return true; } void KisPaintingAssistant::saveXmlList(QDomDocument& doc, QDomElement& assistantsElement,int count) { if (d->s->id == "ellipse"){ QDomElement assistantElement = doc.createElement("assistant"); assistantElement.setAttribute("type", "ellipse"); assistantElement.setAttribute("filename", QString("ellipse%1.assistant").arg(count)); assistantsElement.appendChild(assistantElement); } else if (d->s->id == "spline"){ QDomElement assistantElement = doc.createElement("assistant"); assistantElement.setAttribute("type", "spline"); assistantElement.setAttribute("filename", QString("spline%1.assistant").arg(count)); assistantsElement.appendChild(assistantElement); } else if (d->s->id == "perspective"){ QDomElement assistantElement = doc.createElement("assistant"); assistantElement.setAttribute("type", "perspective"); assistantElement.setAttribute("filename", QString("perspective%1.assistant").arg(count)); assistantsElement.appendChild(assistantElement); } else if (d->s->id == "vanishing point"){ QDomElement assistantElement = doc.createElement("assistant"); assistantElement.setAttribute("type", "vanishing point"); assistantElement.setAttribute("filename", QString("vanishing point%1.assistant").arg(count)); assistantsElement.appendChild(assistantElement); } else if (d->s->id == "infinite ruler"){ QDomElement assistantElement = doc.createElement("assistant"); assistantElement.setAttribute("type", "infinite ruler"); assistantElement.setAttribute("filename", QString("infinite ruler%1.assistant").arg(count)); assistantsElement.appendChild(assistantElement); } else if (d->s->id == "parallel ruler"){ QDomElement assistantElement = doc.createElement("assistant"); assistantElement.setAttribute("type", "parallel ruler"); assistantElement.setAttribute("filename", QString("parallel ruler%1.assistant").arg(count)); assistantsElement.appendChild(assistantElement); } else if (d->s->id == "concentric ellipse"){ QDomElement assistantElement = doc.createElement("assistant"); assistantElement.setAttribute("type", "concentric ellipse"); assistantElement.setAttribute("filename", QString("concentric ellipse%1.assistant").arg(count)); assistantsElement.appendChild(assistantElement); } else if (d->s->id == "fisheye-point"){ QDomElement assistantElement = doc.createElement("assistant"); assistantElement.setAttribute("type", "fisheye-point"); assistantElement.setAttribute("filename", QString("fisheye-point%1.assistant").arg(count)); assistantsElement.appendChild(assistantElement); } else if (d->s->id == "ruler"){ QDomElement assistantElement = doc.createElement("assistant"); assistantElement.setAttribute("type", "ruler"); assistantElement.setAttribute("filename", QString("ruler%1.assistant").arg(count)); assistantsElement.appendChild(assistantElement); } } void KisPaintingAssistant::findPerspectiveAssistantHandleLocation() { QList hHandlesList; QList vHandlesList; uint vHole = 0,hHole = 0; KisPaintingAssistantHandleSP oppHandle; if (d->handles.size() == 4 && d->s->id == "perspective") { //get the handle opposite to the first handle oppHandle = oppHandleOne(); //Sorting handles into two list, X sorted and Y sorted into hHandlesList and vHandlesList respectively. Q_FOREACH (const KisPaintingAssistantHandleSP handle,d->handles) { hHandlesList.append(handle); hHole = hHandlesList.size() - 1; vHandlesList.append(handle); vHole = vHandlesList.size() - 1; /* sort handles on the basis of X-coordinate */ while(hHole > 0 && hHandlesList.at(hHole -1).data()->x() > handle.data()->x()) { hHandlesList.swap(hHole-1, hHole); hHole = hHole - 1; } /* sort handles on the basis of Y-coordinate */ while(vHole > 0 && vHandlesList.at(vHole -1).data()->y() > handle.data()->y()) { vHandlesList.swap(vHole-1, vHole); vHole = vHole - 1; } } /* give the handles their respective positions */ if(vHandlesList.at(0).data()->x() > vHandlesList.at(1).data()->x()) { d->topLeft = vHandlesList.at(1); d->topRight= vHandlesList.at(0); } else { d->topLeft = vHandlesList.at(0); d->topRight = vHandlesList.at(1); } if(vHandlesList.at(2).data()->x() > vHandlesList.at(3).data()->x()) { d->bottomLeft = vHandlesList.at(3); d->bottomRight = vHandlesList.at(2); } else { d->bottomLeft= vHandlesList.at(2); d->bottomRight = vHandlesList.at(3); } /* find if the handles that should be opposite are actually oppositely positioned */ if (( (d->topLeft == d->handles.at(0).data() && d->bottomRight == oppHandle) || (d->topLeft == oppHandle && d->bottomRight == d->handles.at(0).data()) || (d->topRight == d->handles.at(0).data() && d->bottomLeft == oppHandle) || (d->topRight == oppHandle && d->bottomLeft == d->handles.at(0).data()) ) ) {} else { if(hHandlesList.at(0).data()->y() > hHandlesList.at(1).data()->y()) { d->topLeft = hHandlesList.at(1); d->bottomLeft= hHandlesList.at(0); } else { d->topLeft = hHandlesList.at(0); d->bottomLeft = hHandlesList.at(1); } if(hHandlesList.at(2).data()->y() > hHandlesList.at(3).data()->y()) { d->topRight = hHandlesList.at(3); d->bottomRight = hHandlesList.at(2); } else { d->topRight= hHandlesList.at(2); d->bottomRight = hHandlesList.at(3); } } /* Setting the middle handles as needed */ if(!d->bottomMiddle && !d->topMiddle && !d->leftMiddle && !d->rightMiddle) { d->bottomMiddle = new KisPaintingAssistantHandle((d->bottomLeft.data()->x() + d->bottomRight.data()->x())*0.5, (d->bottomLeft.data()->y() + d->bottomRight.data()->y())*0.5); d->topMiddle = new KisPaintingAssistantHandle((d->topLeft.data()->x() + d->topRight.data()->x())*0.5, (d->topLeft.data()->y() + d->topRight.data()->y())*0.5); d->rightMiddle= new KisPaintingAssistantHandle((d->topRight.data()->x() + d->bottomRight.data()->x())*0.5, (d->topRight.data()->y() + d->bottomRight.data()->y())*0.5); d->leftMiddle= new KisPaintingAssistantHandle((d->bottomLeft.data()->x() + d->topLeft.data()->x())*0.5, (d->bottomLeft.data()->y() + d->topLeft.data()->y())*0.5); addHandle(d->rightMiddle.data(), HandleType::SIDE); addHandle(d->leftMiddle.data(), HandleType::SIDE); addHandle(d->bottomMiddle.data(), HandleType::SIDE); addHandle(d->topMiddle.data(), HandleType::SIDE); } else { d->bottomMiddle.data()->operator =(QPointF((d->bottomLeft.data()->x() + d->bottomRight.data()->x())*0.5, (d->bottomLeft.data()->y() + d->bottomRight.data()->y())*0.5)); d->topMiddle.data()->operator =(QPointF((d->topLeft.data()->x() + d->topRight.data()->x())*0.5, (d->topLeft.data()->y() + d->topRight.data()->y())*0.5)); d->rightMiddle.data()->operator =(QPointF((d->topRight.data()->x() + d->bottomRight.data()->x())*0.5, (d->topRight.data()->y() + d->bottomRight.data()->y())*0.5)); d->leftMiddle.data()->operator =(QPointF((d->bottomLeft.data()->x() + d->topLeft.data()->x())*0.5, (d->bottomLeft.data()->y() + d->topLeft.data()->y())*0.5)); } } } KisPaintingAssistantHandleSP KisPaintingAssistant::oppHandleOne() { QPointF intersection(0,0); if((QLineF(d->handles.at(0).data()->toPoint(),d->handles.at(1).data()->toPoint()).intersect(QLineF(d->handles.at(2).data()->toPoint(),d->handles.at(3).data()->toPoint()), &intersection) != QLineF::NoIntersection) && (QLineF(d->handles.at(0).data()->toPoint(),d->handles.at(1).data()->toPoint()).intersect(QLineF(d->handles.at(2).data()->toPoint(),d->handles.at(3).data()->toPoint()), &intersection) != QLineF::UnboundedIntersection)) { return d->handles.at(1); } else if((QLineF(d->handles.at(0).data()->toPoint(),d->handles.at(2).data()->toPoint()).intersect(QLineF(d->handles.at(1).data()->toPoint(),d->handles.at(3).data()->toPoint()), &intersection) != QLineF::NoIntersection) && (QLineF(d->handles.at(0).data()->toPoint(),d->handles.at(2).data()->toPoint()).intersect(QLineF(d->handles.at(1).data()->toPoint(),d->handles.at(3).data()->toPoint()), &intersection) != QLineF::UnboundedIntersection)) { return d->handles.at(2); } else { return d->handles.at(3); } } KisPaintingAssistantHandleSP KisPaintingAssistant::topLeft() { return d->topLeft; } const KisPaintingAssistantHandleSP KisPaintingAssistant::topLeft() const { return d->topLeft; } KisPaintingAssistantHandleSP KisPaintingAssistant::bottomLeft() { return d->bottomLeft; } const KisPaintingAssistantHandleSP KisPaintingAssistant::bottomLeft() const { return d->bottomLeft; } KisPaintingAssistantHandleSP KisPaintingAssistant::topRight() { return d->topRight; } const KisPaintingAssistantHandleSP KisPaintingAssistant::topRight() const { return d->topRight; } KisPaintingAssistantHandleSP KisPaintingAssistant::bottomRight() { return d->bottomRight; } const KisPaintingAssistantHandleSP KisPaintingAssistant::bottomRight() const { return d->bottomRight; } KisPaintingAssistantHandleSP KisPaintingAssistant::topMiddle() { return d->topMiddle; } const KisPaintingAssistantHandleSP KisPaintingAssistant::topMiddle() const { return d->topMiddle; } KisPaintingAssistantHandleSP KisPaintingAssistant::bottomMiddle() { return d->bottomMiddle; } const KisPaintingAssistantHandleSP KisPaintingAssistant::bottomMiddle() const { return d->bottomMiddle; } KisPaintingAssistantHandleSP KisPaintingAssistant::rightMiddle() { return d->rightMiddle; } const KisPaintingAssistantHandleSP KisPaintingAssistant::rightMiddle() const { return d->rightMiddle; } KisPaintingAssistantHandleSP KisPaintingAssistant::leftMiddle() { return d->leftMiddle; } const KisPaintingAssistantHandleSP KisPaintingAssistant::leftMiddle() const { return d->leftMiddle; } const QList& KisPaintingAssistant::handles() const { return d->handles; } QList KisPaintingAssistant::handles() { return d->handles; } const QList& KisPaintingAssistant::sideHandles() const { return d->sideHandles; } QList KisPaintingAssistant::sideHandles() { return d->sideHandles; } bool KisPaintingAssistant::areTwoPointsClose(const QPointF& pointOne, const QPointF& pointTwo) { int m_handleSize = 16; QRectF handlerect(pointTwo - QPointF(m_handleSize * 0.5, m_handleSize * 0.5), QSizeF(m_handleSize, m_handleSize)); return handlerect.contains(pointOne); } KisPaintingAssistantHandleSP KisPaintingAssistant::closestCornerHandleFromPoint(QPointF point) { if (!d->s->m_canvas) { return 0; } if (areTwoPointsClose(point, pixelToView(topLeft()->toPoint()))) { return topLeft(); } else if (areTwoPointsClose(point, pixelToView(topRight()->toPoint()))) { return topRight(); } else if (areTwoPointsClose(point, pixelToView(bottomLeft()->toPoint()))) { return bottomLeft(); } else if (areTwoPointsClose(point, pixelToView(bottomRight()->toPoint()))) { return bottomRight(); } return 0; } QPointF KisPaintingAssistant::pixelToView(const QPoint pixelCoords) const { QPointF documentCoord = d->s->m_canvas->image()->pixelToDocument(pixelCoords); return d->s->m_canvas->viewConverter()->documentToView(documentCoord); } double KisPaintingAssistant::norm2(const QPointF& p) { return p.x() * p.x() + p.y() * p.y(); } QList KisPaintingAssistant::cloneAssistantList(const QList &list) { QMap handleMap; QList clonedList; for (auto i = list.begin(); i != list.end(); ++i) { clonedList << (*i)->clone(handleMap); } return clonedList; } /* * KisPaintingAssistantFactory classes */ KisPaintingAssistantFactory::KisPaintingAssistantFactory() { } KisPaintingAssistantFactory::~KisPaintingAssistantFactory() { } KisPaintingAssistantFactoryRegistry::KisPaintingAssistantFactoryRegistry() { } KisPaintingAssistantFactoryRegistry::~KisPaintingAssistantFactoryRegistry() { Q_FOREACH (const QString &id, keys()) { delete get(id); } dbgRegistry << "deleting KisPaintingAssistantFactoryRegistry "; } KisPaintingAssistantFactoryRegistry* KisPaintingAssistantFactoryRegistry::instance() { return s_instance; } diff --git a/libs/ui/kis_painting_assistant.h b/libs/ui/kis_painting_assistant.h index 7bd94461fc..1a8b58eff1 100644 --- a/libs/ui/kis_painting_assistant.h +++ b/libs/ui/kis_painting_assistant.h @@ -1,243 +1,250 @@ /* * Copyright (c) 2008 Cyrille Berger * Copyright (c) 2017 Scott Petrovic * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef _KIS_PAINTING_ASSISTANT_H_ #define _KIS_PAINTING_ASSISTANT_H_ #include #include #include #include #include #include #include #include #include #include #include class QPainter; class QRect; class QRectF; class KoStore; class KisCoordinatesConverter; class KisCanvas2; class QDomDocument; class QDomElement; #include #include class KisPaintingAssistantHandle; typedef KisSharedPtr KisPaintingAssistantHandleSP; class KisPaintingAssistant; class QPainterPath; enum HandleType { NORMAL, SIDE, CORNER, VANISHING_POINT, ANCHOR }; /** * Represent an handle of the assistant, used to edit the parameters * of an assistants. Handles can be shared between assistants. */ class KRITAUI_EXPORT KisPaintingAssistantHandle : public QPointF, public KisShared { friend class KisPaintingAssistant; public: KisPaintingAssistantHandle(double x, double y); explicit KisPaintingAssistantHandle(QPointF p); KisPaintingAssistantHandle(const KisPaintingAssistantHandle&); ~KisPaintingAssistantHandle(); void mergeWith(KisPaintingAssistantHandleSP); void uncache(); KisPaintingAssistantHandle& operator=(const QPointF&); void setType(char type); char handleType() const; + /** + * Returns the pointer to the "chief" assistant, + * which is supposed to handle transformations of the + * handle, when all the assistants are transformed + */ + KisPaintingAssistant* chiefAssistant() const; + private: void registerAssistant(KisPaintingAssistant*); void unregisterAssistant(KisPaintingAssistant*); bool containsAssistant(KisPaintingAssistant*) const; private: struct Private; Private* const d; }; /** * A KisPaintingAssistant is an object that assist the drawing on the canvas. * With this class you can implement virtual equivalent to ruler or compas. */ class KRITAUI_EXPORT KisPaintingAssistant { public: KisPaintingAssistant(const QString& id, const QString& name); virtual ~KisPaintingAssistant(); virtual KisPaintingAssistantSP clone(QMap &handleMap) const = 0; const QString& id() const; const QString& name() const; bool isSnappingActive() const; void setSnappingActive(bool set); /** * Adjust the position given in parameter. * @param point the coordinates in point in the document reference * @param strokeBegin the coordinates of the beginning of the stroke */ virtual QPointF adjustPosition(const QPointF& point, const QPointF& strokeBegin) = 0; virtual void endStroke() { } virtual QPointF getEditorPosition() const = 0; // Returns editor widget position in document-space coordinates. virtual int numHandles() const = 0; void replaceHandle(KisPaintingAssistantHandleSP _handle, KisPaintingAssistantHandleSP _with); void addHandle(KisPaintingAssistantHandleSP handle, HandleType type); QPointF viewportConstrainedEditorPosition(const KisCoordinatesConverter* converter, const QSize editorSize); QColor effectiveAssistantColor() const; bool useCustomColor(); void setUseCustomColor(bool useCustomColor); void setAssistantCustomColor(QColor color); QColor assistantCustomColor(); void setAssistantGlobalColorCache(const QColor &color); virtual void drawAssistant(QPainter& gc, const QRectF& updateRect, const KisCoordinatesConverter *converter, bool cached = true,KisCanvas2 *canvas=0, bool assistantVisible=true, bool previewVisible=true); void uncache(); const QList& handles() const; QList handles(); const QList& sideHandles() const; QList sideHandles(); QByteArray saveXml( QMap &handleMap); virtual void saveCustomXml(QXmlStreamWriter* xml); //in case specific assistants have custom properties (like vanishing point) void loadXml(KoStore *store, QMap &handleMap, QString path); virtual bool loadCustomXml(QXmlStreamReader* xml); void saveXmlList(QDomDocument& doc, QDomElement& ssistantsElement, int count); void findPerspectiveAssistantHandleLocation(); KisPaintingAssistantHandleSP oppHandleOne(); /** * Get the topLeft, bottomLeft, topRight and BottomRight corners of the assistant * Some assistants like the perspective grid have custom logic built around certain handles */ const KisPaintingAssistantHandleSP topLeft() const; KisPaintingAssistantHandleSP topLeft(); const KisPaintingAssistantHandleSP topRight() const; KisPaintingAssistantHandleSP topRight(); const KisPaintingAssistantHandleSP bottomLeft() const; KisPaintingAssistantHandleSP bottomLeft(); const KisPaintingAssistantHandleSP bottomRight() const; KisPaintingAssistantHandleSP bottomRight(); const KisPaintingAssistantHandleSP topMiddle() const; KisPaintingAssistantHandleSP topMiddle(); const KisPaintingAssistantHandleSP rightMiddle() const; KisPaintingAssistantHandleSP rightMiddle(); const KisPaintingAssistantHandleSP leftMiddle() const; KisPaintingAssistantHandleSP leftMiddle(); const KisPaintingAssistantHandleSP bottomMiddle() const; KisPaintingAssistantHandleSP bottomMiddle(); // calculates whether a point is near one of the corner points of the assistant // returns: a corner point from the perspective assistant if the given node is close // only called once in code when calculating the perspective assistant KisPaintingAssistantHandleSP closestCornerHandleFromPoint(QPointF point); // determines if two points are close to each other // only used by the nodeNearPoint function (perspective grid assistant). bool areTwoPointsClose(const QPointF& pointOne, const QPointF& pointTwo); /// determines if the assistant has enough handles to be considered created /// new assistants get in a "creation" phase where they are currently being made on the canvas /// it will return false if we are in the middle of creating the assistant. virtual bool isAssistantComplete() const; /// Transform the assistant using the given \p tranform. Please note that \p transform /// should be in 'document' coordinate system. /// Used with image-wide transformations. virtual void transform(const QTransform &transform); public: /** * This will render the final output. The drawCache does rendering most of the time so be sure to check that */ void drawPath(QPainter& painter, const QPainterPath& path, bool drawActive=true); void drawPreview(QPainter& painter, const QPainterPath& path); static double norm2(const QPointF& p); protected: explicit KisPaintingAssistant(const KisPaintingAssistant &rhs, QMap &handleMap); virtual QRect boundingRect() const; /// performance layer where the graphics can be drawn from a cache instead of generated every render update virtual void drawCache(QPainter& gc, const KisCoordinatesConverter *converter, bool assistantVisible=true) = 0; void initHandles(QList _handles); QList m_handles; QPointF pixelToView(const QPoint pixelCoords) const; public: /// clones the list of assistants /// the originally shared handles will still be shared /// the cloned assistants do not share any handle with the original assistants static QList cloneAssistantList(const QList &list); private: struct Private; Private* const d; }; /** * Allow to create a painting assistant. */ class KRITAUI_EXPORT KisPaintingAssistantFactory { public: KisPaintingAssistantFactory(); virtual ~KisPaintingAssistantFactory(); virtual QString id() const = 0; virtual QString name() const = 0; virtual KisPaintingAssistant* createPaintingAssistant() const = 0; }; class KRITAUI_EXPORT KisPaintingAssistantFactoryRegistry : public KoGenericRegistry { public: KisPaintingAssistantFactoryRegistry(); ~KisPaintingAssistantFactoryRegistry() override; static KisPaintingAssistantFactoryRegistry* instance(); }; #endif diff --git a/libs/ui/widgets/kis_paintop_presets_popup.cpp b/libs/ui/widgets/kis_paintop_presets_popup.cpp index f73d1d260b..9a27502383 100644 --- a/libs/ui/widgets/kis_paintop_presets_popup.cpp +++ b/libs/ui/widgets/kis_paintop_presets_popup.cpp @@ -1,850 +1,850 @@ /* This file is part of the KDE project * Copyright (C) 2008 Boudewijn Rempt * Copyright (C) 2010 Lukáš Tvrdý * Copyright (C) 2011 Silvio Heinrich * * 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 "widgets/kis_paintop_presets_popup.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 "kis_config.h" #include "KisResourceServerProvider.h" #include "kis_lod_availability_widget.h" #include "kis_signal_auto_connection.h" #include // ones from brush engine selector #include #include struct KisPaintOpPresetsPopup::Private { public: Ui_WdgPaintOpSettings uiWdgPaintOpPresetSettings; QGridLayout *layout; KisPaintOpConfigWidget *settingsWidget; QFont smallFont; KisCanvasResourceProvider *resourceProvider; KisFavoriteResourceManager *favoriteResManager; bool detached; bool ignoreHideEvents; bool isCreatingBrushFromScratch = false; QSize minimumSettingsWidgetSize; QRect detachedGeometry; KisSignalAutoConnectionsStore widgetConnections; }; KisPaintOpPresetsPopup::KisPaintOpPresetsPopup(KisCanvasResourceProvider * resourceProvider, KisFavoriteResourceManager* favoriteResourceManager, KisPresetSaveWidget* savePresetWidget, QWidget * parent) : QWidget(parent) , m_d(new Private()) { setObjectName("KisPaintOpPresetsPopup"); setFont(KoDockRegistry::dockFont()); current_paintOpId = ""; m_d->resourceProvider = resourceProvider; m_d->favoriteResManager = favoriteResourceManager; m_d->uiWdgPaintOpPresetSettings.setupUi(this); m_d->layout = new QGridLayout(m_d->uiWdgPaintOpPresetSettings.frmOptionWidgetContainer); m_d->layout->setSizeConstraint(QLayout::SetFixedSize); m_d->uiWdgPaintOpPresetSettings.scratchPad->setupScratchPad(resourceProvider, Qt::white); m_d->uiWdgPaintOpPresetSettings.scratchPad->setCutoutOverlayRect(QRect(25, 25, 200, 200)); m_d->uiWdgPaintOpPresetSettings.dirtyPresetIndicatorButton->setToolTip(i18n("The settings for this preset have changed from their default.")); m_d->uiWdgPaintOpPresetSettings.showPresetsButton->setToolTip(i18n("Toggle showing presets")); m_d->uiWdgPaintOpPresetSettings.showScratchpadButton->setToolTip(i18n("Toggle showing scratchpad")); m_d->uiWdgPaintOpPresetSettings.reloadPresetButton->setToolTip(i18n("Reload the brush preset")); m_d->uiWdgPaintOpPresetSettings.renameBrushPresetButton->setToolTip(i18n("Rename the brush preset")); // creating a new preset from scratch. Part of the brush presets area // the menu options will get filled up later when we are generating all available paintops // in the filter drop-down newPresetBrushEnginesMenu = new QMenu(); // overwrite existing preset and saving a new preset use the same dialog saveDialog = savePresetWidget; saveDialog->scratchPadSetup(resourceProvider); saveDialog->setFavoriteResourceManager(m_d->favoriteResManager); // this is needed when saving the preset saveDialog->hide(); // the area on the brush editor for renaming the brush. make sure edit fields are hidden by default toggleBrushRenameUIActive(false); // DETAIL and THUMBNAIL view changer QMenu* menu = new QMenu(this); menu->setStyleSheet("margin: 6px"); menu->addSection(i18nc("@title Which elements to display (e.g., thumbnails or details)", "Display")); QActionGroup *actionGroup = new QActionGroup(this); KisPresetChooser::ViewMode mode = (KisPresetChooser::ViewMode)KisConfig(true).presetChooserViewMode(); QAction* action = menu->addAction(KisIconUtils::loadIcon("view-preview"), i18n("Thumbnails"), m_d->uiWdgPaintOpPresetSettings.presetWidget, SLOT(slotThumbnailMode())); action->setCheckable(true); action->setChecked(mode == KisPresetChooser::THUMBNAIL); action->setActionGroup(actionGroup); action = menu->addAction(KisIconUtils::loadIcon("view-list-details"), i18n("Details"), m_d->uiWdgPaintOpPresetSettings.presetWidget, SLOT(slotDetailMode())); action->setCheckable(true); action->setChecked(mode == KisPresetChooser::DETAIL); action->setActionGroup(actionGroup); // add horizontal slider for the icon size QSlider* iconSizeSlider = new QSlider(this); iconSizeSlider->setOrientation(Qt::Horizontal); iconSizeSlider->setRange(30, 80); iconSizeSlider->setValue(m_d->uiWdgPaintOpPresetSettings.presetWidget->iconSize()); iconSizeSlider->setMinimumHeight(20); iconSizeSlider->setMinimumWidth(40); iconSizeSlider->setTickInterval(10); QWidgetAction *sliderAction= new QWidgetAction(this); sliderAction->setDefaultWidget(iconSizeSlider); menu->addSection(i18n("Icon Size")); menu->addAction(sliderAction); // configure the button and assign menu m_d->uiWdgPaintOpPresetSettings.presetChangeViewToolButton->setMenu(menu); m_d->uiWdgPaintOpPresetSettings.presetChangeViewToolButton->setPopupMode(QToolButton::InstantPopup); // loading preset from scratch option m_d->uiWdgPaintOpPresetSettings.newPresetEngineButton->setPopupMode(QToolButton::InstantPopup); // show/hide buttons KisConfig cfg(true); m_d->uiWdgPaintOpPresetSettings.showScratchpadButton->setCheckable(true); m_d->uiWdgPaintOpPresetSettings.showScratchpadButton->setChecked(cfg.scratchpadVisible()); if (cfg.scratchpadVisible()) { slotSwitchScratchpad(true); // show scratchpad } else { slotSwitchScratchpad(false); } m_d->uiWdgPaintOpPresetSettings.showPresetsButton->setCheckable(true); m_d->uiWdgPaintOpPresetSettings.showPresetsButton->setChecked(false); slotSwitchShowPresets(false); // hide presets by default // Connections connect(m_d->uiWdgPaintOpPresetSettings.paintPresetIcon, SIGNAL(clicked()), m_d->uiWdgPaintOpPresetSettings.scratchPad, SLOT(paintPresetImage())); connect(saveDialog, SIGNAL(resourceSelected(KoResource*)), this, SLOT(resourceSelected(KoResource*))); connect (m_d->uiWdgPaintOpPresetSettings.renameBrushPresetButton, SIGNAL(clicked(bool)), this, SLOT(slotRenameBrushActivated())); connect (m_d->uiWdgPaintOpPresetSettings.cancelBrushNameUpdateButton, SIGNAL(clicked(bool)), this, SLOT(slotRenameBrushDeactivated())); connect(m_d->uiWdgPaintOpPresetSettings.updateBrushNameButton, SIGNAL(clicked(bool)), this, SLOT(slotSaveRenameCurrentBrush())); connect(m_d->uiWdgPaintOpPresetSettings.renameBrushNameTextField, SIGNAL(returnPressed()), SLOT(slotSaveRenameCurrentBrush())); connect(iconSizeSlider, SIGNAL(sliderMoved(int)), m_d->uiWdgPaintOpPresetSettings.presetWidget, SLOT(slotSetIconSize(int))); connect(iconSizeSlider, SIGNAL(sliderReleased()), m_d->uiWdgPaintOpPresetSettings.presetWidget, SLOT(slotSaveIconSize())); connect(m_d->uiWdgPaintOpPresetSettings.showScratchpadButton, SIGNAL(clicked(bool)), this, SLOT(slotSwitchScratchpad(bool))); connect(m_d->uiWdgPaintOpPresetSettings.showPresetsButton, SIGNAL(clicked(bool)), this, SLOT(slotSwitchShowPresets(bool))); connect(m_d->uiWdgPaintOpPresetSettings.eraseScratchPad, SIGNAL(clicked()), m_d->uiWdgPaintOpPresetSettings.scratchPad, SLOT(fillDefault())); connect(m_d->uiWdgPaintOpPresetSettings.fillLayer, SIGNAL(clicked()), m_d->uiWdgPaintOpPresetSettings.scratchPad, SLOT(fillLayer())); connect(m_d->uiWdgPaintOpPresetSettings.fillGradient, SIGNAL(clicked()), m_d->uiWdgPaintOpPresetSettings.scratchPad, SLOT(fillGradient())); connect(m_d->uiWdgPaintOpPresetSettings.fillSolid, SIGNAL(clicked()), m_d->uiWdgPaintOpPresetSettings.scratchPad, SLOT(fillBackground())); m_d->settingsWidget = 0; setSizePolicy(QSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed)); connect(m_d->uiWdgPaintOpPresetSettings.saveBrushPresetButton, SIGNAL(clicked()), this, SLOT(slotSaveBrushPreset())); connect(m_d->uiWdgPaintOpPresetSettings.saveNewBrushPresetButton, SIGNAL(clicked()), this, SLOT(slotSaveNewBrushPreset())); connect(m_d->uiWdgPaintOpPresetSettings.reloadPresetButton, SIGNAL(clicked()), this, SIGNAL(reloadPresetClicked())); connect(m_d->uiWdgPaintOpPresetSettings.dirtyPresetCheckBox, SIGNAL(toggled(bool)), this, SIGNAL(dirtyPresetToggled(bool))); connect(m_d->uiWdgPaintOpPresetSettings.eraserBrushSizeCheckBox, SIGNAL(toggled(bool)), this, SIGNAL(eraserBrushSizeToggled(bool))); connect(m_d->uiWdgPaintOpPresetSettings.eraserBrushOpacityCheckBox, SIGNAL(toggled(bool)), this, SIGNAL(eraserBrushOpacityToggled(bool))); // preset widget connections connect(m_d->uiWdgPaintOpPresetSettings.presetWidget->smallPresetChooser, SIGNAL(resourceSelected(KoResource*)), this, SIGNAL(signalResourceSelected(KoResource*))); connect(m_d->uiWdgPaintOpPresetSettings.reloadPresetButton, SIGNAL(clicked()), m_d->uiWdgPaintOpPresetSettings.presetWidget->smallPresetChooser, SLOT(updateViewSettings())); connect(m_d->uiWdgPaintOpPresetSettings.reloadPresetButton, SIGNAL(clicked()), SLOT(slotUpdatePresetSettings())); m_d->detached = false; m_d->ignoreHideEvents = false; m_d->minimumSettingsWidgetSize = QSize(0, 0); m_d->detachedGeometry = QRect(100, 100, 0, 0); m_d->uiWdgPaintOpPresetSettings.dirtyPresetCheckBox->setChecked(cfg.useDirtyPresets()); m_d->uiWdgPaintOpPresetSettings.eraserBrushSizeCheckBox->setChecked(cfg.useEraserBrushSize()); m_d->uiWdgPaintOpPresetSettings.eraserBrushOpacityCheckBox->setChecked(cfg.useEraserBrushOpacity()); m_d->uiWdgPaintOpPresetSettings.wdgLodAvailability->setCanvasResourceManager(resourceProvider->resourceManager()); connect(resourceProvider->resourceManager(), SIGNAL(canvasResourceChanged(int,QVariant)), SLOT(slotResourceChanged(int,QVariant))); connect(m_d->uiWdgPaintOpPresetSettings.wdgLodAvailability, SIGNAL(sigUserChangedLodAvailability(bool)), SLOT(slotLodAvailabilityChanged(bool))); connect(m_d->uiWdgPaintOpPresetSettings.wdgLodAvailability, SIGNAL(sigUserChangedLodThreshold(qreal)), SLOT(slotLodThresholdChanged(qreal))); slotResourceChanged(KisCanvasResourceProvider::LodAvailability, resourceProvider->resourceManager()-> resource(KisCanvasResourceProvider::LodAvailability)); slotResourceChanged(KisCanvasResourceProvider::LodSizeThreshold, resourceProvider->resourceManager()-> resource(KisCanvasResourceProvider::LodSizeThreshold)); connect(m_d->uiWdgPaintOpPresetSettings.brushEgineComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(slotUpdatePaintOpFilter())); connect(m_d->uiWdgPaintOpPresetSettings.bnBlacklistPreset, SIGNAL(clicked()), this, SLOT(slotBlackListCurrentPreset())); updateThemedIcons(); // setup things like the scene construct images, layers, etc that is a one-time thing m_d->uiWdgPaintOpPresetSettings.liveBrushPreviewView->setup(); } void KisPaintOpPresetsPopup::slotBlackListCurrentPreset() { KisPaintOpPresetResourceServer * rServer = KisResourceServerProvider::instance()->paintOpPresetServer(); KisPaintOpPresetSP curPreset = m_d->resourceProvider->currentPreset(); if (rServer->resourceByName(curPreset->name())) { rServer->removeResourceAndBlacklist(curPreset); } } void KisPaintOpPresetsPopup::slotRenameBrushActivated() { toggleBrushRenameUIActive(true); } void KisPaintOpPresetsPopup::slotRenameBrushDeactivated() { toggleBrushRenameUIActive(false); } void KisPaintOpPresetsPopup::toggleBrushRenameUIActive(bool isRenaming) { // This function doesn't really do anything except get the UI in a state to rename a brush preset m_d->uiWdgPaintOpPresetSettings.renameBrushNameTextField->setVisible(isRenaming); m_d->uiWdgPaintOpPresetSettings.updateBrushNameButton->setVisible(isRenaming); m_d->uiWdgPaintOpPresetSettings.cancelBrushNameUpdateButton->setVisible(isRenaming); // hide these below areas while renaming m_d->uiWdgPaintOpPresetSettings.currentBrushNameLabel->setVisible(!isRenaming); m_d->uiWdgPaintOpPresetSettings.renameBrushPresetButton->setVisible(!isRenaming); m_d->uiWdgPaintOpPresetSettings.saveBrushPresetButton->setEnabled(!isRenaming); m_d->uiWdgPaintOpPresetSettings.saveBrushPresetButton->setVisible(!isRenaming); m_d->uiWdgPaintOpPresetSettings.saveNewBrushPresetButton->setEnabled(!isRenaming); m_d->uiWdgPaintOpPresetSettings.saveNewBrushPresetButton->setVisible(!isRenaming); // if the presets area is shown, only then can you show/hide the load default brush // need to think about weird state when you are in the middle of renaming a brush // what happens if you try to change presets. maybe we should auto-hide (or disable) // the presets area in this case if (m_d->uiWdgPaintOpPresetSettings.presetWidget->isVisible()) { m_d->uiWdgPaintOpPresetSettings.newPresetEngineButton->setVisible(!isRenaming); m_d->uiWdgPaintOpPresetSettings.bnBlacklistPreset->setVisible(!isRenaming); } } void KisPaintOpPresetsPopup::slotSaveRenameCurrentBrush() { // if you are renaming a brush, that is different than updating the settings // make sure we are in a clean state before renaming. This logic might change, // but that is what we are going with for now emit reloadPresetClicked(); m_d->favoriteResManager->setBlockUpdates(true); // get a reference to the existing (and new) file name and path that we are working with KisPaintOpPresetSP curPreset = m_d->resourceProvider->currentPreset(); if (!curPreset) return; KisPaintOpPresetResourceServer * rServer = KisResourceServerProvider::instance()->paintOpPresetServer(); QString saveLocation = rServer->saveLocation(); QString originalPresetName = curPreset->name(); QString renamedPresetName = m_d->uiWdgPaintOpPresetSettings.renameBrushNameTextField->text(); QString originalPresetPathAndFile = saveLocation + originalPresetName + curPreset->defaultFileExtension(); QString renamedPresetPathAndFile = saveLocation + renamedPresetName + curPreset->defaultFileExtension(); // create a new brush preset with the name specified and add to resource provider KisPaintOpPresetSP newPreset = curPreset->clone(); newPreset->setFilename(renamedPresetPathAndFile); // this also contains the path newPreset->setName(renamedPresetName); newPreset->setImage(curPreset->image()); // use existing thumbnail (might not need to do this) newPreset->setDirty(false); newPreset->setValid(true); rServer->addResource(newPreset); resourceSelected(newPreset.data()); // refresh and select our freshly renamed resource // Now blacklist the original file if (rServer->resourceByName(originalPresetName)) { rServer->removeResourceAndBlacklist(curPreset); } m_d->favoriteResManager->setBlockUpdates(false); toggleBrushRenameUIActive(false); // this returns the UI to its original state after saving slotUpdatePresetSettings(); // update visibility of dirty preset and icon } void KisPaintOpPresetsPopup::slotResourceChanged(int key, const QVariant &value) { if (key == KisCanvasResourceProvider::LodAvailability) { m_d->uiWdgPaintOpPresetSettings.wdgLodAvailability->slotUserChangedLodAvailability(value.toBool()); } else if (key == KisCanvasResourceProvider::LodSizeThreshold) { m_d->uiWdgPaintOpPresetSettings.wdgLodAvailability->slotUserChangedLodThreshold(value.toDouble()); } else if (key == KisCanvasResourceProvider::Size) { m_d->uiWdgPaintOpPresetSettings.wdgLodAvailability->slotUserChangedSize(value.toDouble()); } } void KisPaintOpPresetsPopup::slotLodAvailabilityChanged(bool value) { m_d->resourceProvider->resourceManager()->setResource(KisCanvasResourceProvider::LodAvailability, QVariant(value)); } void KisPaintOpPresetsPopup::slotLodThresholdChanged(qreal value) { m_d->resourceProvider->resourceManager()->setResource(KisCanvasResourceProvider::LodSizeThreshold, QVariant(value)); } KisPaintOpPresetsPopup::~KisPaintOpPresetsPopup() { if (m_d->settingsWidget) { m_d->layout->removeWidget(m_d->settingsWidget); m_d->settingsWidget->hide(); m_d->settingsWidget->setParent(0); m_d->settingsWidget = 0; } delete m_d; delete newPresetBrushEnginesMenu; } void KisPaintOpPresetsPopup::setPaintOpSettingsWidget(QWidget * widget) { if (m_d->settingsWidget) { m_d->layout->removeWidget(m_d->settingsWidget); m_d->uiWdgPaintOpPresetSettings.frmOptionWidgetContainer->updateGeometry(); } m_d->layout->update(); updateGeometry(); m_d->widgetConnections.clear(); m_d->settingsWidget = 0; if (widget) { m_d->settingsWidget = dynamic_cast(widget); KIS_ASSERT_RECOVER_RETURN(m_d->settingsWidget); KisConfig cfg(true); if (m_d->settingsWidget->supportScratchBox() && cfg.scratchpadVisible()) { slotSwitchScratchpad(true); } else { slotSwitchScratchpad(false); } m_d->widgetConnections.addConnection(m_d->settingsWidget, SIGNAL(sigConfigurationItemChanged()), this, SLOT(slotUpdateLodAvailability())); widget->setFont(m_d->smallFont); QSize hint = widget->sizeHint(); m_d->minimumSettingsWidgetSize = QSize(qMax(hint.width(), m_d->minimumSettingsWidgetSize.width()), qMax(hint.height(), m_d->minimumSettingsWidgetSize.height())); widget->setMinimumSize(m_d->minimumSettingsWidgetSize); m_d->layout->addWidget(widget); // hook up connections that will monitor if our preset is dirty or not. Show a notification if it is if (m_d->resourceProvider && m_d->resourceProvider->currentPreset() ) { KisPaintOpPresetSP preset = m_d->resourceProvider->currentPreset(); m_d->widgetConnections.addConnection(preset->updateProxy(), SIGNAL(sigSettingsChanged()), this, SLOT(slotUpdatePresetSettings())); } m_d->layout->update(); widget->show(); } slotUpdateLodAvailability(); } void KisPaintOpPresetsPopup::slotUpdateLodAvailability() { if (!m_d->settingsWidget) return; KisPaintopLodLimitations l = m_d->settingsWidget->lodLimitations(); m_d->uiWdgPaintOpPresetSettings.wdgLodAvailability->setLimitations(l); } QImage KisPaintOpPresetsPopup::cutOutOverlay() { return m_d->uiWdgPaintOpPresetSettings.scratchPad->cutoutOverlay(); } void KisPaintOpPresetsPopup::contextMenuEvent(QContextMenuEvent *e) { Q_UNUSED(e); } void KisPaintOpPresetsPopup::switchDetached(bool show) { if (parentWidget()) { m_d->detached = !m_d->detached; if (m_d->detached) { m_d->ignoreHideEvents = true; if (show) { parentWidget()->show(); } m_d->ignoreHideEvents = false; } else { parentWidget()->hide(); } KisConfig cfg(false); cfg.setPaintopPopupDetached(m_d->detached); } } void KisPaintOpPresetsPopup::setCreatingBrushFromScratch(bool enabled) { m_d->isCreatingBrushFromScratch = enabled; } void KisPaintOpPresetsPopup::resourceSelected(KoResource* resource) { // this gets called every time the brush editor window is opened // TODO: this gets called multiple times whenever the preset is changed in the presets area // the connections probably need to be thought about with this a bit more to keep things in sync m_d->uiWdgPaintOpPresetSettings.presetWidget->smallPresetChooser->setCurrentResource(resource); // find the display name of the brush engine and append it to the selected preset display QString currentBrushEngineName; QPixmap currentBrushEngineIcon = QPixmap(26, 26); currentBrushEngineIcon.fill(Qt::transparent); for(int i=0; i < sortedBrushEnginesList.length(); i++) { if (sortedBrushEnginesList.at(i).id == currentPaintOpId() ) { currentBrushEngineName = sortedBrushEnginesList.at(i).name; currentBrushEngineIcon = sortedBrushEnginesList.at(i).icon.pixmap(26, 26); } } // brush names have underscores as part of the file name (to help with building). We don't really need underscores // when viewing the names, so replace them with spaces QString formattedBrushName = resource->name().replace("_", " "); m_d->uiWdgPaintOpPresetSettings.currentBrushNameLabel->setText(formattedBrushName); m_d->uiWdgPaintOpPresetSettings.currentBrushEngineLabel->setText(i18nc("%1 is the name of a brush engine", "%1 Engine", currentBrushEngineName)); m_d->uiWdgPaintOpPresetSettings.currentBrushEngineIcon->setPixmap(currentBrushEngineIcon); m_d->uiWdgPaintOpPresetSettings.renameBrushNameTextField->setText(resource->name()); // use file name // get the preset image and pop it into the thumbnail area on the top of the brush editor m_d->uiWdgPaintOpPresetSettings.presetThumbnailicon->setPixmap(QPixmap::fromImage(resource->image().scaled(55, 55, Qt::KeepAspectRatio, Qt::SmoothTransformation))); toggleBrushRenameUIActive(false); // reset the UI state of renaming a brush if we are changing brush presets slotUpdatePresetSettings(); // check to see if the dirty preset icon needs to be shown } bool variantLessThan(const KisPaintOpInfo v1, const KisPaintOpInfo v2) { return v1.priority < v2.priority; } void KisPaintOpPresetsPopup::setPaintOpList(const QList< KisPaintOpFactory* >& list) { m_d->uiWdgPaintOpPresetSettings.brushEgineComboBox->clear(); // reset combobox list just in case // create a new list so we can sort it and populate the brush engine combo box sortedBrushEnginesList.clear(); // just in case this function is called again, don't keep adding to the list for(int i=0; i < list.length(); i++) { KisPaintOpInfo paintOpInfo; paintOpInfo.id = list.at(i)->id(); paintOpInfo.name = list.at(i)->name(); paintOpInfo.icon = list.at(i)->icon(); paintOpInfo.priority = list.at(i)->priority(); sortedBrushEnginesList.append(paintOpInfo); } std::stable_sort(sortedBrushEnginesList.begin(), sortedBrushEnginesList.end(), variantLessThan ); // add an "All" option at the front to show all presets QPixmap emptyPixmap = QPixmap(22,22); emptyPixmap.fill(Qt::transparent); // if we create a new brush from scratch, we need a full list of paintops to choose from // we don't want "All", so populate the list before that is added newPresetBrushEnginesMenu->actions().clear(); // clean out list in case we run this again newBrushEngineOptions.clear(); for (int j = 0; j < sortedBrushEnginesList.length(); j++) { KisAction * newEngineAction = static_cast( newPresetBrushEnginesMenu->addAction(sortedBrushEnginesList[j].name)); newEngineAction->setObjectName(sortedBrushEnginesList[j].id); // we need the ID for changing the paintop when action triggered newEngineAction->setIcon(sortedBrushEnginesList[j].icon); newBrushEngineOptions.append(newEngineAction); connect(newEngineAction, SIGNAL(triggered()), this, SLOT(slotCreateNewBrushPresetEngine())); } m_d->uiWdgPaintOpPresetSettings.newPresetEngineButton->setMenu(newPresetBrushEnginesMenu); // fill the list into the brush combo box sortedBrushEnginesList.push_front(KisPaintOpInfo(QString("all_options"), i18n("All"), QString(""), QIcon(emptyPixmap), 0 )); for (int m = 0; m < sortedBrushEnginesList.length(); m++) { m_d->uiWdgPaintOpPresetSettings.brushEgineComboBox->addItem(sortedBrushEnginesList[m].icon, sortedBrushEnginesList[m].name, QVariant(sortedBrushEnginesList[m].id)); } } void KisPaintOpPresetsPopup::setCurrentPaintOpId(const QString& paintOpId) { current_paintOpId = paintOpId; } QString KisPaintOpPresetsPopup::currentPaintOpId() { return current_paintOpId; } void KisPaintOpPresetsPopup::setPresetImage(const QImage& image) { m_d->uiWdgPaintOpPresetSettings.scratchPad->setPresetImage(image); saveDialog->brushPresetThumbnailWidget->setPresetImage(image); } void KisPaintOpPresetsPopup::hideEvent(QHideEvent *event) { if (m_d->ignoreHideEvents) { return; } if (m_d->detached) { m_d->detachedGeometry = window()->geometry(); } QWidget::hideEvent(event); } void KisPaintOpPresetsPopup::showEvent(QShowEvent *) { if (m_d->detached) { window()->setGeometry(m_d->detachedGeometry); } emit brushEditorShown(); } void KisPaintOpPresetsPopup::resizeEvent(QResizeEvent* event) { QWidget::resizeEvent(event); if (parentWidget()) { // Make sure resizing doesn't push this widget out of the screen QRect screenRect = QApplication::desktop()->availableGeometry(this); QRect newPositionRect = kisEnsureInRect(parentWidget()->geometry(), screenRect); parentWidget()->setGeometry(newPositionRect); } } bool KisPaintOpPresetsPopup::detached() const { return m_d->detached; } void KisPaintOpPresetsPopup::slotSwitchScratchpad(bool visible) { // hide all the internal controls except the toggle button m_d->uiWdgPaintOpPresetSettings.scratchPad->setVisible(visible); m_d->uiWdgPaintOpPresetSettings.paintPresetIcon->setVisible(visible); m_d->uiWdgPaintOpPresetSettings.fillGradient->setVisible(visible); m_d->uiWdgPaintOpPresetSettings.fillLayer->setVisible(visible); m_d->uiWdgPaintOpPresetSettings.fillSolid->setVisible(visible); m_d->uiWdgPaintOpPresetSettings.eraseScratchPad->setVisible(visible); m_d->uiWdgPaintOpPresetSettings.scratchpadSidebarLabel->setVisible(visible); if (visible) { m_d->uiWdgPaintOpPresetSettings.showScratchpadButton->setIcon(KisIconUtils::loadIcon("arrow-left")); } else { m_d->uiWdgPaintOpPresetSettings.showScratchpadButton->setIcon(KisIconUtils::loadIcon("arrow-right")); } KisConfig cfg(false); cfg.setScratchpadVisible(visible); } void KisPaintOpPresetsPopup::slotSwitchShowEditor(bool visible) { m_d->uiWdgPaintOpPresetSettings.brushEditorSettingsControls->setVisible(visible); } void KisPaintOpPresetsPopup::slotSwitchShowPresets(bool visible) { m_d->uiWdgPaintOpPresetSettings.presetWidget->setVisible(visible); m_d->uiWdgPaintOpPresetSettings.presetChangeViewToolButton->setVisible(visible); m_d->uiWdgPaintOpPresetSettings.brushEgineComboBox->setVisible(visible); m_d->uiWdgPaintOpPresetSettings.engineFilterLabel->setVisible(visible); m_d->uiWdgPaintOpPresetSettings.presetsSidebarLabel->setVisible(visible); m_d->uiWdgPaintOpPresetSettings.newPresetEngineButton->setVisible(visible); m_d->uiWdgPaintOpPresetSettings.bnBlacklistPreset->setVisible(visible); // we only want a spacer to work when the toggle icon is present. Otherwise the list of presets will shrink // which is something we don't want if (visible) { m_d->uiWdgPaintOpPresetSettings.presetsSpacer->changeSize(0,0, QSizePolicy::Ignored,QSizePolicy::Ignored); m_d->uiWdgPaintOpPresetSettings.showPresetsButton->setIcon(KisIconUtils::loadIcon("arrow-right")); } else { m_d->uiWdgPaintOpPresetSettings.presetsSpacer->changeSize(0,0, QSizePolicy::MinimumExpanding,QSizePolicy::MinimumExpanding); m_d->uiWdgPaintOpPresetSettings.showPresetsButton->setIcon(KisIconUtils::loadIcon("arrow-left")); } } void KisPaintOpPresetsPopup::slotUpdatePaintOpFilter() { QVariant userData = m_d->uiWdgPaintOpPresetSettings.brushEgineComboBox->currentData(); // grab paintOpID from data QString filterPaintOpId = userData.toString(); if (filterPaintOpId == "all_options") { filterPaintOpId = ""; } m_d->uiWdgPaintOpPresetSettings.presetWidget->setPresetFilter(filterPaintOpId); } void KisPaintOpPresetsPopup::slotSaveBrushPreset() { // here we are assuming that people want to keep their existing preset icon. We will just update the // settings and save a new copy with the same name. // there is a dialog with save options, but we don't need to show it in this situation saveDialog->useNewBrushDialog(false); // this mostly just makes sure we keep the existing brush preset name when saving saveDialog->loadExistingThumbnail(); // This makes sure we use the existing preset icon when updating the existing brush preset saveDialog->savePreset(); // refresh the view settings so the brush doesn't appear dirty slotUpdatePresetSettings(); } void KisPaintOpPresetsPopup::slotSaveNewBrushPreset() { saveDialog->useNewBrushDialog(true); saveDialog->saveScratchPadThumbnailArea(m_d->uiWdgPaintOpPresetSettings.scratchPad->cutoutOverlay()); saveDialog->showDialog(); } void KisPaintOpPresetsPopup::slotCreateNewBrushPresetEngine() { emit createPresetFromScratch(sender()->objectName()); } void KisPaintOpPresetsPopup::updateViewSettings() { m_d->uiWdgPaintOpPresetSettings.presetWidget->smallPresetChooser->updateViewSettings(); } void KisPaintOpPresetsPopup::currentPresetChanged(KisPaintOpPresetSP preset) { if (preset) { m_d->uiWdgPaintOpPresetSettings.presetWidget->smallPresetChooser->setCurrentResource(preset.data()); setCurrentPaintOpId(preset->paintOp().id()); } } void KisPaintOpPresetsPopup::updateThemedIcons() { m_d->uiWdgPaintOpPresetSettings.paintPresetIcon->setIcon(KisIconUtils::loadIcon("krita_tool_freehand")); m_d->uiWdgPaintOpPresetSettings.fillLayer->setIcon(KisIconUtils::loadIcon("document-new")); m_d->uiWdgPaintOpPresetSettings.fillLayer->hide(); m_d->uiWdgPaintOpPresetSettings.fillGradient->setIcon(KisIconUtils::loadIcon("krita_tool_gradient")); m_d->uiWdgPaintOpPresetSettings.fillSolid->setIcon(KisIconUtils::loadIcon("krita_tool_color_fill")); m_d->uiWdgPaintOpPresetSettings.eraseScratchPad->setIcon(KisIconUtils::loadIcon("edit-delete")); m_d->uiWdgPaintOpPresetSettings.newPresetEngineButton->setIcon(KisIconUtils::loadIcon("addlayer")); m_d->uiWdgPaintOpPresetSettings.bnBlacklistPreset->setIcon(KisIconUtils::loadIcon("deletelayer")); m_d->uiWdgPaintOpPresetSettings.reloadPresetButton->setIcon(KisIconUtils::loadIcon("updateColorize")); // refresh icon m_d->uiWdgPaintOpPresetSettings.renameBrushPresetButton->setIcon(KisIconUtils::loadIcon("dirty-preset")); // edit icon m_d->uiWdgPaintOpPresetSettings.dirtyPresetIndicatorButton->setIcon(KisIconUtils::loadIcon("warning")); m_d->uiWdgPaintOpPresetSettings.newPresetEngineButton->setIcon(KisIconUtils::loadIcon("addlayer")); m_d->uiWdgPaintOpPresetSettings.bnBlacklistPreset->setIcon(KisIconUtils::loadIcon("deletelayer")); m_d->uiWdgPaintOpPresetSettings.presetChangeViewToolButton->setIcon(KisIconUtils::loadIcon("configure")); // if we cannot see the "Preset label", we know it is not visible // maybe this can also be stored in the config like the scratchpad? if (m_d->uiWdgPaintOpPresetSettings.presetsSidebarLabel->isVisible()) { m_d->uiWdgPaintOpPresetSettings.presetsSpacer->changeSize(0,0, QSizePolicy::Ignored,QSizePolicy::Ignored); m_d->uiWdgPaintOpPresetSettings.showPresetsButton->setIcon(KisIconUtils::loadIcon("arrow-right")); } else { m_d->uiWdgPaintOpPresetSettings.presetsSpacer->changeSize(0,0, QSizePolicy::MinimumExpanding,QSizePolicy::MinimumExpanding); m_d->uiWdgPaintOpPresetSettings.showPresetsButton->setIcon(KisIconUtils::loadIcon("arrow-left")); } // we store whether the scratchpad if visible in the config. KisConfig cfg(true); if (cfg.scratchpadVisible()) { m_d->uiWdgPaintOpPresetSettings.showScratchpadButton->setIcon(KisIconUtils::loadIcon("arrow-left")); } else { m_d->uiWdgPaintOpPresetSettings.showScratchpadButton->setIcon(KisIconUtils::loadIcon("arrow-right")); } } void KisPaintOpPresetsPopup::slotUpdatePresetSettings() { if (!m_d->resourceProvider) { return; } if (!m_d->resourceProvider->currentPreset()) { return; } // hide options on UI if we are creating a brush preset from scratch to prevent confusion if (m_d->isCreatingBrushFromScratch) { m_d->uiWdgPaintOpPresetSettings.dirtyPresetIndicatorButton->setVisible(false); m_d->uiWdgPaintOpPresetSettings.reloadPresetButton->setVisible(false); m_d->uiWdgPaintOpPresetSettings.saveBrushPresetButton->setVisible(false); m_d->uiWdgPaintOpPresetSettings.renameBrushPresetButton->setVisible(false); } else { bool isPresetDirty = m_d->resourceProvider->currentPreset()->isDirty(); // don't need to reload or overwrite a clean preset m_d->uiWdgPaintOpPresetSettings.dirtyPresetIndicatorButton->setVisible(isPresetDirty); m_d->uiWdgPaintOpPresetSettings.reloadPresetButton->setVisible(isPresetDirty); m_d->uiWdgPaintOpPresetSettings.saveBrushPresetButton->setEnabled(isPresetDirty); m_d->uiWdgPaintOpPresetSettings.renameBrushPresetButton->setVisible(true); } // update live preview area in here... // don't update the live preview if the widget is not visible. if (m_d->uiWdgPaintOpPresetSettings.liveBrushPreviewView->isVisible()) { m_d->uiWdgPaintOpPresetSettings.liveBrushPreviewView->setCurrentPreset(m_d->resourceProvider->currentPreset()); - m_d->uiWdgPaintOpPresetSettings.liveBrushPreviewView->updateStroke(); + m_d->uiWdgPaintOpPresetSettings.liveBrushPreviewView->requestUpdateStroke(); } } diff --git a/libs/ui/widgets/kis_preset_live_preview_view.cpp b/libs/ui/widgets/kis_preset_live_preview_view.cpp index 9529a39996..33b97fc675 100644 --- a/libs/ui/widgets/kis_preset_live_preview_view.cpp +++ b/libs/ui/widgets/kis_preset_live_preview_view.cpp @@ -1,345 +1,381 @@ /* * Copyright (c) 2017 Scott Petrovic * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include #include #include #include "kis_paintop_settings.h" #include #include #include "KisAsyncronousStrokeUpdateHelper.h" #include -KisPresetLivePreviewView::KisPresetLivePreviewView(QWidget *parent): QGraphicsView(parent) +KisPresetLivePreviewView::KisPresetLivePreviewView(QWidget *parent) + : QGraphicsView(parent), + m_updateCompressor(100, KisSignalCompressor::FIRST_ACTIVE) { - + connect(&m_updateCompressor, SIGNAL(timeout()), SLOT(updateStroke())); } KisPresetLivePreviewView::~KisPresetLivePreviewView() { delete m_noPreviewText; delete m_brushPreviewScene; } void KisPresetLivePreviewView::setup() { // initializing to 0 helps check later if they actually have something in them m_noPreviewText = 0; m_sceneImageItem = 0; setHorizontalScrollBarPolicy ( Qt::ScrollBarAlwaysOff ); setVerticalScrollBarPolicy ( Qt::ScrollBarAlwaysOff ); // layer image needs to be big enough to get an entire stroke of data m_canvasSize.setWidth(this->width()); m_canvasSize.setHeight(this->height()); m_canvasCenterPoint.setX(m_canvasSize.width()*0.5); m_canvasCenterPoint.setY(m_canvasSize.height()*0.5); m_colorSpace = KoColorSpaceRegistry::instance()->rgb8(); m_image = new KisImage(0, m_canvasSize.width(), m_canvasSize.height(), m_colorSpace, "stroke sample image"); m_layer = new KisPaintLayer(m_image, "livePreviewStrokeSample", OPACITY_OPAQUE_U8, m_colorSpace); // set scene for the view m_brushPreviewScene = new QGraphicsScene(); setScene(m_brushPreviewScene); } void KisPresetLivePreviewView::setCurrentPreset(KisPaintOpPresetSP preset) { m_currentPreset = preset; } -void KisPresetLivePreviewView::updateStroke() +void KisPresetLivePreviewView::requestUpdateStroke() { - paintBackground(); + m_updateCompressor.start(); +} +void KisPresetLivePreviewView::updateStroke() +{ // do not paint a stroke if we are any of these engines (they have some issue currently) if (m_currentPreset->paintOp().id() == "roundmarker" || m_currentPreset->paintOp().id() == "experimentbrush" || m_currentPreset->paintOp().id() == "duplicate") { + paintBackground(); + slotPreviewGenerationCompleted(); return; } - setupAndPaintStroke(); - + if (!m_previewGenerationInProgress) { + paintBackground(); + setupAndPaintStroke(); + } else { + m_updateCompressor.start(); + } +} - // crop the layer so a brush stroke won't go outside of the area - m_layer->paintDevice()->crop(0,0, m_layer->image()->width(), m_layer->image()->height()); +void KisPresetLivePreviewView::slotPreviewGenerationCompleted() +{ + m_previewGenerationInProgress = false; QImage m_temp_image; - m_temp_image = m_layer->paintDevice()->convertToQImage(0); - + m_temp_image = m_layer->paintDevice()->convertToQImage(0, m_image->bounds()); // only add the object once...then just update the pixmap so we can move the preview around if (!m_sceneImageItem) { m_sceneImageItem = m_brushPreviewScene->addPixmap(QPixmap::fromImage(m_temp_image)); } else { m_sceneImageItem->setPixmap(QPixmap::fromImage(m_temp_image)); } - } - - void KisPresetLivePreviewView::paintBackground() { // clean up "no preview" text object if it exists. we will add it later if we need it if (m_noPreviewText) { this->scene()->removeItem(m_noPreviewText); m_noPreviewText = 0; } if (m_currentPreset->paintOp().id() == "colorsmudge" || m_currentPreset->paintOp().id() == "deformbrush" || m_currentPreset->paintOp().id() == "filter") { // easier to see deformations and smudging with alternating stripes in the background // paint the whole background with alternating stripes // filter engine may or may not show things depending on the filter...but it is better than nothing int grayStrips = 20; for (int i=0; i < grayStrips; i++ ) { float sectionPercent = 1.0 / (float)grayStrips; bool isAlternating = i % 2; KoColor fillColor(m_layer->paintDevice()->colorSpace()); if (isAlternating) { fillColor.fromQColor(QColor(80,80,80)); } else { fillColor.fromQColor(QColor(140,140,140)); } const QRect fillRect(m_layer->image()->width()*sectionPercent*i, 0, m_layer->image()->width()*(sectionPercent*i +sectionPercent), m_layer->image()->height()); m_layer->paintDevice()->fill(fillRect, fillColor); } m_paintColor = KoColor(Qt::white, m_colorSpace); } else if (m_currentPreset->paintOp().id() == "roundmarker" || m_currentPreset->paintOp().id() == "experimentbrush" || m_currentPreset->paintOp().id() == "duplicate" ) { // cases where we will not show a preview for now // roundbrush (quick) -- this isn't showing anything, disable showing preview // experimentbrush -- this creates artifacts that carry over to other previews and messes up their display // duplicate (clone) brush doesn't have a preview as it doesn't show anything) if(m_sceneImageItem) { this->scene()->removeItem(m_sceneImageItem); m_sceneImageItem = 0; } QFont font; font.setPixelSize(14); font.setBold(false); m_noPreviewText = this->scene()->addText(i18n("No Preview for this engine"),font); m_noPreviewText->setPos(50, this->height()/4); return; } else { // fill with gray first to clear out what existed from previous preview m_layer->paintDevice()->fill(m_image->bounds(), KoColor(palette().color(QPalette::Background) , m_colorSpace)); m_paintColor = KoColor(palette().color(QPalette::Text), m_colorSpace); } } +class NotificationStroke : public QObject, public KisSimpleStrokeStrategy +{ + Q_OBJECT +public: + NotificationStroke() + { + setClearsRedoOnStart(false); + this->enableJob(JOB_INIT, true, KisStrokeJobData::BARRIER); + this->enableJob(JOB_CANCEL, true, KisStrokeJobData::BARRIER); + } + + void initStrokeCallback() { + emit timeout(); + } + + void cancelStrokeCallback() { + emit cancelled(); + } + +Q_SIGNALS: + void timeout(); + void cancelled(); +}; + void KisPresetLivePreviewView::setupAndPaintStroke() { // limit the brush stroke size. larger brush strokes just don't look good and are CPU intensive // we are making a proxy preset and setting it to the painter...otherwise setting the brush size of the original preset // will fire off signals that make this run in an infinite loop qreal originalPresetSize = m_currentPreset->settings()->paintOpSize(); qreal previewSize = qBound(3.0, m_currentPreset->settings()->paintOpSize(), 25.0 ); // constrain live preview brush size //Except for the sketchbrush where it determine sthe history. if (m_currentPreset->paintOp().id() == "sketchbrush" || m_currentPreset->paintOp().id() == "spraybrush") { previewSize = qMax(3.0, m_currentPreset->settings()->paintOpSize()); } KisPaintOpPresetSP proxy_preset = m_currentPreset->clone(); KisPaintOpSettingsSP settings = proxy_preset->settings(); - proxy_preset->settings()->setPaintOpSize(previewSize); + settings->setPaintOpSize(previewSize); int maxTextureSize = 200; int textureOffsetX = settings->getInt("Texture/Pattern/MaximumOffsetX")*2; int textureOffsetY = settings->getInt("Texture/Pattern/MaximumOffsetY")*2; double textureScale = settings->getDouble("Texture/Pattern/Scale"); if ( textureOffsetX*textureScale> maxTextureSize || textureOffsetY*textureScale > maxTextureSize) { int maxSize = qMax(textureOffsetX, textureOffsetY); double result = qreal(maxTextureSize) / maxSize; settings->setProperty("Texture/Pattern/Scale", result); } - if (m_currentPreset->paintOp().id() == "spraybrush") { + if (proxy_preset->paintOp().id() == "spraybrush") { QDomElement element; QDomDocument d; QString brushDefinition = settings->getString("brush_definition"); if (!brushDefinition.isEmpty()) { d.setContent(brushDefinition, false); element = d.firstChildElement("Brush"); KisBrushSP brush = KisBrush::fromXML(element); qreal width = brush->image().width(); qreal scale = brush->scale(); qreal diameterToBrushRatio = 1.0; qreal diameter = settings->getInt("Spray/diameter"); //hack, 1000 being the maximum possible brushsize. if (brush->filename().endsWith(".svg")) { diameterToBrushRatio = diameter/(1000.0*scale); scale = 25.0 / 1000.0; } else { if (width * scale > 25.0) { diameterToBrushRatio = diameter / (width * scale); scale = 25.0 / width; } } settings->setProperty("Spray/diameter", int(25.0 * diameterToBrushRatio)); brush->setScale(scale); d.clear(); element = d.createElement("Brush"); brush->toXML(d, element); d.appendChild(element); settings->setProperty("brush_definition", d.toString()); } } // Preset preview cannot display gradient color source: there is // no resource manager for KisResourcesSnapshot, therefore gradient is nullptr. // BUG: 385521 (Selecting "Gradient" in brush editor crashes krita) - if (m_currentPreset->paintOp().id() == "paintbrush") { + if (proxy_preset->paintOp().id() == "paintbrush") { QString colorSourceType = settings->getString("ColorSource/Type", "plain"); if (colorSourceType == "gradient") { settings->setProperty("ColorSource/Type", "plain"); } } proxy_preset->setSettings(settings); KisResourcesSnapshotSP resources = new KisResourcesSnapshot(m_image, m_layer); resources->setBrush(proxy_preset); resources->setFGColorOverride(m_paintColor); KisFreehandStrokeInfo *strokeInfo = new KisFreehandStrokeInfo(); KisStrokeStrategy *stroke = new FreehandStrokeStrategy(resources, strokeInfo, kundo2_noi18n("temp_stroke")); KisStrokeId strokeId = m_image->startStroke(stroke); - - - //m_brushPreviewPainter->setPaintOpPreset(proxy_preset, m_layer, m_image); - - - // paint the stroke. The sketchbrush gets a different shape than the others to show how it works - if (m_currentPreset->paintOp().id() == "sketchbrush" - || m_currentPreset->paintOp().id() == "curvebrush" - || m_currentPreset->paintOp().id() == "particlebrush") { + if (proxy_preset->paintOp().id() == "sketchbrush" + || proxy_preset->paintOp().id() == "curvebrush" + || proxy_preset->paintOp().id() == "particlebrush") { qreal startX = m_canvasCenterPoint.x() - (this->width()*0.4); qreal endX = m_canvasCenterPoint.x() + (this->width()*0.4); qreal middle = m_canvasCenterPoint.y(); KisPaintInformation pointOne; pointOne.setPressure(0.0); pointOne.setPos(QPointF(startX, middle)); KisPaintInformation pointTwo; pointTwo.setPressure(0.0); pointTwo.setPos(QPointF(startX, middle)); int repeats = 8; for (int i = 0; i < repeats; i++) { pointOne.setPos(pointTwo.pos()); pointOne.setPressure(pointTwo.pressure()); pointTwo.setPressure((1.0/repeats)*(i+1)); qreal xPos = ((1.0/repeats) * (i+1) * (endX-startX) )+startX; pointTwo.setPos(QPointF(xPos, middle)); qreal offset = (this->height()/(repeats*1.5))*(i+1); qreal handleY = middle + offset; if (i%2 == 0) { handleY = middle - offset; } m_image->addJob(strokeId, new FreehandStrokeStrategy::Data(0, pointOne, QPointF(pointOne.pos().x(), handleY), QPointF(pointTwo.pos().x(), handleY), pointTwo)); m_image->addJob(strokeId, new KisAsyncronousStrokeUpdateHelper::UpdateData(true)); } } else { // paint an S curve m_curvePointPI1.setPos(QPointF(m_canvasCenterPoint.x() - (this->width()*0.45), m_canvasCenterPoint.y() + (this->height()*0.2))); m_curvePointPI1.setPressure(0.0); m_curvePointPI2.setPos(QPointF(m_canvasCenterPoint.x() + (this->width()*0.4), m_canvasCenterPoint.y() - (this->height()*0.2) )); m_curvePointPI2.setPressure(1.0); m_image->addJob(strokeId, new FreehandStrokeStrategy::Data(0, m_curvePointPI1, QPointF(m_canvasCenterPoint.x(), m_canvasCenterPoint.y()-this->height()), QPointF(m_canvasCenterPoint.x(), m_canvasCenterPoint.y()+this->height()), m_curvePointPI2)); m_image->addJob(strokeId, new KisAsyncronousStrokeUpdateHelper::UpdateData(true)); } m_image->endStroke(strokeId); - m_image->waitForDone(); + + m_previewGenerationInProgress = true; + + NotificationStroke *notificationStroke = new NotificationStroke(); + connect(notificationStroke, SIGNAL(timeout()), SLOT(slotPreviewGenerationCompleted())); + KisStrokeId notificationId = m_image->startStroke(notificationStroke); + m_image->endStroke(notificationId); + // TODO: if we don't have any regressions because of it until 4.2.8, then + // just remove this code. // even though the brush is cloned, the proxy_preset still has some connection to the original preset which will mess brush sizing // we need to return brush size to normal.The normal brush sends out a lot of extra signals, so keeping the proxy for now - proxy_preset->settings()->setPaintOpSize(originalPresetSize); + //proxy_preset->settings()->setPaintOpSize(originalPresetSize); } +#include "kis_preset_live_preview_view.moc" diff --git a/libs/ui/widgets/kis_preset_live_preview_view.h b/libs/ui/widgets/kis_preset_live_preview_view.h index cb697b1f48..abace25032 100644 --- a/libs/ui/widgets/kis_preset_live_preview_view.h +++ b/libs/ui/widgets/kis_preset_live_preview_view.h @@ -1,150 +1,157 @@ /* * Copyright (c) 2017 Scott Petrovic * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef _KIS_PRESET_LIVE_PREVIEW_ #define _KIS_PRESET_LIVE_PREVIEW_ #include #include #include #include #include #include "kis_paintop_preset.h" #include "KoColorSpaceRegistry.h" #include "kis_paint_layer.h" #include "kis_painter.h" #include "kis_distance_information.h" #include "kis_painting_information_builder.h" #include #include #include +#include "kis_signal_compressor.h" /** * Widget for displaying a live brush preview of your * selected brush. It listens for signalsetting changes * that the brush preset outputs and updates the preview * accordingly. This class can be added to a UI file * similar to how a QGraphicsView is added */ class KisPresetLivePreviewView : public QGraphicsView { Q_OBJECT public: KisPresetLivePreviewView(QWidget *parent); ~KisPresetLivePreviewView(); /** * @brief one time setup for initialization of many variables. * This live preview might be in a UI file, so make sure to * call this before starting to use it */ void setup(); /** * @brief set the current preset from resource manager for the live preview to use. * Good to call this every stroke update in case the preset has changed * @param preset the current preset from the resource manager */ void setCurrentPreset(KisPaintOpPresetSP preset); - void updateStroke(); + void requestUpdateStroke(); +private Q_SLOTS: + void updateStroke(); + void slotPreviewGenerationCompleted(); private: /// internally sets the image area for brush preview KisImageSP m_image; /// internally sets the layer area for brush preview KisLayerSP m_layer; /// internally sets the color space for brush preview const KoColorSpace *m_colorSpace; /// the color which is used for rendering the stroke KoColor m_paintColor; /// the scene that can add items like text and the brush stroke image QGraphicsScene *m_brushPreviewScene; /// holds the preview brush stroke data QGraphicsPixmapItem *m_sceneImageItem; /// holds the 'no preview available' text object QGraphicsTextItem *m_noPreviewText; /// holds the width and height of the image of the brush preview /// Probably can later add set functions to make this customizable /// It is hard-coded to 1200 x 400 for right now for image size QRect m_canvasSize; /// convenience variable used internally when positioning the objects /// and points in the scene QPointF m_canvasCenterPoint; /// internal variables for constructing the stroke start and end shape /// there are two points that construct the "S" curve with this KisDistanceInformation m_currentDistance; QPainterPath m_curvedLine; KisPaintInformation m_curvePointPI1; KisPaintInformation m_curvePointPI2; /// internally stores the current preset. /// See setCurrentPreset(KisPaintOpPresetSP preset) /// for setting this externally KisPaintOpPresetSP m_currentPreset; /// holds the current zoom(scale) level of scene float m_scaleFactor; /// internal reference for internal brush size /// used to check if our brush size has changed /// do zooming and other things internally if it has changed float m_currentBrushSize = 1.0; + bool m_previewGenerationInProgress = false; + KisSignalCompressor m_updateCompressor; + /// the range of brush sizes that will control zooming in/out const float m_minBrushVal = 10.0; const float m_maxBrushVal = 100.0; /// range of scale values. 1.0 == 100% const qreal m_minScale = 1.0; const qreal m_maxScale = 0.3; /// multiplier that is used for lengthening the brush stroke points const float m_minStrokeScale = 0.4; // for smaller brush stroke const float m_maxStrokeScale = 1.0; // for larger brush stroke /** * @brief works as both clearing the previous stroke, providing * striped backgrounds for smudging brushes, and text if there is no preview */ void paintBackground(); /** * @brief creates and performs the actual stroke that goes on top of the background * this is internally and should always be called after the paintBackground() */ void setupAndPaintStroke(); }; #endif diff --git a/plugins/filters/convertheightnormalmap/kis_convert_height_to_normal_map_filter.cpp b/plugins/filters/convertheightnormalmap/kis_convert_height_to_normal_map_filter.cpp index 0d513e3cb6..df19b0c773 100644 --- a/plugins/filters/convertheightnormalmap/kis_convert_height_to_normal_map_filter.cpp +++ b/plugins/filters/convertheightnormalmap/kis_convert_height_to_normal_map_filter.cpp @@ -1,174 +1,171 @@ /* * Copyright (c) 2017 Wolthera van Hövell tot Westerflier * * 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_convert_height_to_normal_map_filter.h" #include "kis_wdg_convert_height_to_normal_map.h" #include #include #include #include #include #include "kis_lod_transform.h" #include K_PLUGIN_FACTORY_WITH_JSON(KritaConvertHeightToNormalMapFilterFactory, "kritaconvertheighttonormalmap.json", registerPlugin();) KritaConvertHeightToNormalMapFilter::KritaConvertHeightToNormalMapFilter(QObject *parent, const QVariantList &) : QObject(parent) { KisFilterRegistry::instance()->add(KisFilterSP(new KisConvertHeightToNormalMapFilter())); } KritaConvertHeightToNormalMapFilter::~KritaConvertHeightToNormalMapFilter() { } KisConvertHeightToNormalMapFilter::KisConvertHeightToNormalMapFilter(): KisFilter(id(), FiltersCategoryEdgeDetectionId, i18n("&Height to Normal Map...")) { setSupportsPainting(true); setSupportsAdjustmentLayers(true); setSupportsLevelOfDetail(true); setColorSpaceIndependence(FULLY_INDEPENDENT); setShowConfigurationWidget(true); } void KisConvertHeightToNormalMapFilter::processImpl(KisPaintDeviceSP device, const QRect &rect, const KisFilterConfigurationSP config, KoUpdater *progressUpdater) const { Q_ASSERT(device); KisFilterConfigurationSP configuration = config ? config : new KisFilterConfiguration(id().id(), 1); KisLodTransformScalar t(device); QVariant value; float horizontalRadius = 1.0; if (configuration->getProperty("horizRadius", value)) { horizontalRadius = t.scale(value.toFloat()); } float verticalRadius = 1.0; if (configuration->getProperty("vertRadius", value)) { verticalRadius = t.scale(value.toFloat()); } QBitArray channelFlags; if (configuration) { channelFlags = configuration->channelFlags(); } - if (channelFlags.isEmpty() || !configuration) { - channelFlags = device->colorSpace()->channelFlags(); - } KisEdgeDetectionKernel::FilterType type = KisEdgeDetectionKernel::SobelVector; if (configuration->getString("type") == "prewitt") { type = KisEdgeDetectionKernel::Prewit; } else if (configuration->getString("type") == "simple") { type = KisEdgeDetectionKernel::Simple; } int channelToConvert = configuration->getInt("channelToConvert", 0); QVector channelOrder(3); QVector channelFlip(3); channelFlip.fill(false); int i = config->getInt("redSwizzle", 0); if (i%2==1 || i==2) { channelFlip[0] = true; } if (i==3) { channelFlip[0] = false; } channelOrder[device->colorSpace()->channels().at(0)->displayPosition()] = qMax(i/2,0); i = config->getInt("greenSwizzle", 2); if (i%2==1 || i==2) { channelFlip[1] = true; } if (i==3) { channelFlip[1] = false; } channelOrder[device->colorSpace()->channels().at(1)->displayPosition()] = qMax(i/2,0); i = config->getInt("blueSwizzle", 4); if (i%2==1 || i==2) { channelFlip[2] = true; } if (i==3) { channelFlip[2] = false; } channelOrder[device->colorSpace()->channels().at(2)->displayPosition()] = qMax(i/2,0); KisEdgeDetectionKernel::convertToNormalMap(device, rect, horizontalRadius, verticalRadius, type, channelToConvert, channelOrder, channelFlip, channelFlags, progressUpdater); } KisFilterConfigurationSP KisConvertHeightToNormalMapFilter::factoryConfiguration() const { KisFilterConfigurationSP config = new KisFilterConfiguration(id().id(), 1); config->setProperty("horizRadius", 1); config->setProperty("vertRadius", 1); config->setProperty("type", "sobol"); config->setProperty("channelToConvert", 0); config->setProperty("lockAspect", true); config->setProperty("redSwizzle", KisWdgConvertHeightToNormalMap::xPlus); config->setProperty("greenSwizzle", KisWdgConvertHeightToNormalMap::yPlus); config->setProperty("blueSwizzle", KisWdgConvertHeightToNormalMap::zPlus); return config; } KisConfigWidget *KisConvertHeightToNormalMapFilter::createConfigurationWidget(QWidget *parent, const KisPaintDeviceSP dev, bool) const { return new KisWdgConvertHeightToNormalMap(parent, dev->colorSpace()); } QRect KisConvertHeightToNormalMapFilter::neededRect(const QRect &rect, const KisFilterConfigurationSP _config, int lod) const { KisLodTransformScalar t(lod); QVariant value; /** * NOTE: integer division by two is done on purpose, * because the kernel size is always odd */ const int halfWidth = _config->getProperty("horizRadius", value) ? KisEdgeDetectionKernel::kernelSizeFromRadius(t.scale(value.toFloat())) / 2 : 5; const int halfHeight = _config->getProperty("vertRadius", value) ? KisEdgeDetectionKernel::kernelSizeFromRadius(t.scale(value.toFloat())) / 2 : 5; return rect.adjusted(-halfWidth * 2, -halfHeight * 2, halfWidth * 2, halfHeight * 2); } QRect KisConvertHeightToNormalMapFilter::changedRect(const QRect &rect, const KisFilterConfigurationSP _config, int lod) const { KisLodTransformScalar t(lod); QVariant value; const int halfWidth = _config->getProperty("horizRadius", value) ? KisEdgeDetectionKernel::kernelSizeFromRadius(t.scale(value.toFloat())) / 2 : 5; const int halfHeight = _config->getProperty("vertRadius", value) ? KisEdgeDetectionKernel::kernelSizeFromRadius(t.scale(value.toFloat())) / 2 : 5; return rect.adjusted( -halfWidth, -halfHeight, halfWidth, halfHeight); } #include "kis_convert_height_to_normal_map_filter.moc" diff --git a/plugins/filters/edgedetection/kis_edge_detection_filter.cpp b/plugins/filters/edgedetection/kis_edge_detection_filter.cpp index 1bb977c52a..3826164ae0 100644 --- a/plugins/filters/edgedetection/kis_edge_detection_filter.cpp +++ b/plugins/filters/edgedetection/kis_edge_detection_filter.cpp @@ -1,159 +1,156 @@ /* * Copyright (c) 2017 Wolthera van Hövell tot Westerflier * * 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_edge_detection_filter.h" #include "kis_wdg_edge_detection.h" #include #include #include #include #include #include #include #include #include #include #include "kis_lod_transform.h" #include #include #include K_PLUGIN_FACTORY_WITH_JSON(KritaEdgeDetectionFilterFactory, "kritaedgedetection.json", registerPlugin();) KritaEdgeDetectionFilter::KritaEdgeDetectionFilter(QObject *parent, const QVariantList &) : QObject(parent) { KisFilterRegistry::instance()->add(KisFilterSP(new KisEdgeDetectionFilter())); } KritaEdgeDetectionFilter::~KritaEdgeDetectionFilter() { } KisEdgeDetectionFilter::KisEdgeDetectionFilter(): KisFilter(id(), FiltersCategoryEdgeDetectionId, i18n("&Edge Detection...")) { setSupportsPainting(true); setSupportsAdjustmentLayers(true); setSupportsLevelOfDetail(true); setColorSpaceIndependence(FULLY_INDEPENDENT); setShowConfigurationWidget(true); } void KisEdgeDetectionFilter::processImpl(KisPaintDeviceSP device, const QRect &rect, const KisFilterConfigurationSP config, KoUpdater *progressUpdater) const { Q_ASSERT(device != 0); KisFilterConfigurationSP configuration = config ? config : new KisFilterConfiguration(id().id(), 1); KisLodTransformScalar t(device); QVariant value; configuration->getProperty("horizRadius", value); float horizontalRadius = t.scale(value.toFloat()); configuration->getProperty("vertRadius", value); float verticalRadius = t.scale(value.toFloat()); QBitArray channelFlags; if (configuration) { channelFlags = configuration->channelFlags(); } - if (channelFlags.isEmpty() || !configuration) { - channelFlags = device->colorSpace()->channelFlags(); - } KisEdgeDetectionKernel::FilterType type = KisEdgeDetectionKernel::SobelVector; if (config->getString("type") == "prewitt") { type = KisEdgeDetectionKernel::Prewit; } else if (config->getString("type") == "simple") { type = KisEdgeDetectionKernel::Simple; } KisEdgeDetectionKernel::FilterOutput output = KisEdgeDetectionKernel::pythagorean; if (config->getString("output") == "xGrowth") { output = KisEdgeDetectionKernel::xGrowth; } else if (config->getString("output") == "xFall") { output = KisEdgeDetectionKernel::xFall; } else if (config->getString("output") == "yGrowth") { output = KisEdgeDetectionKernel::yGrowth; } else if (config->getString("output") == "yFall") { output = KisEdgeDetectionKernel::yFall; } else if (config->getString("output") == "radian") { output = KisEdgeDetectionKernel::radian; } KisEdgeDetectionKernel::applyEdgeDetection(device, rect, horizontalRadius, verticalRadius, type, channelFlags, progressUpdater, output, config->getBool("transparency", false)); } KisFilterConfigurationSP KisEdgeDetectionFilter::factoryConfiguration() const { KisFilterConfigurationSP config = new KisFilterConfiguration(id().id(), 1); config->setProperty("horizRadius", 1); config->setProperty("vertRadius", 1); config->setProperty("type", "prewitt"); config->setProperty("output", "pythagorean"); config->setProperty("lockAspect", true); config->setProperty("transparency", false); return config; } KisConfigWidget *KisEdgeDetectionFilter::createConfigurationWidget(QWidget *parent, const KisPaintDeviceSP dev, bool) const { Q_UNUSED(dev); return new KisWdgEdgeDetection(parent); } QRect KisEdgeDetectionFilter::neededRect(const QRect &rect, const KisFilterConfigurationSP _config, int lod) const { KisLodTransformScalar t(lod); QVariant value; /** * NOTE: integer division by two is done on purpose, * because the kernel size is always odd */ const int halfWidth = _config->getProperty("horizRadius", value) ? KisEdgeDetectionKernel::kernelSizeFromRadius(t.scale(value.toFloat())) / 2 : 5; const int halfHeight = _config->getProperty("vertRadius", value) ? KisEdgeDetectionKernel::kernelSizeFromRadius(t.scale(value.toFloat())) / 2 : 5; return rect.adjusted(-halfWidth * 2, -halfHeight * 2, halfWidth * 2, halfHeight * 2); } QRect KisEdgeDetectionFilter::changedRect(const QRect &rect, const KisFilterConfigurationSP _config, int lod) const { KisLodTransformScalar t(lod); QVariant value; const int halfWidth = _config->getProperty("horizRadius", value) ? KisEdgeDetectionKernel::kernelSizeFromRadius(t.scale(value.toFloat())) / 2 : 5; const int halfHeight = _config->getProperty("vertRadius", value) ? KisEdgeDetectionKernel::kernelSizeFromRadius(t.scale(value.toFloat())) / 2 : 5; return rect.adjusted( -halfWidth, -halfHeight, halfWidth, halfHeight); } #include "kis_edge_detection_filter.moc" diff --git a/plugins/impex/tiff/kis_tiff_converter.cc b/plugins/impex/tiff/kis_tiff_converter.cc index b68057ec87..6416806553 100644 --- a/plugins/impex/tiff/kis_tiff_converter.cc +++ b/plugins/impex/tiff/kis_tiff_converter.cc @@ -1,812 +1,812 @@ /* * Copyright (c) 2005-2006 Cyrille Berger * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "kis_tiff_converter.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kis_tiff_reader.h" #include "kis_tiff_ycbcr_reader.h" #include "kis_buffer_stream.h" #include "kis_tiff_writer_visitor.h" #include #if TIFFLIB_VERSION < 20111221 typedef size_t tmsize_t; #endif namespace { QPair getColorSpaceForColorType(uint16 sampletype, uint16 color_type, uint16 color_nb_bits, TIFF *image, uint16 &nbchannels, uint16 &extrasamplescount, uint8 &destDepth) { const int bits32 = 32; const int bits16 = 16; const int bits8 = 8; if (color_type == PHOTOMETRIC_MINISWHITE || color_type == PHOTOMETRIC_MINISBLACK) { if (nbchannels == 0) nbchannels = 1; extrasamplescount = nbchannels - 1; // FIX the extrasamples count in case of if (sampletype == SAMPLEFORMAT_IEEEFP) { if (color_nb_bits == 16) { destDepth = 16; return QPair(GrayAColorModelID.id(), Float16BitsColorDepthID.id()); } else if (color_nb_bits == 32) { destDepth = 32; return QPair(GrayAColorModelID.id(), Float32BitsColorDepthID.id()); } } if (color_nb_bits <= 8) { destDepth = 8; return QPair(GrayAColorModelID.id(), Integer8BitsColorDepthID.id()); } else /* if (color_nb_bits == bits16) */ { destDepth = 16; return QPair(GrayAColorModelID.id(), Integer16BitsColorDepthID.id()); } } else if (color_type == PHOTOMETRIC_RGB /*|| color_type == */) { if (nbchannels == 0) nbchannels = 3; extrasamplescount = nbchannels - 3; // FIX the extrasamples count in case of if (sampletype == SAMPLEFORMAT_IEEEFP) { if (color_nb_bits == 16) { destDepth = 16; return QPair(RGBAColorModelID.id(), Float16BitsColorDepthID.id()); } else if (color_nb_bits == 32) { destDepth = 32; return QPair(RGBAColorModelID.id(), Float32BitsColorDepthID.id()); } return QPair(); // sanity check; no support for float of higher or lower bit depth } else { if (color_nb_bits <= 8) { destDepth = 8; return QPair(RGBAColorModelID.id(), Integer8BitsColorDepthID.id()); } else /* if (color_nb_bits == bits16) */ { destDepth = 16; return QPair(RGBAColorModelID.id(), Integer16BitsColorDepthID.id()); } } } else if (color_type == PHOTOMETRIC_YCBCR) { if (nbchannels == 0) nbchannels = 3; extrasamplescount = nbchannels - 3; // FIX the extrasamples count in case of if (sampletype == SAMPLEFORMAT_IEEEFP) { return QPair(); // sanity check; no support for float } if (color_nb_bits <= 8) { destDepth = 8; return QPair(YCbCrAColorModelID.id(), Integer8BitsColorDepthID.id()); } else if (color_nb_bits == bits16) { destDepth = 16; return QPair(YCbCrAColorModelID.id(), Integer16BitsColorDepthID.id()); } else { return QPair(); // sanity check; no support integers of higher bit depth } } else if (color_type == PHOTOMETRIC_SEPARATED) { if (nbchannels == 0) nbchannels = 4; // SEPARATED is in general CMYK but not always, so we check uint16 inkset; if ((TIFFGetField(image, TIFFTAG_INKSET, &inkset) == 0)) { dbgFile << "Image does not define the inkset."; inkset = 2; } if (inkset != INKSET_CMYK) { dbgFile << "Unsupported inkset (right now, only CMYK is supported)"; char** ink_names; uint16 numberofinks; if (TIFFGetField(image, TIFFTAG_INKNAMES, &ink_names) == 1 && TIFFGetField(image, TIFFTAG_NUMBEROFINKS, &numberofinks) == 1) { dbgFile << "Inks are :"; for (uint i = 0; i < numberofinks; i++) { dbgFile << ink_names[i]; } } else { dbgFile << "inknames are not defined !"; // To be able to read stupid adobe files, if there are no information about inks and four channels, then it's a CMYK file : if (nbchannels - extrasamplescount != 4) { return QPair(); } // else - assume it's CMYK and proceed } } if (color_nb_bits <= 8) { destDepth = 8; return QPair(CMYKAColorModelID.id(), Integer8BitsColorDepthID.id()); } else if (color_nb_bits == 16) { destDepth = 16; return QPair(CMYKAColorModelID.id(), Integer16BitsColorDepthID.id()); } else if (sampletype == SAMPLEFORMAT_IEEEFP) { destDepth = bits32; return QPair(CMYKAColorModelID.id(), Float32BitsColorDepthID.id()); } else { return QPair(); // no support for other bit depths } } else if (color_type == PHOTOMETRIC_CIELAB || color_type == PHOTOMETRIC_ICCLAB) { if (nbchannels == 0) nbchannels = 3; extrasamplescount = nbchannels - 3; // FIX the extrasamples count switch(color_nb_bits) { case bits32: { destDepth = bits32; return QPair(LABAColorModelID.id(), Float32BitsColorDepthID.id()); } case bits16: { destDepth = bits16; if (sampletype == SAMPLEFORMAT_IEEEFP) { return QPair(LABAColorModelID.id(), Float16BitsColorDepthID.id()); } else { return QPair(LABAColorModelID.id(), Integer16BitsColorDepthID.id()); } } case bits8: { destDepth = bits8; return QPair(LABAColorModelID.id(), Integer8BitsColorDepthID.id()); } default: { return QPair(); } } } else if (color_type == PHOTOMETRIC_PALETTE) { destDepth = 16; if (nbchannels == 0) nbchannels = 2; extrasamplescount = nbchannels - 2; // FIX the extrasamples count // <-- we will convert the index image to RGBA16 as the palette is always on 16bits colors return QPair(RGBAColorModelID.id(), Integer16BitsColorDepthID.id()); } return QPair(); } } KisPropertiesConfigurationSP KisTIFFOptions::toProperties() const { QHash compToIndex; compToIndex[COMPRESSION_NONE] = 0; compToIndex[COMPRESSION_JPEG] = 1; compToIndex[COMPRESSION_DEFLATE] = 2; compToIndex[COMPRESSION_LZW] = 3; compToIndex[COMPRESSION_PIXARLOG] = 8; KisPropertiesConfigurationSP cfg = new KisPropertiesConfiguration(); cfg->setProperty("compressiontype", compToIndex.value(compressionType, 0)); cfg->setProperty("predictor", predictor - 1); cfg->setProperty("alpha", alpha); cfg->setProperty("flatten", flatten); cfg->setProperty("quality", jpegQuality); cfg->setProperty("deflate", deflateCompress); cfg->setProperty("pixarlog", pixarLogCompress); cfg->setProperty("saveProfile", saveProfile); return cfg; } void KisTIFFOptions::fromProperties(KisPropertiesConfigurationSP cfg) { QHash indexToComp; indexToComp[0] = COMPRESSION_NONE; indexToComp[1] = COMPRESSION_JPEG; indexToComp[2] = COMPRESSION_DEFLATE; indexToComp[3] = COMPRESSION_LZW; indexToComp[4] = COMPRESSION_PIXARLOG; // old value that might be still stored in a config (remove after Krita 5.0 :) ) indexToComp[8] = COMPRESSION_PIXARLOG; compressionType = indexToComp.value( cfg->getInt("compressiontype", 0), COMPRESSION_NONE); predictor = cfg->getInt("predictor", 0) + 1; alpha = cfg->getBool("alpha", true); flatten = cfg->getBool("flatten", true); jpegQuality = cfg->getInt("quality", 80); deflateCompress = cfg->getInt("deflate", 6); pixarLogCompress = cfg->getInt("pixarlog", 6); saveProfile = cfg->getBool("saveProfile", true); } KisTIFFConverter::KisTIFFConverter(KisDocument *doc) { m_doc = doc; m_stop = false; TIFFSetWarningHandler(0); TIFFSetErrorHandler(0); } KisTIFFConverter::~KisTIFFConverter() { } KisImportExportErrorCode KisTIFFConverter::decode(const QString &filename) { dbgFile << "Start decoding TIFF File"; // Opent the TIFF file TIFF *image = 0; if (!KisImportExportAdditionalChecks::doesFileExist(filename)) { return ImportExportCodes::FileNotExist; } if (!KisImportExportAdditionalChecks::isFileReadable(filename)) { return ImportExportCodes::NoAccessToRead; } if ((image = TIFFOpen(QFile::encodeName(filename), "r")) == 0) { dbgFile << "Could not open the file, either it does not exist, either it is not a TIFF :" << filename; return (ImportExportCodes::FileFormatIncorrect); } do { dbgFile << "Read new sub-image"; KisImportExportErrorCode result = readTIFFDirectory(image); if (!result.isOk()) { return result; } } while (TIFFReadDirectory(image)); // Freeing memory TIFFClose(image); return ImportExportCodes::OK; } KisImportExportErrorCode KisTIFFConverter::readTIFFDirectory(TIFF* image) { // Read information about the tiff uint32 width, height; if (TIFFGetField(image, TIFFTAG_IMAGEWIDTH, &width) == 0) { dbgFile << "Image does not define its width"; TIFFClose(image); return ImportExportCodes::FileFormatIncorrect; } if (TIFFGetField(image, TIFFTAG_IMAGELENGTH, &height) == 0) { dbgFile << "Image does not define its height"; TIFFClose(image); return ImportExportCodes::FileFormatIncorrect; } float xres; if (TIFFGetField(image, TIFFTAG_XRESOLUTION, &xres) == 0) { dbgFile << "Image does not define x resolution"; // but we don't stop xres = 100; } float yres; if (TIFFGetField(image, TIFFTAG_YRESOLUTION, &yres) == 0) { dbgFile << "Image does not define y resolution"; // but we don't stop yres = 100; } uint16 depth; if ((TIFFGetField(image, TIFFTAG_BITSPERSAMPLE, &depth) == 0)) { dbgFile << "Image does not define its depth"; depth = 1; } uint16 sampletype; if ((TIFFGetField(image, TIFFTAG_SAMPLEFORMAT, &sampletype) == 0)) { dbgFile << "Image does not define its sample type"; sampletype = SAMPLEFORMAT_UINT; } // Determine the number of channels (useful to know if a file has an alpha or not uint16 nbchannels; if (TIFFGetField(image, TIFFTAG_SAMPLESPERPIXEL, &nbchannels) == 0) { dbgFile << "Image has an undefined number of samples per pixel"; nbchannels = 0; } // Get the number of extrasamples and information about them uint16 *sampleinfo = 0, extrasamplescount; if (TIFFGetField(image, TIFFTAG_EXTRASAMPLES, &extrasamplescount, &sampleinfo) == 0) { extrasamplescount = 0; } // Determine the colorspace uint16 color_type; if (TIFFGetField(image, TIFFTAG_PHOTOMETRIC, &color_type) == 0) { dbgFile << "Image has an undefined photometric interpretation"; color_type = PHOTOMETRIC_MINISWHITE; } uint8 dstDepth = 0; QPair colorSpaceIdTag = getColorSpaceForColorType(sampletype, color_type, depth, image, nbchannels, extrasamplescount, dstDepth); if (colorSpaceIdTag.first.isEmpty()) { dbgFile << "Image has an unsupported colorspace :" << color_type << " for this depth :" << depth; TIFFClose(image); return ImportExportCodes::FormatColorSpaceUnsupported; } dbgFile << "Colorspace is :" << colorSpaceIdTag.first << colorSpaceIdTag.second << " with a depth of" << depth << " and with a nb of channels of" << nbchannels; // Read image profile dbgFile << "Reading profile"; const KoColorProfile* profile = 0; quint32 EmbedLen; quint8* EmbedBuffer; if (TIFFGetField(image, TIFFTAG_ICCPROFILE, &EmbedLen, &EmbedBuffer) == 1) { dbgFile << "Profile found"; QByteArray rawdata; rawdata.resize(EmbedLen); memcpy(rawdata.data(), EmbedBuffer, EmbedLen); profile = KoColorSpaceRegistry::instance()->createColorProfile(colorSpaceIdTag.first, colorSpaceIdTag.second, rawdata); } const QString colorSpaceId = KoColorSpaceRegistry::instance()->colorSpaceId(colorSpaceIdTag.first, colorSpaceIdTag.second); // Check that the profile is used by the color space if (profile && !KoColorSpaceRegistry::instance()->profileIsCompatible(profile, colorSpaceId)) { dbgFile << "The profile " << profile->name() << " is not compatible with the color space model " << colorSpaceIdTag.first << " " << colorSpaceIdTag.second; profile = 0; } // Do not use the linear gamma profile for 16 bits/channel by default, tiff files are usually created with // gamma correction. XXX: Should we ask the user? if (!profile) { dbgFile << "No profile found; trying to assign a default one."; if (colorSpaceIdTag.first == RGBAColorModelID.id()) { profile = KoColorSpaceRegistry::instance()->profileByName("sRGB-elle-V2-srgbtrc.icc"); } else if (colorSpaceIdTag.first == GrayAColorModelID.id()) { profile = KoColorSpaceRegistry::instance()->profileByName("Gray-D50-elle-V2-srgbtrc.icc"); } else if (colorSpaceIdTag.first == CMYKAColorModelID.id()) { profile = KoColorSpaceRegistry::instance()->profileByName("Chemical proof"); } else if (colorSpaceIdTag.first == LABAColorModelID.id()) { profile = KoColorSpaceRegistry::instance()->profileByName("Lab identity build-in"); } if (!profile) { dbgFile << "No suitable default profile found."; } } // Retrieve a pointer to the colorspace const KoColorSpace* cs = 0; if (profile && profile->isSuitableForOutput()) { dbgFile << "image has embedded profile:" << profile -> name() << ""; cs = KoColorSpaceRegistry::instance()->colorSpace(colorSpaceIdTag.first, colorSpaceIdTag.second, profile); } else { cs = KoColorSpaceRegistry::instance()->colorSpace(colorSpaceIdTag.first, colorSpaceIdTag.second, 0); } if (cs == 0) { dbgFile << "Colorspace" << colorSpaceIdTag.first << colorSpaceIdTag.second << " is not available, please check your installation."; TIFFClose(image); return ImportExportCodes::FormatColorSpaceUnsupported; } // Create the cmsTransform if needed KoColorTransformation* transform = 0; if (profile && !profile->isSuitableForOutput()) { dbgFile << "The profile can't be used in krita, need conversion"; transform = KoColorSpaceRegistry::instance()->colorSpace(colorSpaceIdTag.first, colorSpaceIdTag.second, profile)->createColorConverter(cs, KoColorConversionTransformation::internalRenderingIntent(), KoColorConversionTransformation::internalConversionFlags()); } // Check if there is an alpha channel int8 alphapos = -1; // <- no alpha // Check which extra is alpha if any dbgFile << "There are" << nbchannels << " channels and" << extrasamplescount << " extra channels"; if (sampleinfo) { // index images don't have any sampleinfo, and therefore sampleinfo == 0 for (int i = 0; i < extrasamplescount; i ++) { dbgFile << "sample" << i << "extra sample count" << extrasamplescount << "color channel count" << (cs->colorChannelCount()) << "Number of channels" << nbchannels << "sample info" << sampleinfo[i]; if (sampleinfo[i] == EXTRASAMPLE_UNSPECIFIED) { qWarning() << "Extra sample type not defined for this file, assuming unassociated alpha."; alphapos = i; } if (sampleinfo[i] == EXTRASAMPLE_ASSOCALPHA) { // XXX: dangelo: the color values are already multiplied with // the alpha value. This needs to be reversed later (postprocessor?) qWarning() << "Associated alpha in this file: krita does not handle plremultiplied alpha."; alphapos = i; } if (sampleinfo[i] == EXTRASAMPLE_UNASSALPHA) { // color values are not premultiplied with alpha, and can be used as they are. alphapos = i; } } } dbgFile << "Alpha pos:" << alphapos; // Read META Information KoDocumentInfo * info = m_doc->documentInfo(); char* text; if (TIFFGetField(image, TIFFTAG_ARTIST, &text) == 1) { info->setAuthorInfo("creator", text); } if (TIFFGetField(image, TIFFTAG_DOCUMENTNAME, &text) == 1) { info->setAboutInfo("title", text); } if (TIFFGetField(image, TIFFTAG_IMAGEDESCRIPTION, &text) == 1) { info->setAboutInfo("description", text); } // Get the planar configuration uint16 planarconfig; if (TIFFGetField(image, TIFFTAG_PLANARCONFIG, &planarconfig) == 0) { dbgFile << "Plannar configuration is not define"; TIFFClose(image); return ImportExportCodes::FileFormatIncorrect; } // Creating the KisImageSP if (! m_image) { m_image = new KisImage(m_doc->createUndoStore(), width, height, cs, "built image"); m_image->setResolution( POINT_TO_INCH(xres), POINT_TO_INCH(yres )); // It is the "invert" macro because we convert from pointer-per-inchs to points Q_CHECK_PTR(m_image); } else { if (m_image->width() < (qint32)width || m_image->height() < (qint32)height) { quint32 newwidth = (m_image->width() < (qint32)width) ? width : m_image->width(); quint32 newheight = (m_image->height() < (qint32)height) ? height : m_image->height(); m_image->resizeImage(QRect(0,0,newwidth, newheight)); } } KisPaintLayer* layer = new KisPaintLayer(m_image.data(), m_image -> nextLayerName(), quint8_MAX); tdata_t buf = 0; tdata_t* ps_buf = 0; // used only for planar configuration separated KisBufferStreamBase* tiffstream; KisTIFFReaderBase* tiffReader = 0; quint8 poses[5]; KisTIFFPostProcessor* postprocessor = 0; // Configure poses uint8 nbcolorsamples = nbchannels - extrasamplescount; switch (color_type) { case PHOTOMETRIC_MINISWHITE: { poses[0] = 0; poses[1] = 1; postprocessor = new KisTIFFPostProcessorInvert(nbcolorsamples); } break; case PHOTOMETRIC_MINISBLACK: { poses[0] = 0; poses[1] = 1; postprocessor = new KisTIFFPostProcessor(nbcolorsamples); } break; case PHOTOMETRIC_CIELAB: { poses[0] = 0; poses[1] = 1; poses[2] = 2; poses[3] = 3; postprocessor = new KisTIFFPostProcessorCIELABtoICCLAB(nbcolorsamples); } break; case PHOTOMETRIC_ICCLAB: { poses[0] = 0; poses[1] = 1; poses[2] = 2; poses[3] = 3; postprocessor = new KisTIFFPostProcessor(nbcolorsamples); } break; case PHOTOMETRIC_RGB: { if (sampletype == SAMPLEFORMAT_IEEEFP) { poses[2] = 2; poses[1] = 1; poses[0] = 0; poses[3] = 3; } else { poses[0] = 2; poses[1] = 1; poses[2] = 0; poses[3] = 3; } postprocessor = new KisTIFFPostProcessor(nbcolorsamples); } break; case PHOTOMETRIC_SEPARATED: { poses[0] = 0; poses[1] = 1; poses[2] = 2; poses[3] = 3; poses[4] = 4; postprocessor = new KisTIFFPostProcessor(nbcolorsamples); } break; default: break; } // Initisalize tiffReader uint16 * lineSizeCoeffs = new uint16[nbchannels]; uint16 vsubsampling = 1; uint16 hsubsampling = 1; for (uint i = 0; i < nbchannels; i++) { lineSizeCoeffs[i] = 1; } if (color_type == PHOTOMETRIC_PALETTE) { uint16 *red; // No need to free them they are free by libtiff uint16 *green; uint16 *blue; if ((TIFFGetField(image, TIFFTAG_COLORMAP, &red, &green, &blue)) == 0) { dbgFile << "Indexed image does not define a palette"; TIFFClose(image); delete [] lineSizeCoeffs; return ImportExportCodes::FileFormatIncorrect; } tiffReader = new KisTIFFReaderFromPalette(layer->paintDevice(), red, green, blue, poses, alphapos, depth, sampletype, nbcolorsamples, extrasamplescount, transform, postprocessor); } else if (color_type == PHOTOMETRIC_YCBCR) { TIFFGetFieldDefaulted(image, TIFFTAG_YCBCRSUBSAMPLING, &hsubsampling, &vsubsampling); lineSizeCoeffs[1] = hsubsampling; lineSizeCoeffs[2] = hsubsampling; uint16 position; TIFFGetFieldDefaulted(image, TIFFTAG_YCBCRPOSITIONING, &position); if (dstDepth == 8) { tiffReader = new KisTIFFYCbCrReaderTarget8Bit(layer->paintDevice(), layer->image()->width(), layer->image()->height(), poses, alphapos, depth, sampletype, nbcolorsamples, extrasamplescount, transform, postprocessor, hsubsampling, vsubsampling); } else if (dstDepth == 16) { tiffReader = new KisTIFFYCbCrReaderTarget16Bit(layer->paintDevice(), layer->image()->width(), layer->image()->height(), poses, alphapos, depth, sampletype, nbcolorsamples, extrasamplescount, transform, postprocessor, hsubsampling, vsubsampling); } } else if (dstDepth == 8) { tiffReader = new KisTIFFReaderTarget8bit(layer->paintDevice(), poses, alphapos, depth, sampletype, nbcolorsamples, extrasamplescount, transform, postprocessor); } else if (dstDepth == 16) { uint16 alphaValue; if (sampletype == SAMPLEFORMAT_IEEEFP) { alphaValue = 15360; // representation of 1.0 in half } else { alphaValue = quint16_MAX; } tiffReader = new KisTIFFReaderTarget16bit(layer->paintDevice(), poses, alphapos, depth, sampletype, nbcolorsamples, extrasamplescount, transform, postprocessor, alphaValue); } else if (dstDepth == 32) { union { float f; uint32 i; } alphaValue; if (sampletype == SAMPLEFORMAT_IEEEFP) { alphaValue.f = 1.0f; } else { alphaValue.i = quint32_MAX; } tiffReader = new KisTIFFReaderTarget32bit(layer->paintDevice(), poses, alphapos, depth, sampletype, nbcolorsamples, extrasamplescount, transform, postprocessor, alphaValue.i); } if (!tiffReader) { delete postprocessor; delete[] lineSizeCoeffs; TIFFClose(image); dbgFile << "Image has an invalid/unsupported color type: " << color_type; return ImportExportCodes::FileFormatIncorrect; } if (TIFFIsTiled(image)) { dbgFile << "tiled image"; uint32 tileWidth, tileHeight; uint32 x, y; TIFFGetField(image, TIFFTAG_TILEWIDTH, &tileWidth); TIFFGetField(image, TIFFTAG_TILELENGTH, &tileHeight); uint32 linewidth = (tileWidth * depth * nbchannels) / 8; if (planarconfig == PLANARCONFIG_CONTIG) { buf = _TIFFmalloc(TIFFTileSize(image)); if (depth < 16) { tiffstream = new KisBufferStreamContigBelow16((uint8*)buf, depth, linewidth); } else if (depth < 32) { tiffstream = new KisBufferStreamContigBelow32((uint8*)buf, depth, linewidth); } else { tiffstream = new KisBufferStreamContigAbove32((uint8*)buf, depth, linewidth); } } else { ps_buf = new tdata_t[nbchannels]; uint32 * lineSizes = new uint32[nbchannels]; - tmsize_t baseSize = TIFFTileSize(image) / nbchannels; + tmsize_t baseSize = TIFFTileSize(image); for (uint i = 0; i < nbchannels; i++) { ps_buf[i] = _TIFFmalloc(baseSize); - lineSizes[i] = tileWidth; // baseSize / lineSizeCoeffs[i]; + lineSizes[i] = tileWidth;; } tiffstream = new KisBufferStreamSeperate((uint8**) ps_buf, nbchannels, depth, lineSizes); delete [] lineSizes; } dbgFile << linewidth << "" << nbchannels << "" << layer->paintDevice()->colorSpace()->colorChannelCount(); for (y = 0; y < height; y += tileHeight) { for (x = 0; x < width; x += tileWidth) { dbgFile << "Reading tile x =" << x << " y =" << y; if (planarconfig == PLANARCONFIG_CONTIG) { TIFFReadTile(image, buf, x, y, 0, (tsample_t) - 1); } else { for (uint i = 0; i < nbchannels; i++) { TIFFReadTile(image, ps_buf[i], x, y, 0, i); } } uint32 realTileWidth = (x + tileWidth) < width ? tileWidth : width - x; for (uint yintile = 0; y + yintile < height && yintile < tileHeight / vsubsampling;) { tiffReader->copyDataToChannels(x, y + yintile , realTileWidth, tiffstream); yintile += 1; tiffstream->moveToLine(yintile); } tiffstream->restart(); } } } else { dbgFile << "striped image"; tsize_t stripsize = TIFFStripSize(image); uint32 rowsPerStrip; TIFFGetFieldDefaulted(image, TIFFTAG_ROWSPERSTRIP, &rowsPerStrip); dbgFile << rowsPerStrip << "" << height; rowsPerStrip = qMin(rowsPerStrip, height); // when TIFFNumberOfStrips(image) == 1 it might happen that rowsPerStrip is incorrectly set if (planarconfig == PLANARCONFIG_CONTIG) { buf = _TIFFmalloc(stripsize); if (depth < 16) { tiffstream = new KisBufferStreamContigBelow16((uint8*)buf, depth, stripsize / rowsPerStrip); } else if (depth < 32) { tiffstream = new KisBufferStreamContigBelow32((uint8*)buf, depth, stripsize / rowsPerStrip); } else { tiffstream = new KisBufferStreamContigAbove32((uint8*)buf, depth, stripsize / rowsPerStrip); } } else { ps_buf = new tdata_t[nbchannels]; uint32 scanLineSize = stripsize / rowsPerStrip; dbgFile << " scanLineSize for each plan =" << scanLineSize; uint32 * lineSizes = new uint32[nbchannels]; for (uint i = 0; i < nbchannels; i++) { ps_buf[i] = _TIFFmalloc(stripsize); lineSizes[i] = scanLineSize / lineSizeCoeffs[i]; } tiffstream = new KisBufferStreamSeperate((uint8**) ps_buf, nbchannels, depth, lineSizes); delete [] lineSizes; } dbgFile << "Scanline size =" << TIFFRasterScanlineSize(image) << " / strip size =" << TIFFStripSize(image) << " / rowsPerStrip =" << rowsPerStrip << " stripsize/rowsPerStrip =" << stripsize / rowsPerStrip; uint32 y = 0; dbgFile << " NbOfStrips =" << TIFFNumberOfStrips(image) << " rowsPerStrip =" << rowsPerStrip << " stripsize =" << stripsize; for (uint32 strip = 0; y < height; strip++) { if (planarconfig == PLANARCONFIG_CONTIG) { TIFFReadEncodedStrip(image, TIFFComputeStrip(image, y, 0) , buf, (tsize_t) - 1); } else { for (uint i = 0; i < nbchannels; i++) { TIFFReadEncodedStrip(image, TIFFComputeStrip(image, y, i), ps_buf[i], (tsize_t) - 1); } } for (uint32 yinstrip = 0 ; yinstrip < rowsPerStrip && y < height ;) { uint linesread = tiffReader->copyDataToChannels(0, y, width, tiffstream); y += linesread; yinstrip += linesread; tiffstream->moveToLine(yinstrip); } tiffstream->restart(); } } tiffReader->finalize(); delete[] lineSizeCoeffs; delete tiffReader; delete tiffstream; if (planarconfig == PLANARCONFIG_CONTIG) { _TIFFfree(buf); } else { for (uint i = 0; i < nbchannels; i++) { _TIFFfree(ps_buf[i]); } delete[] ps_buf; } m_image->addNode(KisNodeSP(layer), m_image->rootLayer().data()); return ImportExportCodes::OK; } KisImportExportErrorCode KisTIFFConverter::buildImage(const QString &filename) { return decode(filename); } KisImageSP KisTIFFConverter::image() { return m_image; } KisImportExportErrorCode KisTIFFConverter::buildFile(const QString &filename, KisImageSP kisimage, KisTIFFOptions options) { dbgFile << "Start writing TIFF File"; KIS_ASSERT_RECOVER_RETURN_VALUE(kisimage, ImportExportCodes::InternalError); // Open file for writing TIFF *image; if ((image = TIFFOpen(QFile::encodeName(filename), "w")) == 0) { dbgFile << "Could not open the file for writing" << filename; return ImportExportCodes::NoAccessToWrite; } // Set the document information KoDocumentInfo * info = m_doc->documentInfo(); QString title = info->aboutInfo("title"); if (!title.isEmpty()) { if (!TIFFSetField(image, TIFFTAG_DOCUMENTNAME, title.toLatin1().constData())) { TIFFClose(image); return ImportExportCodes::ErrorWhileWriting; } } QString abstract = info->aboutInfo("description"); if (!abstract.isEmpty()) { if (!TIFFSetField(image, TIFFTAG_IMAGEDESCRIPTION, abstract.toLatin1().constData())) { TIFFClose(image); return ImportExportCodes::ErrorWhileWriting; } } QString author = info->authorInfo("creator"); if (!author.isEmpty()) { if(!TIFFSetField(image, TIFFTAG_ARTIST, author.toLatin1().constData())) { TIFFClose(image); return ImportExportCodes::ErrorWhileWriting; } } dbgFile << "xres: " << INCH_TO_POINT(kisimage->xRes()) << " yres: " << INCH_TO_POINT(kisimage->yRes()); if (!TIFFSetField(image, TIFFTAG_XRESOLUTION, INCH_TO_POINT(kisimage->xRes()))) { // It is the "invert" macro because we convert from pointer-per-inchs to points TIFFClose(image); return ImportExportCodes::ErrorWhileWriting; } if (!TIFFSetField(image, TIFFTAG_YRESOLUTION, INCH_TO_POINT(kisimage->yRes()))) { TIFFClose(image); return ImportExportCodes::ErrorWhileWriting; } KisGroupLayer* root = dynamic_cast(kisimage->rootLayer().data()); KIS_ASSERT_RECOVER(root) { TIFFClose(image); return ImportExportCodes::InternalError; } KisTIFFWriterVisitor* visitor = new KisTIFFWriterVisitor(image, &options); if (!(visitor->visit(root))) { TIFFClose(image); return ImportExportCodes::Failure; } TIFFClose(image); return ImportExportCodes::OK; } void KisTIFFConverter::cancel() { m_stop = true; }