diff --git a/config-oiio.h.cmake b/config-oiio.h.cmake deleted file mode 100644 index ac4ea698e7..0000000000 --- a/config-oiio.h.cmake +++ /dev/null @@ -1,7 +0,0 @@ -/* config-ocio.h. Generated by cmake from config-ocio.h.cmake */ - -/* Define if you have ocio, the OpenColorIO Library */ -#cmakedefine HAVE_OCIO 1 - - - diff --git a/krita/data/symbols/CMakeLists.txt b/krita/data/symbols/CMakeLists.txt index 483d7399e5..889d695435 100644 --- a/krita/data/symbols/CMakeLists.txt +++ b/krita/data/symbols/CMakeLists.txt @@ -1,6 +1,7 @@ install( FILES BalloonSymbols.svg pepper_carrot_speech_bubbles.svg + preset_icons.svg DESTINATION ${DATA_INSTALL_DIR}/krita/symbols) diff --git a/krita/data/symbols/preset_icons.svg b/krita/data/symbols/preset_icons.svg new file mode 100644 index 0000000000..1c29478ddb --- /dev/null +++ b/krita/data/symbols/preset_icons.svg @@ -0,0 +1,2281 @@ + + + Krita Brush Preset Icon Library + + + + image/svg+xml + + Krita Brush Preset Icon Library + + + Wolthera van Hövell tot Westerflier +Ramon Miranda +David Revoy + + + + + Krita Foundation(see contributors) + + + + + Krita Foundation + + + A set of symbols for making presets icons easier. + July 2017 + + + + + + + + + + + + + + + + + + + + + + Background gradient rectangle + + + + Preset Layout + + + + + + + + + Kneadable Eraser Preset Icon + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Spray can + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Cutter + + + + + + + + + + + + + + + + + + + + + + + + + Palette Knife + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Thin Brush + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Wide Brush + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Fineliner + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Brushpen + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Sharpie + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Thin Marker + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Wide Marker + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Long Marker + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Brush Alchohol Marker + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Wide Alchohol Marker + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Thin Alchohol Marker + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Pastel + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Wide Charcoal Strick + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Medium Charcoal Stick + + + + + + + + Thin Charcoal Stick + + + + + + + + + + + + + + + + + + + + + + + + + + Pencil with White Core + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Charcoal Pencil + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Yellow Pencil + + + + + + + + + + + + + + + + + + + + + + + + + Green Pencil + + + + + + + + + + + + + + + + + + + + + KRITAN + + + 648 + + + Big Eraser 1 + + + + + + + + + + + + + + + + + + + + Big Eraser 2 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Mechanical Pencil + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Red Pen + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Ballpoint Pen + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tablet Stylus + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Wet Wide Brush + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Wide Brush + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Sponge + + + + + + + + + + + + + + + + + + + + + Base for Tablet stylus + + + + + + + + + + + + + + + + + + Base for Red Pen + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tool base for a blue pen + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Toolbase for a ballpoint pen + + + + + + + + + + + + + + + + + + Base for a fineliner + + + + + + + + + + + + + + + Toolbase for a white pen + + + + + + + + + + + + + Tool base for a wide marker + + + + + + + + + Tool base for a sharpie + + + + + + + + + + Base with red label + + + + + + + + + + Base for a marker or fineliner + + + + + + + + + + + Base for a thin marker + + + + + + + + + + + + + + + Toolbase for a very thin marker + + + + + + + + Base for a thin alchoholmarker + + + + + + + Base for a wide alchol marker + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + A thin spray can + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + A Wide Spray Can + + + + + + + + + + + + Base for a red pencil + + + + + Base ofr a green pencil + + + + + + + + + + Base for a blue pencil + + + + + + + + + + + + + + Base with low yellow band + + + + + + + + + + + + + Base with high yellow band + + + + + + + + + Brown base for a brush + + + + + + + + + + Green base for a brush + + + + + + + + + + + A Wooden Base + + + + + + + + + + + + + + + Head for a small alchohol marker + + + + + + + + + + + + + + + + Head for a big alchohol marker + + + + + + + + + + Head for a White Pencil + + + + + + + + + + Head for a black pencil + + + + + + + + + + + + + + + Head for a pencil + + + + + + Pencilhead + + + + + + + A thin brush head + + + + + + + A wide brush head + + + + + + + + + + + Palette Knife Head + + + + + + + + + + + + + + + + Head for a cutting knife + + + + + + + + + + + + + Fineliner Head + + + + + + + + + + + Mechanical Pencil Head + + + + + + + + + Ballpoint Head 1 + + + + + + + + Ballpoint Head 2 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Chisel Marker Head + + + + + + + + + + Thin Chisel Marker Head + + + + + + + + + + Round Marker Head + + + + + + + + + + Wide Chisel Marker Head + + + + + + + + + + + + + + + Sharpie Marker Head + + + + + + + + + + + + + Long Chisel Marker Head + + + + + + + + + + Tablet Stylus Head + + + + + + + + + + + + Very Wide Alchohol Marker Chisel Head + + + + + + + + + + + + Very Wide Alchohol Marker Head + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/libs/ui/KisNodeDelegate.cpp b/libs/ui/KisNodeDelegate.cpp index 19fae61fb7..49b8838d6e 100644 --- a/libs/ui/KisNodeDelegate.cpp +++ b/libs/ui/KisNodeDelegate.cpp @@ -1,883 +1,895 @@ /* Copyright (c) 2006 Gábor Lehel Copyright (c) 2008 Cyrille Berger Copyright (c) 2011 José Luis Vergara 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 "kis_config.h" #include "KisNodeDelegate.h" #include "kis_node_model.h" #include "KisNodeToolTip.h" #include "KisNodeView.h" #include "KisPart.h" #include "input/kis_input_manager.h" #include #include #include #include #include #include #include #include #include #include #include #include "kis_node_view_color_scheme.h" #include "kis_icon_utils.h" #include "kis_layer_properties_icons.h" #include "krita_utils.h" +#include "kis_config_notifier.h" typedef KisBaseNode::Property* OptionalProperty; #include class KisNodeDelegate::Private { public: Private() : view(0), edit(0) { } KisNodeView *view; QPointer edit; KisNodeToolTip tip; + QColor checkersColor1; + QColor checkersColor2; + QList rightmostProperties(const KisBaseNode::PropertyList &props) const; int numProperties(const QModelIndex &index) const; OptionalProperty findProperty(KisBaseNode::PropertyList &props, const OptionalProperty &refProp) const; OptionalProperty findVisibilityProperty(KisBaseNode::PropertyList &props) const; void toggleProperty(KisBaseNode::PropertyList &props, OptionalProperty prop, bool controlPressed, const QModelIndex &index); }; KisNodeDelegate::KisNodeDelegate(KisNodeView *view, QObject *parent) : QAbstractItemDelegate(parent) , d(new Private) { d->view = view; QApplication::instance()->installEventFilter(this); + connect(KisConfigNotifier::instance(), SIGNAL(configChanged()), SLOT(slotConfigChanged())); + slotConfigChanged(); } KisNodeDelegate::~KisNodeDelegate() { delete d; } QSize KisNodeDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const { Q_UNUSED(index); KisNodeViewColorScheme scm; return QSize(option.rect.width(), scm.rowHeight()); } void KisNodeDelegate::paint(QPainter *p, const QStyleOptionViewItem &o, const QModelIndex &index) const { p->save(); { QStyleOptionViewItem option = getOptions(o, index); QStyle *style = option.widget ? option.widget->style() : QApplication::style(); style->drawPrimitive(QStyle::PE_PanelItemViewItem, &option, p, option.widget); bool shouldGrayOut = index.data(KisNodeModel::ShouldGrayOutRole).toBool(); if (shouldGrayOut) { option.state &= ~QStyle::State_Enabled; } p->setFont(option.font); drawColorLabel(p, option, index); drawFrame(p, option, index); drawThumbnail(p, option, index); drawText(p, option, index); drawIcons(p, option, index); drawVisibilityIconHijack(p, option, index); drawDecoration(p, option, index); drawExpandButton(p, option, index); drawBranch(p, option, index); drawProgressBar(p, option, index); } p->restore(); } void KisNodeDelegate::drawBranch(QPainter *p, const QStyleOptionViewItem &option, const QModelIndex &index) const { Q_UNUSED(index); KisNodeViewColorScheme scm; const QPoint base = scm.relThumbnailRect().translated(option.rect.topLeft()).topLeft() - QPoint( scm.indentation(), 0); // there is no indention if we are starting negative, so don't draw a branch if (base.x() < 0) { return; } QPen oldPen = p->pen(); const qreal oldOpacity = p->opacity(); // remember previous opacity p->setOpacity(1.0); QColor color = scm.gridColor(option, d->view); QColor bgColor = option.state & QStyle::State_Selected ? qApp->palette().color(QPalette::Base) : qApp->palette().color(QPalette::Text); color = KritaUtils::blendColors(color, bgColor, 0.9); // TODO: if we are a mask type, use dotted lines for the branch style // p->setPen(QPen(p->pen().color(), 2, Qt::DashLine, Qt::RoundCap, Qt::RoundJoin)); p->setPen(QPen(color, 0, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin)); QPoint p2 = base + QPoint(scm.iconSize() - scm.decorationMargin()*2, scm.iconSize()*0.45); QPoint p3 = base + QPoint(scm.iconSize() - scm.decorationMargin()*2, scm.iconSize()); QPoint p4 = base + QPoint(scm.iconSize()*1.4, scm.iconSize()); p->drawLine(p2, p3); p->drawLine(p3, p4); // draw parent lines (keep drawing until x position is less than 0 QPoint p5 = p2 - QPoint(scm.indentation(), 0); QPoint p6 = p3 - QPoint(scm.indentation(), 0); QPoint parentBase1 = p5; QPoint parentBase2 = p6; // indent lines needs to be very subtle to avoid making the docker busy looking color = KritaUtils::blendColors(color, bgColor, 0.9); // makes it a little lighter than L lines p->setPen(QPen(color, 0, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin)); while (parentBase1.x() > scm.visibilityColumnWidth()) { p->drawLine(parentBase1, parentBase2); parentBase1 = parentBase1 - QPoint(scm.indentation(), 0); parentBase2 = parentBase2 - QPoint(scm.indentation(), 0); } p->setPen(oldPen); p->setOpacity(oldOpacity); } void KisNodeDelegate::drawColorLabel(QPainter *p, const QStyleOptionViewItem &option, const QModelIndex &index) const { KisNodeViewColorScheme scm; const int label = index.data(KisNodeModel::ColorLabelIndexRole).toInt(); QColor color = scm.colorLabel(label); if (color.alpha() <= 0) return; QColor bgColor = qApp->palette().color(QPalette::Base); color = KritaUtils::blendColors(color, bgColor, 0.3); const QRect rect = option.state & QStyle::State_Selected ? iconsRect(option, index) : option.rect.adjusted(-scm.indentation(), 0, 0, 0); p->fillRect(rect, color); } void KisNodeDelegate::drawFrame(QPainter *p, const QStyleOptionViewItem &option, const QModelIndex &index) const { KisNodeViewColorScheme scm; QPen oldPen = p->pen(); p->setPen(scm.gridColor(option, d->view)); const QPoint base = option.rect.topLeft(); QPoint p2 = base + QPoint(-scm.indentation() - 1, 0); QPoint p3 = base + QPoint(2 * scm.decorationMargin() + scm.decorationSize(), 0); QPoint p4 = base + QPoint(-1, 0); QPoint p5(iconsRect(option, index).left() - 1, base.y()); QPoint p6(option.rect.right(), base.y()); QPoint v(0, option.rect.height()); // draw a line that goes the length of the entire frame. one for the // top, and one for the bottom QPoint pTopLeft(0, option.rect.topLeft().y()); QPoint pTopRight(option.rect.bottomRight().x(),option.rect.topLeft().y() ); p->drawLine(pTopLeft, pTopRight); QPoint pBottomLeft(0, option.rect.topLeft().y() + scm.rowHeight()); QPoint pBottomRight(option.rect.bottomRight().x(),option.rect.topLeft().y() + scm.rowHeight() ); p->drawLine(pBottomLeft, pBottomRight); const bool paintForParent = index.parent().isValid() && !index.row(); if (paintForParent) { QPoint p1(-2 * scm.indentation() - 1, 0); p1 += base; p->drawLine(p1, p2); } QPoint k0(0, base.y()); QPoint k1(1 * scm.border() + 2 * scm.visibilityMargin() + scm.visibilitySize(), base.y()); p->drawLine(k0, k1); p->drawLine(k0 + v, k1 + v); p->drawLine(k0, k0 + v); p->drawLine(k1, k1 + v); p->drawLine(p2, p6); p->drawLine(p2 + v, p6 + v); p->drawLine(p2, p2 + v); p->drawLine(p3, p3 + v); p->drawLine(p4, p4 + v); p->drawLine(p5, p5 + v); p->drawLine(p6, p6 + v); //// For debugging purposes only //p->setPen(Qt::blue); //KritaUtils::renderExactRect(p, iconsRect(option, index)); //KritaUtils::renderExactRect(p, textRect(option, index)); //KritaUtils::renderExactRect(p, scm.relThumbnailRect().translated(option.rect.topLeft())); p->setPen(oldPen); } void KisNodeDelegate::drawThumbnail(QPainter *p, const QStyleOptionViewItem &option, const QModelIndex &index) const { KisNodeViewColorScheme scm; const int thumbSize = scm.thumbnailSize(); const qreal oldOpacity = p->opacity(); // remember previous opacity QImage img = index.data(int(KisNodeModel::BeginThumbnailRole) + thumbSize).value(); if (!(option.state & QStyle::State_Enabled)) { p->setOpacity(0.35); } QRect fitRect = scm.relThumbnailRect().translated(option.rect.topLeft()); QPoint offset; offset.setX((fitRect.width() - img.width()) / 2); offset.setY((fitRect.height() - img.height()) / 2); offset += fitRect.topLeft(); - KisConfig cfg; - // paint in a checkerboard pattern behind the layer contents to represent transparent const int step = scm.thumbnailSize() / 6; QImage checkers(2 * step, 2 * step, QImage::Format_ARGB32); QPainter gc(&checkers); - gc.fillRect(QRect(0, 0, step, step), cfg.checkersColor1()); - gc.fillRect(QRect(step, 0, step, step), cfg.checkersColor2()); - gc.fillRect(QRect(step, step, step, step), cfg.checkersColor1()); - gc.fillRect(QRect(0, step, step, step), cfg.checkersColor2()); + gc.fillRect(QRect(0, 0, step, step), d->checkersColor1); + gc.fillRect(QRect(step, 0, step, step), d->checkersColor2); + gc.fillRect(QRect(step, step, step, step), d->checkersColor1); + gc.fillRect(QRect(0, step, step, step), d->checkersColor2); QBrush brush(checkers); p->setBrushOrigin(offset); p->fillRect(img.rect().translated(offset), brush); p->drawImage(offset, img); p->setOpacity(oldOpacity); // restore old opacity QRect borderRect = kisGrowRect(img.rect(), 1).translated(offset); KritaUtils::renderExactRect(p, borderRect, scm.gridColor(option, d->view)); } QRect KisNodeDelegate::iconsRect(const QStyleOptionViewItem &option, const QModelIndex &index) const { KisNodeViewColorScheme scm; int propCount = d->numProperties(index); const int iconsWidth = propCount * (scm.iconSize() + 2 * scm.iconMargin()) + (propCount - 1) * scm.border(); const int x = option.rect.x() + option.rect.width() - (iconsWidth + scm.border()); const int y = option.rect.y() + scm.border(); return QRect(x, y, iconsWidth, scm.rowHeight() - scm.border()); } QRect KisNodeDelegate::textRect(const QStyleOptionViewItem &option, const QModelIndex &index) const { KisNodeViewColorScheme scm; static QFont f; static int minbearing = 1337 + 666; //can be 0 or negative, 2003 is less likely if (minbearing == 2003 || f != option.font) { f = option.font; //getting your bearings can be expensive, so we cache them minbearing = option.fontMetrics.minLeftBearing() + option.fontMetrics.minRightBearing(); } const int decorationOffset = 2 * scm.border() + 2 * scm.decorationMargin() + scm.decorationSize(); const int width = iconsRect(option, index).left() - option.rect.x() - scm.border() + minbearing - decorationOffset; return QRect(option.rect.x() - minbearing + decorationOffset, option.rect.y() + scm.border(), width, scm.rowHeight() - scm.border()); } void KisNodeDelegate::drawText(QPainter *p, const QStyleOptionViewItem &option, const QModelIndex &index) const { KisNodeViewColorScheme scm; const QRect rc = textRect(option, index) .adjusted(scm.textMargin(), 0, -scm.textMargin(), 0); QPen oldPen = p->pen(); const qreal oldOpacity = p->opacity(); // remember previous opacity p->setPen(option.palette.color(QPalette::Active,QPalette::Text )); if (!(option.state & QStyle::State_Enabled)) { p->setOpacity(0.55); } const QString text = index.data(Qt::DisplayRole).toString(); const QString elided = elidedText(p->fontMetrics(), rc.width(), Qt::ElideRight, text); p->drawText(rc, Qt::AlignLeft | Qt::AlignVCenter, elided); p->setPen(oldPen); // restore pen settings p->setOpacity(oldOpacity); } QList KisNodeDelegate::Private::rightmostProperties(const KisBaseNode::PropertyList &props) const { QList list; QList prependList; list << OptionalProperty(0); list << OptionalProperty(0); list << OptionalProperty(0); KisBaseNode::PropertyList::const_iterator it = props.constBegin(); KisBaseNode::PropertyList::const_iterator end = props.constEnd(); for (; it != end; ++it) { if (!it->isMutable) continue; if (it->id == KisLayerPropertiesIcons::visible.id()) { // noop... } else if (it->id == KisLayerPropertiesIcons::locked.id()) { list[0] = OptionalProperty(&(*it)); } else if (it->id == KisLayerPropertiesIcons::inheritAlpha.id()) { list[1] = OptionalProperty(&(*it)); } else if (it->id == KisLayerPropertiesIcons::alphaLocked.id()) { list[2] = OptionalProperty(&(*it)); } else { prependList.prepend(OptionalProperty(&(*it))); } } { QMutableListIterator i(prependList); i.toBack(); while (i.hasPrevious()) { OptionalProperty val = i.previous(); int emptyIndex = list.lastIndexOf(0); if (emptyIndex < 0) break; list[emptyIndex] = val; i.remove(); } } return prependList + list; } int KisNodeDelegate::Private::numProperties(const QModelIndex &index) const { KisBaseNode::PropertyList props = index.data(KisNodeModel::PropertiesRole).value(); QList realProps = rightmostProperties(props); return realProps.size(); } OptionalProperty KisNodeDelegate::Private::findProperty(KisBaseNode::PropertyList &props, const OptionalProperty &refProp) const { KisBaseNode::PropertyList::iterator it = props.begin(); KisBaseNode::PropertyList::iterator end = props.end(); for (; it != end; ++it) { if (it->id == refProp->id) { return &(*it); } } return 0; } OptionalProperty KisNodeDelegate::Private::findVisibilityProperty(KisBaseNode::PropertyList &props) const { KisBaseNode::PropertyList::iterator it = props.begin(); KisBaseNode::PropertyList::iterator end = props.end(); for (; it != end; ++it) { if (it->id == KisLayerPropertiesIcons::visible.id()) { return &(*it); } } return 0; } void KisNodeDelegate::drawIcons(QPainter *p, const QStyleOptionViewItem &option, const QModelIndex &index) const { KisNodeViewColorScheme scm; const QRect r = iconsRect(option, index); QTransform oldTransform = p->transform(); QPen oldPen = p->pen(); p->setTransform(QTransform::fromTranslate(r.x(), r.y())); p->setPen(scm.gridColor(option, d->view)); int x = 0; const int y = (scm.rowHeight() - scm.border() - scm.iconSize()) / 2; KisBaseNode::PropertyList props = index.data(KisNodeModel::PropertiesRole).value(); QList realProps = d->rightmostProperties(props); Q_FOREACH (OptionalProperty prop, realProps) { x += scm.iconMargin(); if (prop) { QIcon icon = prop->state.toBool() ? prop->onIcon : prop->offIcon; bool fullColor = prop->state.toBool() && option.state & QStyle::State_Enabled; const qreal oldOpacity = p->opacity(); // remember previous opacity if (fullColor) { p->setOpacity(1.0); } else { p->setOpacity(0.35); } p->drawPixmap(x, y, icon.pixmap(scm.iconSize(), QIcon::Normal)); p->setOpacity(oldOpacity); // restore old opacity } x += scm.iconSize() + scm.iconMargin(); p->drawLine(x, 0, x, scm.rowHeight() - scm.border()); x += scm.border(); } p->setTransform(oldTransform); p->setPen(oldPen); } QRect KisNodeDelegate::visibilityClickRect(const QStyleOptionViewItem &option, const QModelIndex &index) const { Q_UNUSED(index); KisNodeViewColorScheme scm; return QRect(scm.border(), scm.border() + option.rect.top(), 2 * scm.visibilityMargin() + scm.visibilitySize(), scm.rowHeight() - scm.border()); } QRect KisNodeDelegate::decorationClickRect(const QStyleOptionViewItem &option, const QModelIndex &index) const { Q_UNUSED(option); Q_UNUSED(index); KisNodeViewColorScheme scm; QRect realVisualRect = d->view->originalVisualRect(index); return QRect(realVisualRect.left(), scm.border() + realVisualRect.top(), 2 * scm.decorationMargin() + scm.decorationSize(), scm.rowHeight() - scm.border()); } void KisNodeDelegate::drawVisibilityIconHijack(QPainter *p, const QStyleOptionViewItem &option, const QModelIndex &index) const { /** * Small hack Alert: * * Here wepaint over the area that sits basically outside our layer's * row. Anyway, just update it later... */ KisNodeViewColorScheme scm; KisBaseNode::PropertyList props = index.data(KisNodeModel::PropertiesRole).value(); OptionalProperty prop = d->findVisibilityProperty(props); if (!prop) return; const int x = scm.border() + scm.visibilityMargin(); const int y = option.rect.top() + (scm.rowHeight() - scm.border() - scm.visibilitySize()) / 2; QIcon icon = prop->state.toBool() ? prop->onIcon : prop->offIcon; p->setOpacity(1.0); p->drawPixmap(x, y, icon.pixmap(scm.visibilitySize(), QIcon::Normal)); //// For debugging purposes only // p->save(); // p->setPen(Qt::blue); // KritaUtils::renderExactRect(p, visibilityClickRect(option, index)); // p->restore(); } void KisNodeDelegate::drawDecoration(QPainter *p, const QStyleOptionViewItem &option, const QModelIndex &index) const { KisNodeViewColorScheme scm; QIcon icon = index.data(Qt::DecorationRole).value(); if (!icon.isNull()) { QPixmap pixmap = icon.pixmap(scm.decorationSize(), (option.state & QStyle::State_Enabled) ? QIcon::Normal : QIcon::Disabled); const QRect rc = scm.relDecorationRect().translated(option.rect.topLeft()); const qreal oldOpacity = p->opacity(); // remember previous opacity if (!(option.state & QStyle::State_Enabled)) { p->setOpacity(0.35); } p->drawPixmap(rc.topLeft(), pixmap); p->setOpacity(oldOpacity); // restore old opacity } } void KisNodeDelegate::drawExpandButton(QPainter *p, const QStyleOptionViewItem &option, const QModelIndex &index) const { Q_UNUSED(index); KisNodeViewColorScheme scm; QRect rc = scm.relExpandButtonRect().translated(option.rect.topLeft()); rc = kisGrowRect(rc, 0); if (!(option.state & QStyle::State_Children)) { return; } QString iconName = option.state & QStyle::State_Open ? "arrow-down" : "arrow-right"; QIcon icon = KisIconUtils::loadIcon(iconName); QPixmap pixmap = icon.pixmap(rc.width(), (option.state & QStyle::State_Enabled) ? QIcon::Normal : QIcon::Disabled); p->drawPixmap(rc.topLeft(), pixmap); } void KisNodeDelegate::Private::toggleProperty(KisBaseNode::PropertyList &props, OptionalProperty clickedProperty, bool controlPressed, const QModelIndex &index) { QAbstractItemModel *model = view->model(); // Using Ctrl+click to enter stasis if (controlPressed && clickedProperty->canHaveStasis) { // STEP 0: Prepare to Enter or Leave control key stasis quint16 numberOfLeaves = model->rowCount(index.parent()); QModelIndex eachItem; // STEP 1: Go. if (clickedProperty->isInStasis == false) { // Enter /* Make every leaf of this node go State = False, saving the old property value to stateInStasis */ for (quint16 i = 0; i < numberOfLeaves; ++i) { // Foreach leaf in the node (index.parent()) eachItem = model->index(i, 1, index.parent()); // The entire property list has to be altered because model->setData cannot set individual properties KisBaseNode::PropertyList eachPropertyList = eachItem.data(KisNodeModel::PropertiesRole).value(); OptionalProperty prop = findProperty(eachPropertyList, clickedProperty); if (!prop) continue; prop->stateInStasis = prop->state.toBool(); prop->state = eachItem == index; prop->isInStasis = true; model->setData(eachItem, QVariant::fromValue(eachPropertyList), KisNodeModel::PropertiesRole); } for (quint16 i = 0; i < numberOfLeaves; ++i) { // Foreach leaf in the node (index.parent()) eachItem = model->index(i, 1, index.parent()); KisBaseNode::PropertyList eachPropertyList = eachItem.data(KisNodeModel::PropertiesRole).value(); OptionalProperty prop = findProperty(eachPropertyList, clickedProperty); if (!prop) continue; } } else { // Leave /* Make every leaf of this node go State = stateInStasis */ for (quint16 i = 0; i < numberOfLeaves; ++i) { eachItem = model->index(i, 1, index.parent()); // The entire property list has to be altered because model->setData cannot set individual properties KisBaseNode::PropertyList eachPropertyList = eachItem.data(KisNodeModel::PropertiesRole).value(); OptionalProperty prop = findProperty(eachPropertyList, clickedProperty); if (!prop) continue; prop->state = prop->stateInStasis; prop->isInStasis = false; model->setData(eachItem, QVariant::fromValue(eachPropertyList), KisNodeModel::PropertiesRole); } } } else { clickedProperty->state = !clickedProperty->state.toBool(); model->setData(index, QVariant::fromValue(props), KisNodeModel::PropertiesRole); } } bool KisNodeDelegate::editorEvent(QEvent *event, QAbstractItemModel *model, const QStyleOptionViewItem &option, const QModelIndex &index) { KisNodeViewColorScheme scm; if ((event->type() == QEvent::MouseButtonPress || event->type() == QEvent::MouseButtonDblClick) && (index.flags() & Qt::ItemIsEnabled)) { QMouseEvent *mouseEvent = static_cast(event); /** * Small hack Alert: * * Here we handle clicking even when it happened outside * the rectangle of the current index. The point is, we * use some virtual scroling offset to move the tree to the * right of the visibility icon. So the icon itself is placed * in an empty area that doesn't belong to any index. But we still * handle it. */ const QRect iconsRect = this->iconsRect(option, index); const bool iconsClicked = iconsRect.isValid() && iconsRect.contains(mouseEvent->pos()); const QRect visibilityRect = visibilityClickRect(option, index); const bool visibilityClicked = visibilityRect.isValid() && visibilityRect.contains(mouseEvent->pos()); const QRect decorationRect = decorationClickRect(option, index); const bool decorationClicked = decorationRect.isValid() && decorationRect.contains(mouseEvent->pos()); const bool leftButton = mouseEvent->buttons() & Qt::LeftButton; if (leftButton && iconsClicked) { KisBaseNode::PropertyList props = index.data(KisNodeModel::PropertiesRole).value(); QList realProps = d->rightmostProperties(props); const int numProps = realProps.size(); const int iconWidth = scm.iconSize() + 2 * scm.iconMargin() + scm.border(); const int xPos = mouseEvent->pos().x() - iconsRect.left(); const int clickedIcon = xPos / iconWidth; const int distToBorder = qMin(xPos % iconWidth, iconWidth - xPos % iconWidth); if (iconsClicked && clickedIcon >= 0 && clickedIcon < numProps && distToBorder > scm.iconMargin()) { OptionalProperty clickedProperty = realProps[clickedIcon]; if (!clickedProperty) return false; d->toggleProperty(props, clickedProperty, mouseEvent->modifiers() == Qt::ControlModifier, index); return true; } } else if (leftButton && visibilityClicked) { KisBaseNode::PropertyList props = index.data(KisNodeModel::PropertiesRole).value(); OptionalProperty clickedProperty = d->findVisibilityProperty(props); if (!clickedProperty) return false; d->toggleProperty(props, clickedProperty, mouseEvent->modifiers() == Qt::ControlModifier, index); return true; } else if (leftButton && decorationClicked) { bool isExpandable = model->hasChildren(index); if (isExpandable) { bool isExpanded = d->view->isExpanded(index); d->view->setExpanded(index, !isExpanded); return true; } } if (mouseEvent->button() == Qt::LeftButton && mouseEvent->modifiers() == Qt::AltModifier) { d->view->setCurrentIndex(index); model->setData(index, true, KisNodeModel::AlternateActiveRole); return true; } } else if (event->type() == QEvent::ToolTip) { if (!KisConfig().hidePopups()) { QHelpEvent *helpEvent = static_cast(event); d->tip.showTip(d->view, helpEvent->pos(), option, index); } return true; } else if (event->type() == QEvent::Leave) { d->tip.hide(); } return false; } QWidget *KisNodeDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem&, const QModelIndex&) const { d->edit = new QLineEdit(parent); d->edit->installEventFilter(const_cast(this)); //hack? return d->edit; } void KisNodeDelegate::setEditorData(QWidget *widget, const QModelIndex &index) const { QLineEdit *edit = qobject_cast(widget); Q_ASSERT(edit); edit->setText(index.data(Qt::DisplayRole).toString()); } void KisNodeDelegate::setModelData(QWidget *widget, QAbstractItemModel *model, const QModelIndex &index) const { QLineEdit *edit = qobject_cast(widget); Q_ASSERT(edit); model->setData(index, edit->text(), Qt::DisplayRole); } void KisNodeDelegate::updateEditorGeometry(QWidget *widget, const QStyleOptionViewItem &option, const QModelIndex &index) const { Q_UNUSED(index); widget->setGeometry(option.rect); } // PROTECTED bool KisNodeDelegate::eventFilter(QObject *object, QEvent *event) { switch (event->type()) { case QEvent::MouseButtonPress: { if (d->edit) { QMouseEvent *me = static_cast(event); if (!QRect(d->edit->mapToGlobal(QPoint()), d->edit->size()).contains(me->globalPos())) { emit commitData(d->edit); emit closeEditor(d->edit); } } } break; case QEvent::KeyPress: { QLineEdit *edit = qobject_cast(object); if (edit && edit == d->edit) { QKeyEvent *ke = static_cast(event); switch (ke->key()) { case Qt::Key_Escape: emit closeEditor(edit); return true; case Qt::Key_Tab: emit commitData(edit); emit closeEditor(edit,EditNextItem); return true; case Qt::Key_Backtab: emit commitData(edit); emit closeEditor(edit, EditPreviousItem); return true; case Qt::Key_Return: case Qt::Key_Enter: emit commitData(edit); emit closeEditor(edit); return true; default: break; } } } break; case QEvent::ShortcutOverride : { QLineEdit *edit = qobject_cast(object); if (edit && edit == d->edit){ auto* key = static_cast(event); if (key->modifiers() == Qt::NoModifier){ switch (key->key()){ case Qt::Key_Escape: case Qt::Key_Tab: case Qt::Key_Backtab: case Qt::Key_Return: case Qt::Key_Enter: event->accept(); return true; default: break; } } } } break; case QEvent::FocusOut : { QLineEdit *edit = qobject_cast(object); if (edit && edit == d->edit) { emit commitData(edit); emit closeEditor(edit); } } default: break; } return QAbstractItemDelegate::eventFilter(object, event); } // PRIVATE QStyleOptionViewItem KisNodeDelegate::getOptions(const QStyleOptionViewItem &o, const QModelIndex &index) { QStyleOptionViewItem option = o; QVariant v = index.data(Qt::FontRole); if (v.isValid()) { option.font = v.value(); option.fontMetrics = QFontMetrics(option.font); } v = index.data(Qt::TextAlignmentRole); if (v.isValid()) option.displayAlignment = QFlag(v.toInt()); v = index.data(Qt::TextColorRole); if (v.isValid()) option.palette.setColor(QPalette::Text, v.value()); v = index.data(Qt::BackgroundColorRole); if (v.isValid()) option.palette.setColor(QPalette::Window, v.value()); return option; } QRect KisNodeDelegate::progressBarRect(const QStyleOptionViewItem &option, const QModelIndex &index) const { return iconsRect(option, index); } void KisNodeDelegate::drawProgressBar(QPainter *p, const QStyleOptionViewItem &option, const QModelIndex &index) const { QVariant value = index.data(KisNodeModel::ProgressRole); if (!value.isNull() && (value.toInt() >= 0 && value.toInt() <= 100)) { const QRect r = progressBarRect(option, index); p->save(); { p->setClipRect(r); QStyle* style = QApplication::style(); QStyleOptionProgressBar opt; opt.minimum = 0; opt.maximum = 100; opt.progress = value.toInt(); opt.textVisible = true; opt.textAlignment = Qt::AlignHCenter; opt.text = i18n("%1 %", opt.progress); opt.rect = r; opt.orientation = Qt::Horizontal; opt.state = option.state; style->drawControl(QStyle::CE_ProgressBar, &opt, p, 0); } p->restore(); } } + +void KisNodeDelegate::slotConfigChanged() +{ + KisConfig cfg; + + d->checkersColor1 = cfg.checkersColor1(); + d->checkersColor2 = cfg.checkersColor2(); +} diff --git a/libs/ui/KisNodeDelegate.h b/libs/ui/KisNodeDelegate.h index 9ccd9a693f..1cd0606c0a 100644 --- a/libs/ui/KisNodeDelegate.h +++ b/libs/ui/KisNodeDelegate.h @@ -1,82 +1,85 @@ /* Copyright (c) 2006 Gábor Lehel 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_DOCUMENT_SECTION_DELEGATE_H #define KIS_DOCUMENT_SECTION_DELEGATE_H #include class KisNodeView; class KisNodeModel; /** * See KisNodeModel and KisNodeView. * * A delegate provides the gui machinery, using Qt's model/view terminology. * This class is owned by KisNodeView to do the work of generating the * graphical representation of each item. */ class KisNodeDelegate: public QAbstractItemDelegate { Q_OBJECT public: explicit KisNodeDelegate(KisNodeView *view, QObject *parent = 0); ~KisNodeDelegate() override; void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override; QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override; bool editorEvent(QEvent *event, QAbstractItemModel *model, const QStyleOptionViewItem &option, const QModelIndex &index) override; QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const override; void setEditorData(QWidget *editor, const QModelIndex &index) const override; void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override; void updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex& index) const override; protected: bool eventFilter(QObject *object, QEvent *event) override; private: typedef KisNodeModel Model; typedef KisNodeView View; class Private; Private* const d; static QStyleOptionViewItem getOptions(const QStyleOptionViewItem &option, const QModelIndex &index); QRect progressBarRect(const QStyleOptionViewItem &option, const QModelIndex &index) const; void drawProgressBar(QPainter *p, const QStyleOptionViewItem &option, const QModelIndex &index) const; void drawBranch(QPainter *p, const QStyleOptionViewItem &option, const QModelIndex &index) const; void drawColorLabel(QPainter *p, const QStyleOptionViewItem &option, const QModelIndex &index) const; void drawFrame(QPainter *p, const QStyleOptionViewItem &option, const QModelIndex &index) const; void drawThumbnail(QPainter *p, const QStyleOptionViewItem &option, const QModelIndex &index) const; QRect iconsRect(const QStyleOptionViewItem &option, const QModelIndex &index) const; QRect textRect(const QStyleOptionViewItem &option, const QModelIndex &index) const; void drawText(QPainter *p, const QStyleOptionViewItem &option, const QModelIndex &index) const; void drawIcons(QPainter *p, const QStyleOptionViewItem &option, const QModelIndex &index) const; QRect visibilityClickRect(const QStyleOptionViewItem &option, const QModelIndex &index) const; QRect decorationClickRect(const QStyleOptionViewItem &option, const QModelIndex &index) const; void drawVisibilityIconHijack(QPainter *p, const QStyleOptionViewItem &option, const QModelIndex &index) const; void drawDecoration(QPainter *p, const QStyleOptionViewItem &option, const QModelIndex &index) const; void drawExpandButton(QPainter *p, const QStyleOptionViewItem &option, const QModelIndex &index) const; + +private Q_SLOTS: + void slotConfigChanged(); }; #endif diff --git a/libs/ui/canvas/kis_coordinates_converter.cpp b/libs/ui/canvas/kis_coordinates_converter.cpp index b8329a97a6..36f6aa4805 100644 --- a/libs/ui/canvas/kis_coordinates_converter.cpp +++ b/libs/ui/canvas/kis_coordinates_converter.cpp @@ -1,441 +1,441 @@ /* * Copyright (c) 2010 Dmitry Kazakov * Copyright (c) 2011 Silvio Heinrich * * 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_coordinates_converter.h" #include #include #include #include struct KisCoordinatesConverter::Private { Private(): isXAxisMirrored(false), isYAxisMirrored(false), rotationAngle(0.0) { } KisImageWSP image; bool isXAxisMirrored; bool isYAxisMirrored; qreal rotationAngle; QSizeF canvasWidgetSize; QPointF documentOffset; QTransform flakeToWidget; QTransform imageToDocument; QTransform documentToFlake; QTransform widgetToViewport; }; /** * When vastScrolling value is less than 0.5 it is possible * that the whole scrolling area (viewport) will be smaller than * the size of the widget. In such cases the image should be * centered in the widget. Previously we used a special parameter * documentOrigin for this purpose, now the value for this * centering is calculated dynamically, helping the offset to * center the image inside the widget * * Note that the correction is null when the size of the document * plus vast scrolling reserve is larger than the widget. This * is always true for vastScrolling parameter > 0.5. */ QPointF KisCoordinatesConverter::centeringCorrection() const { KisConfig cfg; QSize documentSize = imageRectInWidgetPixels().toAlignedRect().size(); QPointF dPoint(documentSize.width(), documentSize.height()); QPointF wPoint(m_d->canvasWidgetSize.width(), m_d->canvasWidgetSize.height()); QPointF minOffset = -cfg.vastScrolling() * wPoint; QPointF maxOffset = dPoint - wPoint + cfg.vastScrolling() * wPoint; QPointF range = maxOffset - minOffset; range.rx() = qMin(range.x(), (qreal)0.0); range.ry() = qMin(range.y(), (qreal)0.0); range /= 2; return -range; } /** * The document offset and the position of the top left corner of the * image must always coincide, that is why we need to correct them to * and fro. * * When we change zoom level, the calculation of the new offset is * done by KoCanvasControllerWidget, that is why we just passively fix * the flakeToWidget transform to conform the offset and wait until * the canvas controller will recenter us. * * But when we do our own transformations of the canvas, like rotation * and mirroring, we cannot rely on the centering of the canvas * controller and we do it ourselves. Then we just set new offset and * return its value to be set in the canvas controller explicitly. */ void KisCoordinatesConverter::correctOffsetToTransformation() { m_d->documentOffset = -(imageRectInWidgetPixels().topLeft() - centeringCorrection()).toPoint(); } void KisCoordinatesConverter::correctTransformationToOffset() { QPointF topLeft = imageRectInWidgetPixels().topLeft(); QPointF diff = (-topLeft) - m_d->documentOffset; diff += centeringCorrection(); m_d->flakeToWidget *= QTransform::fromTranslate(diff.x(), diff.y()); } void KisCoordinatesConverter::recalculateTransformations() { if(!m_d->image) return; m_d->imageToDocument = QTransform::fromScale(1 / m_d->image->xRes(), 1 / m_d->image->yRes()); qreal zoomX, zoomY; KoZoomHandler::zoom(&zoomX, &zoomY); m_d->documentToFlake = QTransform::fromScale(zoomX, zoomY); correctTransformationToOffset(); QRectF irect = imageRectInWidgetPixels(); QRectF wrect = QRectF(QPoint(0,0), m_d->canvasWidgetSize); QRectF rrect = irect & wrect; QTransform reversedTransform = flakeToWidgetTransform().inverted(); QRectF canvasBounds = reversedTransform.mapRect(rrect); QPointF offset = canvasBounds.topLeft(); m_d->widgetToViewport = reversedTransform * QTransform::fromTranslate(-offset.x(), -offset.y()); } KisCoordinatesConverter::KisCoordinatesConverter() : m_d(new Private) { } KisCoordinatesConverter::~KisCoordinatesConverter() { delete m_d; } void KisCoordinatesConverter::setCanvasWidgetSize(QSize size) { m_d->canvasWidgetSize = size; recalculateTransformations(); } void KisCoordinatesConverter::setImage(KisImageWSP image) { m_d->image = image; recalculateTransformations(); } void KisCoordinatesConverter::setDocumentOffset(const QPoint& offset) { QPointF diff = m_d->documentOffset - offset; m_d->documentOffset = offset; m_d->flakeToWidget *= QTransform::fromTranslate(diff.x(), diff.y()); recalculateTransformations(); } QPoint KisCoordinatesConverter::documentOffset() const { return QPoint(int(m_d->documentOffset.x()), int(m_d->documentOffset.y())); } qreal KisCoordinatesConverter::rotationAngle() const { return m_d->rotationAngle; } void KisCoordinatesConverter::setZoom(qreal zoom) { KoZoomHandler::setZoom(zoom); recalculateTransformations(); } qreal KisCoordinatesConverter::effectiveZoom() const { qreal scaleX, scaleY; this->imageScale(&scaleX, &scaleY); if (scaleX != scaleY) { qWarning() << "WARNING: Zoom is not isotropic!" << ppVar(scaleX) << ppVar(scaleY) << ppVar(qFuzzyCompare(scaleX, scaleY)); } // zoom by average of x and y return 0.5 * (scaleX + scaleY); } QPoint KisCoordinatesConverter::rotate(QPointF center, qreal angle) { QTransform rot; rot.rotate(angle); m_d->flakeToWidget *= QTransform::fromTranslate(-center.x(),-center.y()); m_d->flakeToWidget *= rot; m_d->flakeToWidget *= QTransform::fromTranslate(center.x(), center.y()); m_d->rotationAngle = std::fmod(m_d->rotationAngle + angle, 360.0); correctOffsetToTransformation(); recalculateTransformations(); return m_d->documentOffset.toPoint(); } QPoint KisCoordinatesConverter::mirror(QPointF center, bool mirrorXAxis, bool mirrorYAxis) { bool keepOrientation = false; // XXX: Keep here for now, maybe some day we can restore the parameter again. bool doXMirroring = m_d->isXAxisMirrored ^ mirrorXAxis; bool doYMirroring = m_d->isYAxisMirrored ^ mirrorYAxis; qreal scaleX = doXMirroring ? -1.0 : 1.0; qreal scaleY = doYMirroring ? -1.0 : 1.0; QTransform mirror = QTransform::fromScale(scaleX, scaleY); QTransform rot; rot.rotate(m_d->rotationAngle); m_d->flakeToWidget *= QTransform::fromTranslate(-center.x(),-center.y()); if (keepOrientation) { m_d->flakeToWidget *= rot.inverted(); } m_d->flakeToWidget *= mirror; if (keepOrientation) { m_d->flakeToWidget *= rot; } m_d->flakeToWidget *= QTransform::fromTranslate(center.x(),center.y()); if (!keepOrientation && (doXMirroring ^ doYMirroring)) { m_d->rotationAngle = -m_d->rotationAngle; } m_d->isXAxisMirrored = mirrorXAxis; m_d->isYAxisMirrored = mirrorYAxis; correctOffsetToTransformation(); recalculateTransformations(); return m_d->documentOffset.toPoint(); } bool KisCoordinatesConverter::xAxisMirrored() const { return m_d->isXAxisMirrored; } bool KisCoordinatesConverter::yAxisMirrored() const { return m_d->isYAxisMirrored; } QPoint KisCoordinatesConverter::resetRotation(QPointF center) { QTransform rot; rot.rotate(-m_d->rotationAngle); m_d->flakeToWidget *= QTransform::fromTranslate(-center.x(), -center.y()); m_d->flakeToWidget *= rot; m_d->flakeToWidget *= QTransform::fromTranslate(center.x(), center.y()); m_d->rotationAngle = 0.0; correctOffsetToTransformation(); recalculateTransformations(); return m_d->documentOffset.toPoint(); } QTransform KisCoordinatesConverter::imageToWidgetTransform() const{ return m_d->imageToDocument * m_d->documentToFlake * m_d->flakeToWidget; } QTransform KisCoordinatesConverter::imageToDocumentTransform() const { return m_d->imageToDocument; } QTransform KisCoordinatesConverter::documentToFlakeTransform() const { return m_d->documentToFlake; } QTransform KisCoordinatesConverter::flakeToWidgetTransform() const { return m_d->flakeToWidget; } QTransform KisCoordinatesConverter::documentToWidgetTransform() const { return m_d->documentToFlake * m_d->flakeToWidget; } QTransform KisCoordinatesConverter::viewportToWidgetTransform() const { return m_d->widgetToViewport.inverted(); } QTransform KisCoordinatesConverter::imageToViewportTransform() const { return m_d->imageToDocument * m_d->documentToFlake * m_d->flakeToWidget * m_d->widgetToViewport; } void KisCoordinatesConverter::getQPainterCheckersInfo(QTransform *transform, QPointF *brushOrigin, - QPolygonF *polygon) const + QPolygonF *polygon, + const bool scrollCheckers) const { /** * Qt has different rounding for QPainter::drawRect/drawImage. * The image is rounded mathematically, while rect in aligned * to the next integer. That causes transparent line appear on * the canvas. * * See: https://bugreports.qt.nokia.com/browse/QTBUG-22827 */ QRectF imageRect = imageRectInViewportPixels(); imageRect.adjust(0,0,-0.5,-0.5); - KisConfig cfg; - if (cfg.scrollCheckers()) { + if (scrollCheckers) { *transform = viewportToWidgetTransform(); *polygon = imageRect; *brushOrigin = imageToViewport(QPointF(0,0)); } else { *transform = QTransform(); *polygon = viewportToWidgetTransform().map(imageRect); *brushOrigin = QPoint(0,0); } } void KisCoordinatesConverter::getOpenGLCheckersInfo(const QRectF &viewportRect, QTransform *textureTransform, QTransform *modelTransform, QRectF *textureRect, QRectF *modelRect, const bool scrollCheckers) const { if(scrollCheckers) { *textureTransform = QTransform(); *textureRect = QRectF(0, 0, viewportRect.width(),viewportRect.height()); } else { *textureTransform = viewportToWidgetTransform(); *textureRect = viewportRect; } *modelTransform = viewportToWidgetTransform(); *modelRect = viewportRect; } QPointF KisCoordinatesConverter::imageCenterInWidgetPixel() const { if(!m_d->image) return QPointF(); QPolygonF poly = imageToWidget(QPolygon(m_d->image->bounds())); return (poly[0] + poly[1] + poly[2] + poly[3]) / 4.0; } // these functions return a bounding rect if the canvas is rotated QRectF KisCoordinatesConverter::imageRectInWidgetPixels() const { if(!m_d->image) return QRectF(); return imageToWidget(m_d->image->bounds()); } QRectF KisCoordinatesConverter::imageRectInViewportPixels() const { if(!m_d->image) return QRectF(); return imageToViewport(m_d->image->bounds()); } QRect KisCoordinatesConverter::imageRectInImagePixels() const { if(!m_d->image) return QRect(); return m_d->image->bounds(); } QRectF KisCoordinatesConverter::imageRectInDocumentPixels() const { if(!m_d->image) return QRectF(); return imageToDocument(m_d->image->bounds()); } QSizeF KisCoordinatesConverter::imageSizeInFlakePixels() const { if(!m_d->image) return QSizeF(); qreal scaleX, scaleY; imageScale(&scaleX, &scaleY); QSize imageSize = m_d->image->size(); return QSizeF(imageSize.width() * scaleX, imageSize.height() * scaleY); } QRectF KisCoordinatesConverter::widgetRectInFlakePixels() const { return widgetToFlake(QRectF(QPoint(0,0), m_d->canvasWidgetSize)); } QPointF KisCoordinatesConverter::flakeCenterPoint() const { QRectF widgetRect = widgetRectInFlakePixels(); return QPointF(widgetRect.left() + widgetRect.width() / 2, widgetRect.top() + widgetRect.height() / 2); } QPointF KisCoordinatesConverter::widgetCenterPoint() const { return QPointF(m_d->canvasWidgetSize.width() / 2.0, m_d->canvasWidgetSize.height() / 2.0); } void KisCoordinatesConverter::imageScale(qreal *scaleX, qreal *scaleY) const { if(!m_d->image) { *scaleX = 1.0; *scaleY = 1.0; return; } // get the x and y zoom level of the canvas qreal zoomX, zoomY; KoZoomHandler::zoom(&zoomX, &zoomY); // Get the KisImage resolution qreal resX = m_d->image->xRes(); qreal resY = m_d->image->yRes(); // Compute the scale factors *scaleX = zoomX / resX; *scaleY = zoomY / resY; } diff --git a/libs/ui/canvas/kis_coordinates_converter.h b/libs/ui/canvas/kis_coordinates_converter.h index 803a5d94bd..e7ce76b2c9 100644 --- a/libs/ui/canvas/kis_coordinates_converter.h +++ b/libs/ui/canvas/kis_coordinates_converter.h @@ -1,162 +1,163 @@ /* * Copyright (c) 2010 Dmitry Kazakov * Copyright (c) 2011 Silvio Heinrich * * 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_COORDINATES_CONVERTER_H #define KIS_COORDINATES_CONVERTER_H #include #include #include "kritaui_export.h" #include "kis_types.h" #define EPSILON 1e-6 #define SCALE_LESS_THAN(scX, scY, value) \ (scX < (value) - EPSILON && scY < (value) - EPSILON) #define SCALE_MORE_OR_EQUAL_TO(scX, scY, value) \ (scX > (value) - EPSILON && scY > (value) - EPSILON) namespace _Private { template struct Traits { typedef T Result; static T map(const QTransform& transform, const T& obj) { return transform.map(obj); } }; template<> struct Traits { typedef QRectF Result; static QRectF map(const QTransform& transform, const QRectF& rc) { return transform.mapRect(rc); } }; template<> struct Traits: public Traits { }; template<> struct Traits: public Traits { }; template<> struct Traits: public Traits { }; template<> struct Traits: public Traits { }; } class KRITAUI_EXPORT KisCoordinatesConverter: public KoZoomHandler { public: KisCoordinatesConverter(); ~KisCoordinatesConverter() override; void setCanvasWidgetSize(QSize size); void setImage(KisImageWSP image); void setDocumentOffset(const QPoint &offset); QPoint documentOffset() const; qreal rotationAngle() const; QPoint rotate(QPointF center, qreal angle); QPoint mirror(QPointF center, bool mirrorXAxis, bool mirrorYAxis); bool xAxisMirrored() const; bool yAxisMirrored() const; QPoint resetRotation(QPointF center); void setZoom(qreal zoom) override; /** * A composition of to scale methods: zoom level + image resolution */ qreal effectiveZoom() const; template typename _Private::Traits::Result imageToViewport(const T& obj) const { return _Private::Traits::map(imageToViewportTransform(), obj); } template typename _Private::Traits::Result viewportToImage(const T& obj) const { return _Private::Traits::map(imageToViewportTransform().inverted(), obj); } template typename _Private::Traits::Result flakeToWidget(const T& obj) const { return _Private::Traits::map(flakeToWidgetTransform(), obj); } template typename _Private::Traits::Result widgetToFlake(const T& obj) const { return _Private::Traits::map(flakeToWidgetTransform().inverted(), obj); } template typename _Private::Traits::Result widgetToViewport(const T& obj) const { return _Private::Traits::map(viewportToWidgetTransform().inverted(), obj); } template typename _Private::Traits::Result viewportToWidget(const T& obj) const { return _Private::Traits::map(viewportToWidgetTransform(), obj); } template typename _Private::Traits::Result documentToWidget(const T& obj) const { return _Private::Traits::map(documentToWidgetTransform(), obj); } template typename _Private::Traits::Result widgetToDocument(const T& obj) const { return _Private::Traits::map(documentToWidgetTransform().inverted(), obj); } template typename _Private::Traits::Result imageToDocument(const T& obj) const { return _Private::Traits::map(imageToDocumentTransform(), obj); } template typename _Private::Traits::Result documentToImage(const T& obj) const { return _Private::Traits::map(imageToDocumentTransform().inverted(), obj); } template typename _Private::Traits::Result documentToFlake(const T& obj) const { return _Private::Traits::map(documentToFlakeTransform(), obj); } template typename _Private::Traits::Result flakeToDocument(const T& obj) const { return _Private::Traits::map(documentToFlakeTransform().inverted(), obj); } template typename _Private::Traits::Result imageToWidget(const T& obj) const { return _Private::Traits::map(imageToWidgetTransform(), obj); } template typename _Private::Traits::Result widgetToImage(const T& obj) const { return _Private::Traits::map(imageToWidgetTransform().inverted(), obj); } QTransform imageToWidgetTransform() const; QTransform imageToDocumentTransform() const; QTransform documentToFlakeTransform() const; QTransform imageToViewportTransform() const; QTransform viewportToWidgetTransform() const; QTransform flakeToWidgetTransform() const; QTransform documentToWidgetTransform() const; void getQPainterCheckersInfo(QTransform *transform, QPointF *brushOrigin, - QPolygonF *poligon) const; + QPolygonF *poligon, + const bool scrollCheckers) const; void getOpenGLCheckersInfo(const QRectF &viewportRect, QTransform *textureTransform, QTransform *modelTransform, QRectF *textureRect, QRectF *modelRect, - bool scrollCheckers) const; + const bool scrollCheckers) const; QPointF imageCenterInWidgetPixel() const; QRectF imageRectInWidgetPixels() const; QRectF imageRectInViewportPixels() const; QSizeF imageSizeInFlakePixels() const; QRectF widgetRectInFlakePixels() const; QRect imageRectInImagePixels() const; QRectF imageRectInDocumentPixels() const; QPointF flakeCenterPoint() const; QPointF widgetCenterPoint() const; void imageScale(qreal *scaleX, qreal *scaleY) const; private: friend class KisZoomAndPanTest; QPointF centeringCorrection() const; void correctOffsetToTransformation(); void correctTransformationToOffset(); void recalculateTransformations(); private: struct Private; Private * const m_d; }; #endif /* KIS_COORDINATES_CONVERTER_H */ diff --git a/libs/ui/canvas/kis_qpainter_canvas.cpp b/libs/ui/canvas/kis_qpainter_canvas.cpp index 387aeb2c76..35ce05b83f 100644 --- a/libs/ui/canvas/kis_qpainter_canvas.cpp +++ b/libs/ui/canvas/kis_qpainter_canvas.cpp @@ -1,261 +1,266 @@ /* * Copyright (C) Boudewijn Rempt , (C) 2006 * Copyright (C) Lukas Tvrdy , (C) 2009 * * 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_qpainter_canvas.h" #include #include #include #include #include #include #include #include #include #include #include #include #include +#include #include #include "kis_coordinates_converter.h" #include #include #include #include #include #include "KisViewManager.h" #include "kis_canvas2.h" #include "kis_prescaled_projection.h" #include "kis_config.h" #include "kis_canvas_resource_provider.h" #include "KisDocument.h" #include "kis_selection_manager.h" #include "kis_selection.h" #include "kis_canvas_updates_compressor.h" #include "kis_config_notifier.h" #include "kis_group_layer.h" #include "canvas/kis_display_color_converter.h" //#define DEBUG_REPAINT #include class KisQPainterCanvas::Private { public: KisPrescaledProjectionSP prescaledProjection; QBrush checkBrush; QImage buffer; + bool scrollCheckers; }; KisQPainterCanvas::KisQPainterCanvas(KisCanvas2 *canvas, KisCoordinatesConverter *coordinatesConverter, QWidget * parent) : QWidget(parent) , KisCanvasWidgetBase(canvas, coordinatesConverter) , m_d(new Private()) { setAutoFillBackground(true); setAcceptDrops(true); setFocusPolicy(Qt::StrongFocus); setAttribute(Qt::WA_InputMethodEnabled, true); setAttribute(Qt::WA_StaticContents); setAttribute(Qt::WA_OpaquePaintEvent); #ifdef Q_OS_OSX setAttribute(Qt::WA_AcceptTouchEvents, false); #else setAttribute(Qt::WA_AcceptTouchEvents, true); #endif connect(KisConfigNotifier::instance(), SIGNAL(configChanged()), SLOT(slotConfigChanged())); slotConfigChanged(); } KisQPainterCanvas::~KisQPainterCanvas() { delete m_d; } void KisQPainterCanvas::setPrescaledProjection(KisPrescaledProjectionSP prescaledProjection) { m_d->prescaledProjection = prescaledProjection; } void KisQPainterCanvas::paintEvent(QPaintEvent * ev) { KisImageWSP image = canvas()->image(); if (image == 0) return; setAutoFillBackground(false); if (m_d->buffer.size() != size()) { m_d->buffer = QImage(size(), QImage::Format_ARGB32_Premultiplied); } QPainter gc(&m_d->buffer); // we double buffer, so we paint on an image first, then from the image onto the canvas, // so copy the clip region since otherwise we're filling the whole buffer every time with // the background color _and_ the transparent squares. gc.setClipRegion(ev->region()); KisCoordinatesConverter *converter = coordinatesConverter(); gc.save(); gc.setCompositionMode(QPainter::CompositionMode_Source); gc.fillRect(QRect(QPoint(0, 0), size()), borderColor()); QTransform checkersTransform; QPointF brushOrigin; QPolygonF polygon; - converter->getQPainterCheckersInfo(&checkersTransform, &brushOrigin, &polygon); + converter->getQPainterCheckersInfo(&checkersTransform, &brushOrigin, &polygon, m_d->scrollCheckers); gc.setPen(Qt::NoPen); gc.setBrush(m_d->checkBrush); gc.setBrushOrigin(brushOrigin); gc.setTransform(checkersTransform); gc.drawPolygon(polygon); drawImage(gc, ev->rect()); gc.restore(); #ifdef DEBUG_REPAINT QColor color = QColor(random() % 255, random() % 255, random() % 255, 150); gc.fillRect(ev->rect(), color); #endif drawDecorations(gc, ev->rect()); gc.end(); QPainter painter(this); painter.drawImage(ev->rect(), m_d->buffer, ev->rect()); } void KisQPainterCanvas::drawImage(QPainter & gc, const QRect &updateWidgetRect) const { KisCoordinatesConverter *converter = coordinatesConverter(); QTransform imageTransform = converter->viewportToWidgetTransform(); gc.setTransform(imageTransform); gc.setRenderHint(QPainter::SmoothPixmapTransform, true); QRectF viewportRect = converter->widgetToViewport(updateWidgetRect); gc.setCompositionMode(QPainter::CompositionMode_SourceOver); gc.drawImage(viewportRect, m_d->prescaledProjection->prescaledQImage(), viewportRect); } QVariant KisQPainterCanvas::inputMethodQuery(Qt::InputMethodQuery query) const { return processInputMethodQuery(query); } void KisQPainterCanvas::inputMethodEvent(QInputMethodEvent *event) { processInputMethodEvent(event); } void KisQPainterCanvas::channelSelectionChanged(const QBitArray &channelFlags) { Q_ASSERT(m_d->prescaledProjection); m_d->prescaledProjection->setChannelFlags(channelFlags); } void KisQPainterCanvas::setDisplayProfile(KisDisplayColorConverter *colorConverter) { Q_ASSERT(m_d->prescaledProjection); m_d->prescaledProjection->setMonitorProfile(colorConverter->monitorProfile(), colorConverter->renderingIntent(), colorConverter->conversionFlags()); } void KisQPainterCanvas::setDisplayFilter(QSharedPointer displayFilter) { Q_ASSERT(m_d->prescaledProjection); m_d->prescaledProjection->setDisplayFilter(displayFilter); canvas()->startUpdateInPatches(canvas()->image()->bounds()); } void KisQPainterCanvas::setWrapAroundViewingMode(bool value) { Q_UNUSED(value); dbgKrita << "Wrap around viewing mode not implemented in QPainter Canvas."; return; } void KisQPainterCanvas::finishResizingImage(qint32 w, qint32 h) { m_d->prescaledProjection->slotImageSizeChanged(w, h); } KisUpdateInfoSP KisQPainterCanvas::startUpdateCanvasProjection(const QRect & rc, const QBitArray &channelFlags) { Q_UNUSED(channelFlags); return m_d->prescaledProjection->updateCache(rc); } QRect KisQPainterCanvas::updateCanvasProjection(KisUpdateInfoSP info) { /** * It might happen that the canvas type is switched while the * update info is being stuck in the Qt's signals queue. Than a wrong * type of the info may come. So just check it here. */ bool isPPUpdateInfo = dynamic_cast(info.data()); if (isPPUpdateInfo) { m_d->prescaledProjection->recalculateCache(info); return info->dirtyViewportRect(); } else { return QRect(); } } void KisQPainterCanvas::resizeEvent(QResizeEvent *e) { QSize size(e->size()); if (size.width() <= 0) { size.setWidth(1); } if (size.height() <= 0) { size.setHeight(1); } coordinatesConverter()->setCanvasWidgetSize(size); m_d->prescaledProjection->notifyCanvasSizeChanged(size); } void KisQPainterCanvas::slotConfigChanged() { + KisConfig cfg; + m_d->checkBrush = QBrush(createCheckersImage()); + m_d->scrollCheckers = cfg.scrollCheckers(); notifyConfigChanged(); } bool KisQPainterCanvas::callFocusNextPrevChild(bool next) { return focusNextPrevChild(next); } diff --git a/libs/ui/forms/wdgnewimage.ui b/libs/ui/forms/wdgnewimage.ui index 2fad6a6e25..63e3135f2c 100644 --- a/libs/ui/forms/wdgnewimage.ui +++ b/libs/ui/forms/wdgnewimage.ui @@ -1,683 +1,693 @@ WdgNewImage 0 0 600 - 422 + 431 0 0 600 0 16777215 16777215 New Image 0 0 0 0 0 Dimensions 0 140 16777215 16777215 Image Size Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter false false P&redefined: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter cmbPredefined 0 0 Save As: 0 0 Save the current dimensions &Save Landscape ... true true true 2 1.000000000000000 100000000.000000000000000 &Height: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter doubleHeight 0 1.000000000000000 9999.000000000000000 Resolution: 1.000000000000000 100000000.000000000000000 pixels-per-inch ppi W&idth: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter doubleWidth Portrait ... true true true true Qt::Horizontal QSizePolicy::Expanding 191 61 Clipboard 75 75 250 250 QFrame::StyledPanel TextLabel Qt::Vertical 20 40 &Name: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter txtName untitled-1 0 0 Color 0 0 Content Layers: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter Ima&ge Background Opacity: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter sliderOpacity 0 0 1 200 2 &Image Background Color: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter cmbColor Background: Qt::AlignRight|Qt::AlignTop|Qt::AlignTrailing &Description: Qt::AlignRight|Qt::AlignTop|Qt::AlignTrailing txtDescription 0 0 16777215 100 As fi&rst layer As ca&nvas color 0 0 50 0 Reset the image background color in the Image Properties dialog QFrame::NoFrame QFrame::Plain 0 0 0 0 Qt::Vertical 20 40 label_4 lblBackgroundStyle txtDescription lblDescription intNumLayers opacityPanel lblColor cmbColor lblOpacity Qt::Vertical QSizePolicy::Expanding 10 10 + + + + This document... + + + true + + + Qt::Horizontal QSizePolicy::Expanding 480 10 &Create true true KisIntParseSpinBox QSpinBox
kis_int_parse_spin_box.h
KisColorButton QPushButton
kis_color_button.h
KisDoubleSliderSpinBox QWidget
kis_slider_spin_box.h
1
KisColorSpaceSelector QWidget
widgets/kis_color_space_selector.h
1
KisDoubleParseSpinBox QDoubleSpinBox
kis_double_parse_spin_box.h
tabWidget txtName cmbPredefined txtPredefinedName bnSaveAsPredefined doubleWidth doubleHeight doubleResolution cmbWidthUnit cmbHeightUnit bnLandscape bnPortrait createButton intNumLayers cmbColor radioBackgroundAsLayer radioBackgroundAsProjection txtDescription
diff --git a/libs/ui/kis_paintop_box.cc b/libs/ui/kis_paintop_box.cc index 79a7c8a2be..059098a9a5 100644 --- a/libs/ui/kis_paintop_box.cc +++ b/libs/ui/kis_paintop_box.cc @@ -1,1276 +1,1291 @@ /* * kis_paintop_box.cc - part of KImageShop/Krayon/Krita * * Copyright (c) 2004 Boudewijn Rempt (boud@valdyas.org) * Copyright (c) 2009-2011 Sven Langkamp (sven.langkamp@gmail.com) * Copyright (c) 2010 Lukáš Tvrdý * Copyright (C) 2011 Silvio Heinrich * Copyright (C) 2011 Srikanth Tiyyagura * Copyright (c) 2014 Mohit Goyal * * 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_box.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kis_canvas2.h" #include "kis_node_manager.h" #include "KisViewManager.h" #include "kis_canvas_resource_provider.h" #include "kis_resource_server_provider.h" #include "kis_favorite_resource_manager.h" #include "kis_config.h" #include "widgets/kis_popup_button.h" #include "widgets/kis_tool_options_popup.h" #include "widgets/kis_paintop_presets_popup.h" #include "widgets/kis_tool_options_popup.h" #include "widgets/kis_paintop_presets_chooser_popup.h" #include "widgets/kis_workspace_chooser.h" #include "widgets/kis_paintop_list_widget.h" #include "widgets/kis_slider_spin_box.h" #include "widgets/kis_cmb_composite.h" #include "widgets/kis_widget_chooser.h" #include "tool/kis_tool.h" #include "kis_signals_blocker.h" #include "kis_action_manager.h" #include "kis_highlighted_button.h" typedef KoResourceServerSimpleConstruction > KisPaintOpPresetResourceServer; typedef KoResourceServerAdapter > KisPaintOpPresetResourceServerAdapter; KisPaintopBox::KisPaintopBox(KisViewManager *view, QWidget *parent, const char *name) : QWidget(parent) , m_resourceProvider(view->resourceProvider()) , m_optionWidget(0) , m_toolOptionsPopupButton(0) , m_brushEditorPopupButton(0) , m_presetSelectorPopupButton(0) , m_toolOptionsPopup(0) , m_viewManager(view) , m_previousNode(0) , m_currTabletToolID(KoInputDevice::invalid()) , m_presetsEnabled(true) , m_blockUpdate(false) , m_dirtyPresetsEnabled(false) , m_eraserBrushSizeEnabled(false) , m_eraserBrushOpacityEnabled(false) { Q_ASSERT(view != 0); setObjectName(name); KisConfig cfg; m_dirtyPresetsEnabled = cfg.useDirtyPresets(); m_eraserBrushSizeEnabled = cfg.useEraserBrushSize(); m_eraserBrushOpacityEnabled = cfg.useEraserBrushOpacity(); KAcceleratorManager::setNoAccel(this); setWindowTitle(i18n("Painter's Toolchest")); m_favoriteResourceManager = new KisFavoriteResourceManager(this); KConfigGroup grp = KSharedConfig::openConfig()->group("krita").group("Toolbar BrushesAndStuff"); int iconsize = grp.readEntry("IconSize", 32); if (!cfg.toolOptionsInDocker()) { m_toolOptionsPopupButton = new KisPopupButton(this); m_toolOptionsPopupButton->setIcon(KisIconUtils::loadIcon("configure")); m_toolOptionsPopupButton->setToolTip(i18n("Tool Settings")); m_toolOptionsPopupButton->setFixedSize(iconsize, iconsize); } m_brushEditorPopupButton = new KisPopupButton(this); m_brushEditorPopupButton->setIcon(KisIconUtils::loadIcon("paintop_settings_02")); m_brushEditorPopupButton->setToolTip(i18n("Edit brush settings")); m_brushEditorPopupButton->setFixedSize(iconsize, iconsize); m_presetSelectorPopupButton = new KisPopupButton(this); m_presetSelectorPopupButton->setIcon(KisIconUtils::loadIcon("paintop_settings_01")); m_presetSelectorPopupButton->setToolTip(i18n("Choose brush preset")); m_presetSelectorPopupButton->setFixedSize(iconsize, iconsize); m_eraseModeButton = new KisHighlightedToolButton(this); m_eraseModeButton->setFixedSize(iconsize, iconsize); m_eraseModeButton->setCheckable(true); m_eraseAction = m_viewManager->actionManager()->createAction("erase_action"); m_eraseModeButton->setDefaultAction(m_eraseAction); m_reloadButton = new QToolButton(this); m_reloadButton->setFixedSize(iconsize, iconsize); m_reloadAction = m_viewManager->actionManager()->createAction("reload_preset_action"); m_reloadButton->setDefaultAction(m_reloadAction); m_alphaLockButton = new KisHighlightedToolButton(this); m_alphaLockButton->setFixedSize(iconsize, iconsize); m_alphaLockButton->setCheckable(true); KisAction* alphaLockAction = m_viewManager->actionManager()->createAction("preserve_alpha"); m_alphaLockButton->setDefaultAction(alphaLockAction); // pen pressure m_disablePressureButton = new KisHighlightedToolButton(this); m_disablePressureButton->setFixedSize(iconsize, iconsize); m_disablePressureButton->setCheckable(true); m_disablePressureAction = m_viewManager->actionManager()->createAction("disable_pressure"); m_disablePressureButton->setDefaultAction(m_disablePressureAction); // horizontal and vertical mirror toolbar buttons // mirror tool options for the X Mirror QMenu *toolbarMenuXMirror = new QMenu(); hideCanvasDecorationsX = m_viewManager->actionManager()->createAction("mirrorX-hideDecorations"); toolbarMenuXMirror->addAction(hideCanvasDecorationsX); lockActionX = m_viewManager->actionManager()->createAction("mirrorX-lock"); toolbarMenuXMirror->addAction(lockActionX); moveToCenterActionX = m_viewManager->actionManager()->createAction("mirrorX-moveToCenter"); toolbarMenuXMirror->addAction(moveToCenterActionX); // mirror tool options for the Y Mirror QMenu *toolbarMenuYMirror = new QMenu(); hideCanvasDecorationsY = m_viewManager->actionManager()->createAction("mirrorY-hideDecorations"); toolbarMenuYMirror->addAction(hideCanvasDecorationsY); lockActionY = m_viewManager->actionManager()->createAction("mirrorY-lock"); toolbarMenuYMirror->addAction(lockActionY); moveToCenterActionY = m_viewManager->actionManager()->createAction("mirrorY-moveToCenter"); toolbarMenuYMirror->addAction(moveToCenterActionY); // create horizontal and vertical mirror buttons m_hMirrorButton = new KisHighlightedToolButton(this); int menuPadding = 10; m_hMirrorButton->setFixedSize(iconsize + menuPadding, iconsize); m_hMirrorButton->setCheckable(true); m_hMirrorAction = m_viewManager->actionManager()->createAction("hmirror_action"); m_hMirrorButton->setDefaultAction(m_hMirrorAction); m_hMirrorButton->setMenu(toolbarMenuXMirror); m_hMirrorButton->setPopupMode(QToolButton::MenuButtonPopup); m_vMirrorButton = new KisHighlightedToolButton(this); m_vMirrorButton->setFixedSize(iconsize + menuPadding, iconsize); m_vMirrorButton->setCheckable(true); m_vMirrorAction = m_viewManager->actionManager()->createAction("vmirror_action"); m_vMirrorButton->setDefaultAction(m_vMirrorAction); m_vMirrorButton->setMenu(toolbarMenuYMirror); m_vMirrorButton->setPopupMode(QToolButton::MenuButtonPopup); // add connections for horizontal and mirrror buttons connect(lockActionX, SIGNAL(toggled(bool)), this, SLOT(slotLockXMirrorToggle(bool))); connect(lockActionY, SIGNAL(toggled(bool)), this, SLOT(slotLockYMirrorToggle(bool))); connect(moveToCenterActionX, SIGNAL(triggered(bool)), this, SLOT(slotMoveToCenterMirrorX())); connect(moveToCenterActionY, SIGNAL(triggered(bool)), this, SLOT(slotMoveToCenterMirrorY())); connect(hideCanvasDecorationsX, SIGNAL(toggled(bool)), this, SLOT(slotHideDecorationMirrorX(bool))); connect(hideCanvasDecorationsY, SIGNAL(toggled(bool)), this, SLOT(slotHideDecorationMirrorY(bool))); const bool sliderLabels = cfg.sliderLabels(); int sliderWidth; if (sliderLabels) { sliderWidth = 150 * logicalDpiX() / 96; } else { sliderWidth = 120 * logicalDpiX() / 96; } for (int i = 0; i < 3; ++i) { m_sliderChooser[i] = new KisWidgetChooser(i + 1); KisDoubleSliderSpinBox* slOpacity; KisDoubleSliderSpinBox* slFlow; KisDoubleSliderSpinBox* slSize; if (sliderLabels) { slOpacity = m_sliderChooser[i]->addWidget("opacity"); slFlow = m_sliderChooser[i]->addWidget("flow"); slSize = m_sliderChooser[i]->addWidget("size"); slOpacity->setPrefix(QString("%1 ").arg(i18n("Opacity:"))); slFlow->setPrefix(QString("%1 ").arg(i18n("Flow:"))); slSize->setPrefix(QString("%1 ").arg(i18n("Size:"))); } else { slOpacity = m_sliderChooser[i]->addWidget("opacity", i18n("Opacity:")); slFlow = m_sliderChooser[i]->addWidget("flow", i18n("Flow:")); slSize = m_sliderChooser[i]->addWidget("size", i18n("Size:")); } slOpacity->setRange(0.0, 1.0, 2); slOpacity->setValue(1.0); slOpacity->setSingleStep(0.05); slOpacity->setMinimumWidth(qMax(sliderWidth, slOpacity->sizeHint().width())); slOpacity->setFixedHeight(iconsize); slOpacity->setBlockUpdateSignalOnDrag(true); slFlow->setRange(0.0, 1.0, 2); slFlow->setValue(1.0); slFlow->setSingleStep(0.05); slFlow->setMinimumWidth(qMax(sliderWidth, slFlow->sizeHint().width())); slFlow->setFixedHeight(iconsize); slFlow->setBlockUpdateSignalOnDrag(true); slSize->setRange(0, cfg.readEntry("maximumBrushSize", 1000), 2); slSize->setValue(100); slSize->setSingleStep(1); slSize->setExponentRatio(3.0); slSize->setSuffix(i18n(" px")); slSize->setMinimumWidth(qMax(sliderWidth, slSize->sizeHint().width())); slSize->setFixedHeight(iconsize); slSize->setBlockUpdateSignalOnDrag(true); m_sliderChooser[i]->chooseWidget(cfg.toolbarSlider(i + 1)); } m_cmbCompositeOp = new KisCompositeOpComboBox(); m_cmbCompositeOp->setFixedHeight(iconsize); Q_FOREACH (KisAction * a, m_cmbCompositeOp->blendmodeActions()) { m_viewManager->actionManager()->addAction(a->text(), a); } m_workspaceWidget = new KisPopupButton(this); m_workspaceWidget->setIcon(KisIconUtils::loadIcon("view-choose")); m_workspaceWidget->setToolTip(i18n("Choose workspace")); m_workspaceWidget->setFixedSize(iconsize, iconsize); m_workspaceWidget->setPopupWidget(new KisWorkspaceChooser(view)); QHBoxLayout* baseLayout = new QHBoxLayout(this); m_paintopWidget = new QWidget(this); baseLayout->addWidget(m_paintopWidget); baseLayout->setSpacing(4); baseLayout->setContentsMargins(0, 0, 0, 0); m_layout = new QHBoxLayout(m_paintopWidget); if (!cfg.toolOptionsInDocker()) { m_layout->addWidget(m_toolOptionsPopupButton); } m_layout->addWidget(m_brushEditorPopupButton); m_layout->addWidget(m_presetSelectorPopupButton); m_layout->setSpacing(4); m_layout->setContentsMargins(0, 0, 0, 0); QWidget* compositeActions = new QWidget(this); QHBoxLayout* compositeLayout = new QHBoxLayout(compositeActions); compositeLayout->addWidget(m_cmbCompositeOp); compositeLayout->addWidget(m_eraseModeButton); compositeLayout->addWidget(m_alphaLockButton); compositeLayout->setSpacing(4); compositeLayout->setContentsMargins(0, 0, 0, 0); compositeLayout->addWidget(m_reloadButton); QWidgetAction * action; action = new QWidgetAction(this); view->actionCollection()->addAction("composite_actions", action); action->setText(i18n("Brush composite")); action->setDefaultWidget(compositeActions); QWidget* compositePressure = new QWidget(this); QHBoxLayout* pressureLayout = new QHBoxLayout(compositePressure); pressureLayout->addWidget(m_disablePressureButton); pressureLayout->setSpacing(4); pressureLayout->setContentsMargins(0, 0, 0, 0); action = new QWidgetAction(this); view->actionCollection()->addAction("pressure_action", action); action->setText(i18n("Pressure usage (small button)")); action->setDefaultWidget(compositePressure); action = new QWidgetAction(this); KisActionRegistry::instance()->propertizeAction("brushslider1", action); view->actionCollection()->addAction("brushslider1", action); action->setDefaultWidget(m_sliderChooser[0]); connect(action, SIGNAL(triggered()), m_sliderChooser[0], SLOT(showPopupWidget())); connect(m_viewManager->mainWindow(), SIGNAL(themeChanged()), m_sliderChooser[0], SLOT(updateThemedIcons())); action = new QWidgetAction(this); KisActionRegistry::instance()->propertizeAction("brushslider2", action); view->actionCollection()->addAction("brushslider2", action); action->setDefaultWidget(m_sliderChooser[1]); connect(action, SIGNAL(triggered()), m_sliderChooser[1], SLOT(showPopupWidget())); connect(m_viewManager->mainWindow(), SIGNAL(themeChanged()), m_sliderChooser[1], SLOT(updateThemedIcons())); action = new QWidgetAction(this); KisActionRegistry::instance()->propertizeAction("brushslider3", action); view->actionCollection()->addAction("brushslider3", action); action->setDefaultWidget(m_sliderChooser[2]); connect(action, SIGNAL(triggered()), m_sliderChooser[2], SLOT(showPopupWidget())); connect(m_viewManager->mainWindow(), SIGNAL(themeChanged()), m_sliderChooser[2], SLOT(updateThemedIcons())); action = new QWidgetAction(this); KisActionRegistry::instance()->propertizeAction("next_favorite_preset", action); view->actionCollection()->addAction("next_favorite_preset", action); connect(action, SIGNAL(triggered()), this, SLOT(slotNextFavoritePreset())); action = new QWidgetAction(this); KisActionRegistry::instance()->propertizeAction("previous_favorite_preset", action); view->actionCollection()->addAction("previous_favorite_preset", action); connect(action, SIGNAL(triggered()), this, SLOT(slotPreviousFavoritePreset())); action = new QWidgetAction(this); KisActionRegistry::instance()->propertizeAction("previous_preset", action); view->actionCollection()->addAction("previous_preset", action); connect(action, SIGNAL(triggered()), this, SLOT(slotSwitchToPreviousPreset())); if (!cfg.toolOptionsInDocker()) { action = new QWidgetAction(this); KisActionRegistry::instance()->propertizeAction("show_tool_options", action); view->actionCollection()->addAction("show_tool_options", action); connect(action, SIGNAL(triggered()), m_toolOptionsPopupButton, SLOT(showPopupWidget())); } action = new QWidgetAction(this); KisActionRegistry::instance()->propertizeAction("show_brush_editor", action); view->actionCollection()->addAction("show_brush_editor", action); connect(action, SIGNAL(triggered()), m_brushEditorPopupButton, SLOT(showPopupWidget())); action = new QWidgetAction(this); KisActionRegistry::instance()->propertizeAction("show_brush_presets", action); view->actionCollection()->addAction("show_brush_presets", action); connect(action, SIGNAL(triggered()), m_presetSelectorPopupButton, SLOT(showPopupWidget())); QWidget* mirrorActions = new QWidget(this); QHBoxLayout* mirrorLayout = new QHBoxLayout(mirrorActions); mirrorLayout->addWidget(m_hMirrorButton); mirrorLayout->addWidget(m_vMirrorButton); mirrorLayout->setSpacing(4); mirrorLayout->setContentsMargins(0, 0, 0, 0); action = new QWidgetAction(this); KisActionRegistry::instance()->propertizeAction("mirror_actions", action); action->setDefaultWidget(mirrorActions); view->actionCollection()->addAction("mirror_actions", action); action = new QWidgetAction(this); KisActionRegistry::instance()->propertizeAction("workspaces", action); view->actionCollection()->addAction("workspaces", action); action->setDefaultWidget(m_workspaceWidget); if (!cfg.toolOptionsInDocker()) { m_toolOptionsPopup = new KisToolOptionsPopup(); m_toolOptionsPopupButton->setPopupWidget(m_toolOptionsPopup); m_toolOptionsPopup->switchDetached(false); } m_savePresetWidget = new KisPresetSaveWidget(this); m_presetsPopup = new KisPaintOpPresetsPopup(m_resourceProvider, m_favoriteResourceManager, m_savePresetWidget); m_brushEditorPopupButton->setPopupWidget(m_presetsPopup); m_presetsPopup->parentWidget()->setWindowTitle(i18n("Brush Editor")); connect(m_presetsPopup, SIGNAL(brushEditorShown()), SLOT(slotUpdateOptionsWidgetPopup())); connect(m_viewManager->mainWindow(), SIGNAL(themeChanged()), m_presetsPopup, SLOT(updateThemedIcons())); m_presetsChooserPopup = new KisPaintOpPresetsChooserPopup(); m_presetSelectorPopupButton->setPopupWidget(m_presetsChooserPopup); m_currCompositeOpID = KoCompositeOpRegistry::instance().getDefaultCompositeOp().id(); slotNodeChanged(view->activeNode()); // Get all the paintops QList keys = KisPaintOpRegistry::instance()->keys(); QList factoryList; Q_FOREACH (const QString & paintopId, keys) { factoryList.append(KisPaintOpRegistry::instance()->get(paintopId)); } m_presetsPopup->setPaintOpList(factoryList); connect(m_presetsPopup , SIGNAL(paintopActivated(QString)) , SLOT(slotSetPaintop(QString))); connect(m_presetsPopup , SIGNAL(defaultPresetClicked()) , SLOT(slotSetupDefaultPreset())); connect(m_presetsPopup , SIGNAL(signalResourceSelected(KoResource*)), SLOT(resourceSelected(KoResource*))); connect(m_presetsPopup , SIGNAL(reloadPresetClicked()) , SLOT(slotReloadPreset())); connect(m_presetsPopup , SIGNAL(dirtyPresetToggled(bool)) , SLOT(slotDirtyPresetToggled(bool))); connect(m_presetsPopup , SIGNAL(eraserBrushSizeToggled(bool)) , SLOT(slotEraserBrushSizeToggled(bool))); connect(m_presetsPopup , SIGNAL(eraserBrushOpacityToggled(bool)) , SLOT(slotEraserBrushOpacityToggled(bool))); connect(m_presetsChooserPopup, SIGNAL(resourceSelected(KoResource*)) , SLOT(resourceSelected(KoResource*))); connect(m_presetsChooserPopup, SIGNAL(resourceClicked(KoResource*)) , SLOT(resourceSelected(KoResource*))); connect(m_resourceProvider , SIGNAL(sigNodeChanged(const KisNodeSP)) , SLOT(slotNodeChanged(const KisNodeSP))); connect(m_cmbCompositeOp , SIGNAL(currentIndexChanged(int)) , SLOT(slotSetCompositeMode(int))); connect(m_eraseAction , SIGNAL(toggled(bool)) , SLOT(slotToggleEraseMode(bool))); connect(alphaLockAction , SIGNAL(toggled(bool)) , SLOT(slotToggleAlphaLockMode(bool))); connect(m_disablePressureAction , SIGNAL(toggled(bool)) , SLOT(slotDisablePressureMode(bool))); m_disablePressureAction->setChecked(true); connect(m_hMirrorAction , SIGNAL(toggled(bool)) , SLOT(slotHorizontalMirrorChanged(bool))); connect(m_vMirrorAction , SIGNAL(toggled(bool)) , SLOT(slotVerticalMirrorChanged(bool))); connect(m_reloadAction , SIGNAL(triggered()) , SLOT(slotReloadPreset())); connect(m_sliderChooser[0]->getWidget("opacity"), SIGNAL(valueChanged(qreal)), SLOT(slotSlider1Changed())); connect(m_sliderChooser[0]->getWidget("flow") , SIGNAL(valueChanged(qreal)), SLOT(slotSlider1Changed())); connect(m_sliderChooser[0]->getWidget("size") , SIGNAL(valueChanged(qreal)), SLOT(slotSlider1Changed())); connect(m_sliderChooser[1]->getWidget("opacity"), SIGNAL(valueChanged(qreal)), SLOT(slotSlider2Changed())); connect(m_sliderChooser[1]->getWidget("flow") , SIGNAL(valueChanged(qreal)), SLOT(slotSlider2Changed())); connect(m_sliderChooser[1]->getWidget("size") , SIGNAL(valueChanged(qreal)), SLOT(slotSlider2Changed())); connect(m_sliderChooser[2]->getWidget("opacity"), SIGNAL(valueChanged(qreal)), SLOT(slotSlider3Changed())); connect(m_sliderChooser[2]->getWidget("flow") , SIGNAL(valueChanged(qreal)), SLOT(slotSlider3Changed())); connect(m_sliderChooser[2]->getWidget("size") , SIGNAL(valueChanged(qreal)), SLOT(slotSlider3Changed())); //Needed to connect canvas to favorite resource manager connect(m_viewManager->resourceProvider(), SIGNAL(sigFGColorChanged(KoColor)), SLOT(slotUnsetEraseMode())); connect(m_resourceProvider, SIGNAL(sigFGColorUsed(KoColor)), m_favoriteResourceManager, SLOT(slotAddRecentColor(KoColor))); connect(m_resourceProvider, SIGNAL(sigFGColorChanged(KoColor)), m_favoriteResourceManager, SLOT(slotChangeFGColorSelector(KoColor))); connect(m_resourceProvider, SIGNAL(sigBGColorChanged(KoColor)), m_favoriteResourceManager, SLOT(slotSetBGColor(KoColor))); // cold initialization m_favoriteResourceManager->slotChangeFGColorSelector(m_resourceProvider->fgColor()); m_favoriteResourceManager->slotSetBGColor(m_resourceProvider->bgColor()); connect(m_favoriteResourceManager, SIGNAL(sigSetFGColor(KoColor)), m_resourceProvider, SLOT(slotSetFGColor(KoColor))); connect(m_favoriteResourceManager, SIGNAL(sigSetBGColor(KoColor)), m_resourceProvider, SLOT(slotSetBGColor(KoColor))); connect(m_favoriteResourceManager, SIGNAL(sigEnableChangeColor(bool)), m_resourceProvider, SLOT(slotResetEnableFGChange(bool))); connect(view->mainWindow(), SIGNAL(themeChanged()), this, SLOT(slotUpdateSelectionIcon())); slotInputDeviceChanged(KoToolManager::instance()->currentInputDevice()); } KisPaintopBox::~KisPaintopBox() { KisConfig cfg; QMapIterator iter(m_tabletToolMap); while (iter.hasNext()) { iter.next(); //qDebug() << "Writing last used preset for" << iter.key().pointer << iter.key().uniqueID << iter.value().preset->name(); if ((iter.key().pointer) == QTabletEvent::Eraser) { cfg.writeEntry(QString("LastEraser_%1").arg(iter.key().uniqueID) , iter.value().preset->name()); } else { cfg.writeEntry(QString("LastPreset_%1").arg(iter.key().uniqueID) , iter.value().preset->name()); } } // Do not delete the widget, since it it is global to the application, not owned by the view m_presetsPopup->setPaintOpSettingsWidget(0); qDeleteAll(m_paintopOptionWidgets); delete m_favoriteResourceManager; for (int i = 0; i < 3; ++i) { delete m_sliderChooser[i]; } } void KisPaintopBox::restoreResource(KoResource* resource) { KisPaintOpPreset* preset = dynamic_cast(resource); //qDebug() << "restoreResource" << resource << preset; if (preset) { setCurrentPaintop(preset); m_presetsPopup->setPresetImage(preset->image()); m_presetsPopup->resourceSelected(resource); } } void KisPaintopBox::newOptionWidgets(const QList > &optionWidgetList) { if (m_toolOptionsPopup) { m_toolOptionsPopup->newOptionWidgets(optionWidgetList); } } void KisPaintopBox::resourceSelected(KoResource* resource) { KisPaintOpPreset* preset = dynamic_cast(resource); if (preset && preset != m_resourceProvider->currentPreset()) { if (!preset->settings()->isLoadable()) return; if (!m_dirtyPresetsEnabled) { KisSignalsBlocker blocker(m_optionWidget); if (!preset->load()) { warnKrita << "failed to load the preset."; } } //qDebug() << "resourceSelected" << resource->name(); setCurrentPaintop(preset); m_presetsPopup->setPresetImage(preset->image()); m_presetsPopup->resourceSelected(resource); } } void KisPaintopBox::setCurrentPaintop(const KoID& paintop) { KisPaintOpPresetSP preset = activePreset(paintop); Q_ASSERT(preset && preset->settings()); //qDebug() << "setCurrentPaintop();" << paintop << preset; setCurrentPaintop(preset); } void KisPaintopBox::setCurrentPaintop(KisPaintOpPresetSP preset) { //qDebug() << "setCurrentPaintop(); " << preset->name(); if (preset == m_resourceProvider->currentPreset()) { if (preset == m_tabletToolMap[m_currTabletToolID].preset) { return; } } Q_ASSERT(preset); const KoID& paintop = preset->paintOp(); m_presetConnections.clear(); if (m_resourceProvider->currentPreset()) { m_resourceProvider->setPreviousPaintOpPreset(m_resourceProvider->currentPreset()); if (m_optionWidget) { m_optionWidget->hide(); } } if (!m_paintopOptionWidgets.contains(paintop)) m_paintopOptionWidgets[paintop] = KisPaintOpRegistry::instance()->get(paintop.id())->createConfigWidget(this); m_optionWidget = m_paintopOptionWidgets[paintop]; KisSignalsBlocker b(m_optionWidget); preset->setOptionsWidget(m_optionWidget); m_optionWidget->setImage(m_viewManager->image()); m_optionWidget->setNode(m_viewManager->activeNode()); m_presetsPopup->setPaintOpSettingsWidget(m_optionWidget); m_resourceProvider->setPaintOpPreset(preset); Q_ASSERT(m_optionWidget && m_presetSelectorPopupButton); m_presetConnections.addConnection(m_optionWidget, SIGNAL(sigConfigurationUpdated()), this, SLOT(slotGuiChangedCurrentPreset())); m_presetConnections.addConnection(m_optionWidget, SIGNAL(sigSaveLockedConfig(KisPropertiesConfigurationSP)), this, SLOT(slotSaveLockedOptionToPreset(KisPropertiesConfigurationSP))); m_presetConnections.addConnection(m_optionWidget, SIGNAL(sigDropLockedConfig(KisPropertiesConfigurationSP)), this, SLOT(slotDropLockedOption(KisPropertiesConfigurationSP))); // load the current brush engine icon for the brush editor toolbar button KisPaintOpFactory* paintOp = KisPaintOpRegistry::instance()->get(paintop.id()); QString pixFilename = KoResourcePaths::findResource("kis_images", paintOp->pixmap()); m_brushEditorPopupButton->setIcon(QIcon(pixFilename)); m_presetsPopup->setCurrentPaintOpId(paintop.id()); ////qDebug() << "\tsetting the new preset for" << m_currTabletToolID.uniqueID << "to" << preset->name(); m_paintOpPresetMap[m_resourceProvider->currentPreset()->paintOp()] = preset; m_tabletToolMap[m_currTabletToolID].preset = preset; m_tabletToolMap[m_currTabletToolID].paintOpID = preset->paintOp(); if (m_presetsPopup->currentPaintOpId() != paintop.id()) { // Must change the paintop as the current one is not supported // by the new colorspace. dbgKrita << "current paintop " << paintop.name() << " was not set, not supported by colorspace"; } } void KisPaintopBox::slotUpdateOptionsWidgetPopup() { KisPaintOpPresetSP preset = m_resourceProvider->currentPreset(); KIS_SAFE_ASSERT_RECOVER_RETURN(preset); KIS_SAFE_ASSERT_RECOVER_RETURN(m_optionWidget); m_optionWidget->setConfigurationSafe(preset->settings()); m_presetsPopup->resourceSelected(preset.data()); m_presetsPopup->updateViewSettings(); // the m_viewManager->image() is set earlier, but the reference will be missing when the stamp button is pressed // need to later do some research on how and when we should be using weak shared pointers (WSP) that creates this situation m_optionWidget->setImage(m_viewManager->image()); } KisPaintOpPresetSP KisPaintopBox::defaultPreset(const KoID& paintOp) { QString defaultName = paintOp.id() + ".kpp"; QString path = KoResourcePaths::findResource("kis_defaultpresets", defaultName); KisPaintOpPresetSP preset = new KisPaintOpPreset(path); if (!preset->load()) { preset = KisPaintOpRegistry::instance()->defaultPreset(paintOp); } Q_ASSERT(preset); Q_ASSERT(preset->valid()); return preset; } KisPaintOpPresetSP KisPaintopBox::activePreset(const KoID& paintOp) { if (m_paintOpPresetMap[paintOp] == 0) { m_paintOpPresetMap[paintOp] = defaultPreset(paintOp); } return m_paintOpPresetMap[paintOp]; } void KisPaintopBox::updateCompositeOp(QString compositeOpID) { if (!m_optionWidget) return; KisSignalsBlocker blocker(m_optionWidget); KisNodeSP node = m_resourceProvider->currentNode(); if (node && node->paintDevice()) { if (!node->paintDevice()->colorSpace()->hasCompositeOp(compositeOpID)) compositeOpID = KoCompositeOpRegistry::instance().getDefaultCompositeOp().id(); { KisSignalsBlocker b1(m_cmbCompositeOp); m_cmbCompositeOp->selectCompositeOp(KoID(compositeOpID)); } if (compositeOpID != m_currCompositeOpID) { m_currCompositeOpID = compositeOpID; } if (compositeOpID == COMPOSITE_ERASE) { m_eraseModeButton->setChecked(true); } else { m_eraseModeButton->setChecked(false); } } } void KisPaintopBox::setWidgetState(int flags) { if (flags & (ENABLE_COMPOSITEOP | DISABLE_COMPOSITEOP)) { m_cmbCompositeOp->setEnabled(flags & ENABLE_COMPOSITEOP); m_eraseModeButton->setEnabled(flags & ENABLE_COMPOSITEOP); } if (flags & (ENABLE_PRESETS | DISABLE_PRESETS)) { m_presetSelectorPopupButton->setEnabled(flags & ENABLE_PRESETS); m_brushEditorPopupButton->setEnabled(flags & ENABLE_PRESETS); } for (int i = 0; i < 3; ++i) { if (flags & (ENABLE_OPACITY | DISABLE_OPACITY)) m_sliderChooser[i]->getWidget("opacity")->setEnabled(flags & ENABLE_OPACITY); if (flags & (ENABLE_FLOW | DISABLE_FLOW)) m_sliderChooser[i]->getWidget("flow")->setEnabled(flags & ENABLE_FLOW); if (flags & (ENABLE_SIZE | DISABLE_SIZE)) m_sliderChooser[i]->getWidget("size")->setEnabled(flags & ENABLE_SIZE); } } void KisPaintopBox::setSliderValue(const QString& sliderID, qreal value) { for (int i = 0; i < 3; ++i) { KisDoubleSliderSpinBox* slider = m_sliderChooser[i]->getWidget(sliderID); KisSignalsBlocker b(slider); slider->setValue(value); } } void KisPaintopBox::slotSetPaintop(const QString& paintOpId) { if (KisPaintOpRegistry::instance()->get(paintOpId) != 0) { KoID id(paintOpId, KisPaintOpRegistry::instance()->get(paintOpId)->name()); //qDebug() << "slotsetpaintop" << id; setCurrentPaintop(id); } } void KisPaintopBox::slotInputDeviceChanged(const KoInputDevice& inputDevice) { TabletToolMap::iterator toolData = m_tabletToolMap.find(inputDevice); //qDebug() << "slotInputDeviceChanged()" << inputDevice.device() << inputDevice.uniqueTabletId(); m_currTabletToolID = TabletToolID(inputDevice); if (toolData == m_tabletToolMap.end()) { KisConfig cfg; KisPaintOpPresetResourceServer *rserver = KisResourceServerProvider::instance()->paintOpPresetServer(false); KisPaintOpPresetSP preset; if (inputDevice.pointer() == QTabletEvent::Eraser) { preset = rserver->resourceByName(cfg.readEntry(QString("LastEraser_%1").arg(inputDevice.uniqueTabletId()), "Eraser_circle")); } else { preset = rserver->resourceByName(cfg.readEntry(QString("LastPreset_%1").arg(inputDevice.uniqueTabletId()), "Basic_tip_default")); //if (preset) //qDebug() << "found stored preset " << preset->name() << "for" << inputDevice.uniqueTabletId(); //else //qDebug() << "no preset fcound for" << inputDevice.uniqueTabletId(); } if (!preset) { preset = rserver->resourceByName("Basic_tip_default"); } if (preset) { //qDebug() << "inputdevicechanged 1" << preset; setCurrentPaintop(preset); } } else { if (toolData->preset) { //qDebug() << "inputdevicechanged 2" << toolData->preset; setCurrentPaintop(toolData->preset); } else { //qDebug() << "inputdevicechanged 3" << toolData->paintOpID; setCurrentPaintop(toolData->paintOpID); } } } void KisPaintopBox::slotCanvasResourceChanged(int key, const QVariant &value) { if (m_viewManager) { sender()->blockSignals(true); KisPaintOpPresetSP preset = m_viewManager->resourceProvider()->resourceManager()->resource(KisCanvasResourceProvider::CurrentPaintOpPreset).value(); if (preset && m_resourceProvider->currentPreset()->name() != preset->name()) { QString compositeOp = preset->settings()->getString("CompositeOp"); updateCompositeOp(compositeOp); resourceSelected(preset.data()); } /** * Update currently selected preset in both the popup widgets */ m_presetsChooserPopup->canvasResourceChanged(preset); m_presetsPopup->currentPresetChanged(preset); if (key == KisCanvasResourceProvider::CurrentCompositeOp) { if (m_resourceProvider->currentCompositeOp() != m_currCompositeOpID) { updateCompositeOp(m_resourceProvider->currentCompositeOp()); } } if (key == KisCanvasResourceProvider::Size) { setSliderValue("size", m_resourceProvider->size()); } if (key == KisCanvasResourceProvider::Opacity) { setSliderValue("opacity", m_resourceProvider->opacity()); } if (key == KisCanvasResourceProvider::Flow) { setSliderValue("flow", m_resourceProvider->flow()); } if (key == KisCanvasResourceProvider::EraserMode) { m_eraseAction->setChecked(value.toBool()); } if (key == KisCanvasResourceProvider::DisablePressure) { m_disablePressureAction->setChecked(value.toBool()); } sender()->blockSignals(false); } } void KisPaintopBox::slotUpdatePreset() { if (!m_resourceProvider->currentPreset()) return; // block updates of avoid some over updating of the option widget m_blockUpdate = true; setSliderValue("size", m_resourceProvider->size()); { qreal opacity = m_resourceProvider->currentPreset()->settings()->paintOpOpacity(); m_resourceProvider->setOpacity(opacity); setSliderValue("opacity", opacity); setWidgetState(ENABLE_OPACITY); } { setSliderValue("flow", m_resourceProvider->currentPreset()->settings()->paintOpFlow()); setWidgetState(ENABLE_FLOW); } { updateCompositeOp(m_resourceProvider->currentPreset()->settings()->paintOpCompositeOp()); setWidgetState(ENABLE_COMPOSITEOP); } m_blockUpdate = false; } void KisPaintopBox::slotSetupDefaultPreset() { KisPaintOpPresetSP preset = defaultPreset(m_resourceProvider->currentPreset()->paintOp()); preset->setOptionsWidget(m_optionWidget); m_resourceProvider->setPaintOpPreset(preset); } void KisPaintopBox::slotNodeChanged(const KisNodeSP node) { if (m_previousNode.isValid() && m_previousNode->paintDevice()) disconnect(m_previousNode->paintDevice().data(), SIGNAL(colorSpaceChanged(const KoColorSpace*)), this, SLOT(slotColorSpaceChanged(const KoColorSpace*))); // Reconnect colorspace change of node if (node && node->paintDevice()) { connect(node->paintDevice().data(), SIGNAL(colorSpaceChanged(const KoColorSpace*)), this, SLOT(slotColorSpaceChanged(const KoColorSpace*))); m_resourceProvider->setCurrentCompositeOp(m_currCompositeOpID); m_previousNode = node; slotColorSpaceChanged(node->colorSpace()); } if (m_optionWidget) { m_optionWidget->setNode(node); } } void KisPaintopBox::slotColorSpaceChanged(const KoColorSpace* colorSpace) { m_cmbCompositeOp->validate(colorSpace); } void KisPaintopBox::slotToggleEraseMode(bool checked) { const bool oldEraserMode = m_resourceProvider->eraserMode(); m_resourceProvider->setEraserMode(checked); if (oldEraserMode != checked && m_eraserBrushSizeEnabled) { const qreal currentSize = m_resourceProvider->size(); KisPaintOpSettingsSP settings = m_resourceProvider->currentPreset()->settings(); // remember brush size. set the eraser size to the normal brush size if not set if (checked) { settings->setSavedBrushSize(currentSize); if (qFuzzyIsNull(settings->savedEraserSize())) { settings->setSavedEraserSize(currentSize); } } else { settings->setSavedEraserSize(currentSize); if (qFuzzyIsNull(settings->savedBrushSize())) { settings->setSavedBrushSize(currentSize); } } //update value in UI (this is the main place the value is 'stored' in memory) qreal newSize = checked ? settings->savedEraserSize() : settings->savedBrushSize(); m_resourceProvider->setSize(newSize); } if (oldEraserMode != checked && m_eraserBrushOpacityEnabled) { const qreal currentOpacity = m_resourceProvider->opacity(); KisPaintOpSettingsSP settings = m_resourceProvider->currentPreset()->settings(); // remember brush opacity. set the eraser opacity to the normal brush opacity if not set if (checked) { settings->setSavedBrushOpacity(currentOpacity); if (qFuzzyIsNull(settings->savedEraserOpacity())) { settings->setSavedEraserOpacity(currentOpacity); } } else { settings->setSavedEraserOpacity(currentOpacity); if (qFuzzyIsNull(settings->savedBrushOpacity())) { settings->setSavedBrushOpacity(currentOpacity); } } //update value in UI (this is the main place the value is 'stored' in memory) qreal newOpacity = checked ? settings->savedEraserOpacity() : settings->savedBrushOpacity(); m_resourceProvider->setOpacity(newOpacity); } } void KisPaintopBox::slotSetCompositeMode(int index) { Q_UNUSED(index); QString compositeOp = m_cmbCompositeOp->selectedCompositeOp().id(); m_resourceProvider->setCurrentCompositeOp(compositeOp); } void KisPaintopBox::slotHorizontalMirrorChanged(bool value) { m_resourceProvider->setMirrorHorizontal(value); } void KisPaintopBox::slotVerticalMirrorChanged(bool value) { m_resourceProvider->setMirrorVertical(value); } void KisPaintopBox::sliderChanged(int n) { if (!m_optionWidget) // widget will not exist if the are no documents open return; KisSignalsBlocker blocker(m_optionWidget); qreal opacity = m_sliderChooser[n]->getWidget("opacity")->value(); qreal flow = m_sliderChooser[n]->getWidget("flow")->value(); qreal size = m_sliderChooser[n]->getWidget("size")->value(); setSliderValue("opacity", opacity); setSliderValue("flow" , flow); setSliderValue("size" , size); if (m_presetsEnabled) { // IMPORTANT: set the PaintOp size before setting the other properties // it wont work the other way // TODO: why?! m_resourceProvider->setSize(size); m_resourceProvider->setOpacity(opacity); m_resourceProvider->setFlow(flow); KisLockedPropertiesProxySP propertiesProxy = KisLockedPropertiesServer::instance()->createLockedPropertiesProxy(m_resourceProvider->currentPreset()->settings()); propertiesProxy->setProperty("OpacityValue", opacity); propertiesProxy->setProperty("FlowValue", flow); m_optionWidget->setConfigurationSafe(m_resourceProvider->currentPreset()->settings().data()); } else { m_resourceProvider->setOpacity(opacity); } m_presetsPopup->resourceSelected(m_resourceProvider->currentPreset().data()); } void KisPaintopBox::slotSlider1Changed() { sliderChanged(0); } void KisPaintopBox::slotSlider2Changed() { sliderChanged(1); } void KisPaintopBox::slotSlider3Changed() { sliderChanged(2); } void KisPaintopBox::slotToolChanged(KoCanvasController* canvas, int toolId) { Q_UNUSED(canvas); Q_UNUSED(toolId); if (!m_viewManager->canvasBase()) return; QString id = KoToolManager::instance()->activeToolId(); KisTool* tool = dynamic_cast(KoToolManager::instance()->toolById(m_viewManager->canvasBase(), id)); if (tool) { int flags = tool->flags(); if (flags & KisTool::FLAG_USES_CUSTOM_COMPOSITEOP) { setWidgetState(ENABLE_COMPOSITEOP | ENABLE_OPACITY); } else { setWidgetState(DISABLE_COMPOSITEOP | DISABLE_OPACITY); } if (flags & KisTool::FLAG_USES_CUSTOM_PRESET) { setWidgetState(ENABLE_PRESETS); slotUpdatePreset(); m_presetsEnabled = true; } else { setWidgetState(DISABLE_PRESETS); m_presetsEnabled = false; } if (flags & KisTool::FLAG_USES_CUSTOM_SIZE) { setWidgetState(ENABLE_SIZE | ENABLE_FLOW); } else { setWidgetState(DISABLE_SIZE | DISABLE_FLOW); } } else setWidgetState(DISABLE_ALL); } void KisPaintopBox::slotPreviousFavoritePreset() { if (!m_favoriteResourceManager) return; - int i = 0; - Q_FOREACH (KisPaintOpPresetSP preset, m_favoriteResourceManager->favoritePresetList()) { - if (m_resourceProvider->currentPreset() && m_resourceProvider->currentPreset()->name() == preset->name()) { + QVector presets = m_favoriteResourceManager->favoritePresetList(); + for (int i=0; i < presets.size(); ++i) { + if (m_resourceProvider->currentPreset() && + m_resourceProvider->currentPreset()->name() == presets[i]->name()) { if (i > 0) { m_favoriteResourceManager->slotChangeActivePaintop(i - 1); } else { m_favoriteResourceManager->slotChangeActivePaintop(m_favoriteResourceManager->numFavoritePresets() - 1); } + //floating message should have least 2 lines, otherwise + //preset thumbnail will be too small to distinguish + //(because size of image on floating message depends on amount of lines in msg) + m_viewManager->showFloatingMessage( + i18n("%1\nselected", + m_resourceProvider->currentPreset()->name()), + QIcon(QPixmap::fromImage(m_resourceProvider->currentPreset()->image()))); + return; } - i++; } - } void KisPaintopBox::slotNextFavoritePreset() { if (!m_favoriteResourceManager) return; - int i = 0; - Q_FOREACH (KisPaintOpPresetSP preset, m_favoriteResourceManager->favoritePresetList()) { - if (m_resourceProvider->currentPreset()->name() == preset->name()) { + QVector presets = m_favoriteResourceManager->favoritePresetList(); + for(int i = 0; i < presets.size(); ++i) { + if (m_resourceProvider->currentPreset()->name() == presets[i]->name()) { if (i < m_favoriteResourceManager->numFavoritePresets() - 1) { m_favoriteResourceManager->slotChangeActivePaintop(i + 1); } else { m_favoriteResourceManager->slotChangeActivePaintop(0); } + m_viewManager->showFloatingMessage( + i18n("%1\nselected", + m_resourceProvider->currentPreset()->name()), + QIcon(QPixmap::fromImage(m_resourceProvider->currentPreset()->image()))); + return; } - i++; } } void KisPaintopBox::slotSwitchToPreviousPreset() { if (m_resourceProvider->previousPreset()) { //qDebug() << "slotSwitchToPreviousPreset();" << m_resourceProvider->previousPreset(); setCurrentPaintop(m_resourceProvider->previousPreset()); + m_viewManager->showFloatingMessage( + i18n("%1\nselected", + m_resourceProvider->currentPreset()->name()), + QIcon(QPixmap::fromImage(m_resourceProvider->currentPreset()->image()))); } } void KisPaintopBox::slotUnsetEraseMode() { m_eraseAction->setChecked(false); } void KisPaintopBox::slotToggleAlphaLockMode(bool checked) { if (checked) { m_alphaLockButton->actions()[0]->setIcon(KisIconUtils::loadIcon("transparency-locked")); } else { m_alphaLockButton->actions()[0]->setIcon(KisIconUtils::loadIcon("transparency-unlocked")); } m_resourceProvider->setGlobalAlphaLock(checked); } void KisPaintopBox::slotDisablePressureMode(bool checked) { if (checked) { m_disablePressureButton->actions()[0]->setIcon(KisIconUtils::loadIcon("transform_icons_penPressure")); } else { m_disablePressureButton->actions()[0]->setIcon(KisIconUtils::loadIcon("transform_icons_penPressure_locked")); } m_resourceProvider->setDisablePressure(checked); } void KisPaintopBox::slotReloadPreset() { KisSignalsBlocker blocker(m_optionWidget); //Here using the name and fetching the preset from the server was the only way the load was working. Otherwise it was not loading. KisPaintOpPresetResourceServer * rserver = KisResourceServerProvider::instance()->paintOpPresetServer(); KisPaintOpPresetSP preset = rserver->resourceByName(m_resourceProvider->currentPreset()->name()); if (preset) { preset->load(); } } void KisPaintopBox::slotGuiChangedCurrentPreset() // Called only when UI is changed and not when preset is changed { KisPaintOpPresetSP preset = m_resourceProvider->currentPreset(); { /** * Here we postpone all the settings updates events until thye entire writing * operation will be finished. As soon as it is finished, the updates will be * emitted happily (if there were any). */ KisPaintOpPreset::UpdatedPostponer postponer(preset.data()); m_optionWidget->writeConfigurationSafe(const_cast(preset->settings().data())); } // we should also update the preset strip to update the status of the "dirty" mark m_presetsPopup->resourceSelected(m_resourceProvider->currentPreset().data()); // TODO!!!!!!!! //m_presetsPopup->updateViewSettings(); } void KisPaintopBox::slotSaveLockedOptionToPreset(KisPropertiesConfigurationSP p) { QMapIterator i(p->getProperties()); while (i.hasNext()) { i.next(); m_resourceProvider->currentPreset()->settings()->setProperty(i.key(), QVariant(i.value())); if (m_resourceProvider->currentPreset()->settings()->hasProperty(i.key() + "_previous")) { m_resourceProvider->currentPreset()->settings()->removeProperty(i.key() + "_previous"); } } slotGuiChangedCurrentPreset(); } void KisPaintopBox::slotDropLockedOption(KisPropertiesConfigurationSP p) { KisSignalsBlocker blocker(m_optionWidget); KisPaintOpPresetSP preset = m_resourceProvider->currentPreset(); { KisPaintOpPreset::DirtyStateSaver dirtySaver(preset.data()); QMapIterator i(p->getProperties()); while (i.hasNext()) { i.next(); if (preset->settings()->hasProperty(i.key() + "_previous")) { preset->settings()->setProperty(i.key(), preset->settings()->getProperty(i.key() + "_previous")); preset->settings()->removeProperty(i.key() + "_previous"); } } } //slotUpdatePreset(); } void KisPaintopBox::slotDirtyPresetToggled(bool value) { if (!value) { slotReloadPreset(); m_presetsPopup->resourceSelected(m_resourceProvider->currentPreset().data()); m_presetsPopup->updateViewSettings(); } m_dirtyPresetsEnabled = value; KisConfig cfg; cfg.setUseDirtyPresets(m_dirtyPresetsEnabled); } void KisPaintopBox::slotEraserBrushSizeToggled(bool value) { m_eraserBrushSizeEnabled = value; KisConfig cfg; cfg.setUseEraserBrushSize(m_eraserBrushSizeEnabled); } void KisPaintopBox::slotEraserBrushOpacityToggled(bool value) { m_eraserBrushOpacityEnabled = value; KisConfig cfg; cfg.setUseEraserBrushOpacity(m_eraserBrushOpacityEnabled); } void KisPaintopBox::slotUpdateSelectionIcon() { m_hMirrorAction->setIcon(KisIconUtils::loadIcon("symmetry-horizontal")); m_vMirrorAction->setIcon(KisIconUtils::loadIcon("symmetry-vertical")); KisConfig cfg; if (!cfg.toolOptionsInDocker() && m_toolOptionsPopupButton) { m_toolOptionsPopupButton->setIcon(KisIconUtils::loadIcon("configure")); } m_presetSelectorPopupButton->setIcon(KisIconUtils::loadIcon("paintop_settings_01")); m_brushEditorPopupButton->setIcon(KisIconUtils::loadIcon("paintop_settings_02")); m_workspaceWidget->setIcon(KisIconUtils::loadIcon("view-choose")); m_eraseAction->setIcon(KisIconUtils::loadIcon("draw-eraser")); m_reloadAction->setIcon(KisIconUtils::loadIcon("view-refresh")); if (m_disablePressureAction->isChecked()) { m_disablePressureButton->setIcon(KisIconUtils::loadIcon("transform_icons_penPressure")); } else { m_disablePressureButton->setIcon(KisIconUtils::loadIcon("transform_icons_penPressure_locked")); } } void KisPaintopBox::slotLockXMirrorToggle(bool toggleLock) { m_resourceProvider->setMirrorHorizontalLock(toggleLock); } void KisPaintopBox::slotLockYMirrorToggle(bool toggleLock) { m_resourceProvider->setMirrorVerticalLock(toggleLock); } void KisPaintopBox::slotHideDecorationMirrorX(bool toggled) { m_resourceProvider->setMirrorHorizontalHideDecorations(toggled); } void KisPaintopBox::slotHideDecorationMirrorY(bool toggled) { m_resourceProvider->setMirrorVerticalHideDecorations(toggled); } void KisPaintopBox::slotMoveToCenterMirrorX() { m_resourceProvider->mirrorHorizontalMoveCanvasToCenter(); } void KisPaintopBox::slotMoveToCenterMirrorY() { m_resourceProvider->mirrorVerticalMoveCanvasToCenter(); } diff --git a/libs/ui/opengl/kis_opengl_canvas2.cpp b/libs/ui/opengl/kis_opengl_canvas2.cpp index 4c05ba1a37..6cae9872af 100644 --- a/libs/ui/opengl/kis_opengl_canvas2.cpp +++ b/libs/ui/opengl/kis_opengl_canvas2.cpp @@ -1,889 +1,894 @@ /* This file is part of the KDE project * Copyright (C) Boudewijn Rempt , (C) 2006-2013 * Copyright (C) 2015 Michael Abrahams * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #define GL_GLEXT_PROTOTYPES #include "opengl/kis_opengl_canvas2.h" #include "opengl/kis_opengl_canvas2_p.h" #include "opengl/kis_opengl_shader_loader.h" #include "opengl/kis_opengl_canvas_debugger.h" #include "canvas/kis_canvas2.h" #include "canvas/kis_coordinates_converter.h" #include "canvas/kis_display_filter.h" #include "canvas/kis_display_color_converter.h" #include "kis_config.h" #include "kis_config_notifier.h" #include "kis_debug.h" #include #include #include #include #include #include #include #include #include #include #include #ifndef Q_OS_OSX #include #endif #define NEAR_VAL -1000.0 #define FAR_VAL 1000.0 #ifndef GL_CLAMP_TO_EDGE #define GL_CLAMP_TO_EDGE 0x812F #endif #define PROGRAM_VERTEX_ATTRIBUTE 0 #define PROGRAM_TEXCOORD_ATTRIBUTE 1 static bool OPENGL_SUCCESS = false; struct KisOpenGLCanvas2::Private { public: ~Private() { delete displayShader; delete checkerShader; delete solidColorShader; Sync::deleteSync(glSyncObject); } bool canvasInitialized{false}; KisOpenGLImageTexturesSP openGLImageTextures; KisOpenGLShaderLoader shaderLoader; KisShaderProgram *displayShader{0}; KisShaderProgram *checkerShader{0}; KisShaderProgram *solidColorShader{0}; bool displayShaderCompiledWithDisplayFilterSupport{false}; GLfloat checkSizeScale; bool scrollCheckers; QSharedPointer displayFilter; KisOpenGL::FilterMode filterMode; bool proofingConfigIsUpdated=false; GLsync glSyncObject{0}; bool wrapAroundMode{false}; // Stores a quad for drawing the canvas QOpenGLVertexArrayObject quadVAO; QOpenGLBuffer quadBuffers[2]; // Stores data for drawing tool outlines QOpenGLVertexArrayObject outlineVAO; QOpenGLBuffer lineBuffer; QVector3D vertices[6]; QVector2D texCoords[6]; #ifndef Q_OS_OSX QOpenGLFunctions_2_1 *glFn201; #endif + qreal pixelGridDrawingThreshold; + bool pixelGridEnabled; + QColor gridColor; + int xToColWithWrapCompensation(int x, const QRect &imageRect) { int firstImageColumn = openGLImageTextures->xToCol(imageRect.left()); int lastImageColumn = openGLImageTextures->xToCol(imageRect.right()); int colsPerImage = lastImageColumn - firstImageColumn + 1; int numWraps = floor(qreal(x) / imageRect.width()); int remainder = x - imageRect.width() * numWraps; return colsPerImage * numWraps + openGLImageTextures->xToCol(remainder); } int yToRowWithWrapCompensation(int y, const QRect &imageRect) { int firstImageRow = openGLImageTextures->yToRow(imageRect.top()); int lastImageRow = openGLImageTextures->yToRow(imageRect.bottom()); int rowsPerImage = lastImageRow - firstImageRow + 1; int numWraps = floor(qreal(y) / imageRect.height()); int remainder = y - imageRect.height() * numWraps; return rowsPerImage * numWraps + openGLImageTextures->yToRow(remainder); } }; KisOpenGLCanvas2::KisOpenGLCanvas2(KisCanvas2 *canvas, KisCoordinatesConverter *coordinatesConverter, QWidget *parent, KisImageWSP image, KisDisplayColorConverter *colorConverter) : QOpenGLWidget(parent) , KisCanvasWidgetBase(canvas, coordinatesConverter) , d(new Private()) { KisConfig cfg; cfg.setCanvasState("OPENGL_STARTED"); d->openGLImageTextures = KisOpenGLImageTextures::getImageTextures(image, colorConverter->monitorProfile(), colorConverter->renderingIntent(), colorConverter->conversionFlags()); setAcceptDrops(true); setAutoFillBackground(false); setFocusPolicy(Qt::StrongFocus); setAttribute(Qt::WA_NoSystemBackground, true); #ifdef Q_OS_OSX setAttribute(Qt::WA_AcceptTouchEvents, false); #else setAttribute(Qt::WA_AcceptTouchEvents, true); #endif setAttribute(Qt::WA_InputMethodEnabled, true); setAttribute(Qt::WA_DontCreateNativeAncestors, true); setDisplayFilterImpl(colorConverter->displayFilter(), true); connect(KisConfigNotifier::instance(), SIGNAL(configChanged()), SLOT(slotConfigChanged())); slotConfigChanged(); cfg.writeEntry("canvasState", "OPENGL_SUCCESS"); } KisOpenGLCanvas2::~KisOpenGLCanvas2() { delete d; } bool KisOpenGLCanvas2::needsFpsDebugging() const { return KisOpenglCanvasDebugger::instance()->showFpsOnCanvas(); } void KisOpenGLCanvas2::setDisplayFilter(QSharedPointer displayFilter) { setDisplayFilterImpl(displayFilter, false); } void KisOpenGLCanvas2::setDisplayFilterImpl(QSharedPointer displayFilter, bool initializing) { bool needsInternalColorManagement = !displayFilter || displayFilter->useInternalColorManagement(); bool needsFullRefresh = d->openGLImageTextures->setInternalColorManagementActive(needsInternalColorManagement); d->displayFilter = displayFilter; if (!initializing && needsFullRefresh) { canvas()->startUpdateInPatches(canvas()->image()->bounds()); } else if (!initializing) { canvas()->updateCanvas(); } } void KisOpenGLCanvas2::setWrapAroundViewingMode(bool value) { d->wrapAroundMode = value; update(); } inline void rectToVertices(QVector3D* vertices, const QRectF &rc) { vertices[0] = QVector3D(rc.left(), rc.bottom(), 0.f); vertices[1] = QVector3D(rc.left(), rc.top(), 0.f); vertices[2] = QVector3D(rc.right(), rc.bottom(), 0.f); vertices[3] = QVector3D(rc.left(), rc.top(), 0.f); vertices[4] = QVector3D(rc.right(), rc.top(), 0.f); vertices[5] = QVector3D(rc.right(), rc.bottom(), 0.f); } inline void rectToTexCoords(QVector2D* texCoords, const QRectF &rc) { texCoords[0] = QVector2D(rc.left(), rc.bottom()); texCoords[1] = QVector2D(rc.left(), rc.top()); texCoords[2] = QVector2D(rc.right(), rc.bottom()); texCoords[3] = QVector2D(rc.left(), rc.top()); texCoords[4] = QVector2D(rc.right(), rc.top()); texCoords[5] = QVector2D(rc.right(), rc.bottom()); } void KisOpenGLCanvas2::initializeGL() { KisOpenGL::initializeContext(context()); initializeOpenGLFunctions(); #ifndef Q_OS_OSX d->glFn201 = context()->versionFunctions(); if (!d->glFn201) { warnUI << "Cannot obtain QOpenGLFunctions_2_1, glLogicOp cannot be used"; } #endif KisConfig cfg; d->openGLImageTextures->setProofingConfig(canvas()->proofingConfiguration()); d->openGLImageTextures->initGL(context()->functions()); d->openGLImageTextures->generateCheckerTexture(createCheckersImage(cfg.checkSize())); initializeShaders(); // If we support OpenGL 3.2, then prepare our VAOs and VBOs for drawing if (KisOpenGL::hasOpenGL3()) { d->quadVAO.create(); d->quadVAO.bind(); glEnableVertexAttribArray(PROGRAM_VERTEX_ATTRIBUTE); glEnableVertexAttribArray(PROGRAM_TEXCOORD_ATTRIBUTE); // Create the vertex buffer object, it has 6 vertices with 3 components d->quadBuffers[0].create(); d->quadBuffers[0].setUsagePattern(QOpenGLBuffer::StaticDraw); d->quadBuffers[0].bind(); d->quadBuffers[0].allocate(d->vertices, 6 * 3 * sizeof(float)); glVertexAttribPointer(PROGRAM_VERTEX_ATTRIBUTE, 3, GL_FLOAT, GL_FALSE, 0, 0); // Create the texture buffer object, it has 6 texture coordinates with 2 components d->quadBuffers[1].create(); d->quadBuffers[1].setUsagePattern(QOpenGLBuffer::StaticDraw); d->quadBuffers[1].bind(); d->quadBuffers[1].allocate(d->texCoords, 6 * 2 * sizeof(float)); glVertexAttribPointer(PROGRAM_TEXCOORD_ATTRIBUTE, 2, GL_FLOAT, GL_FALSE, 0, 0); // Create the outline buffer, this buffer will store the outlines of // tools and will frequently change data d->outlineVAO.create(); d->outlineVAO.bind(); glEnableVertexAttribArray(PROGRAM_VERTEX_ATTRIBUTE); // The outline buffer has a StreamDraw usage pattern, because it changes constantly d->lineBuffer.create(); d->lineBuffer.setUsagePattern(QOpenGLBuffer::StreamDraw); d->lineBuffer.bind(); glVertexAttribPointer(PROGRAM_VERTEX_ATTRIBUTE, 3, GL_FLOAT, GL_FALSE, 0, 0); } Sync::init(context()); d->canvasInitialized = true; } /** * Loads all shaders and reports compilation problems */ void KisOpenGLCanvas2::initializeShaders() { KIS_SAFE_ASSERT_RECOVER_RETURN(!d->canvasInitialized); delete d->checkerShader; delete d->solidColorShader; d->checkerShader = 0; d->solidColorShader = 0; try { d->checkerShader = d->shaderLoader.loadCheckerShader(); d->solidColorShader = d->shaderLoader.loadSolidColorShader(); } catch (const ShaderLoaderException &e) { reportFailedShaderCompilation(e.what()); } initializeDisplayShader(); } void KisOpenGLCanvas2::initializeDisplayShader() { KIS_SAFE_ASSERT_RECOVER_RETURN(!d->canvasInitialized); bool useHiQualityFiltering = d->filterMode == KisOpenGL::HighQualityFiltering; delete d->displayShader; d->displayShader = 0; try { d->displayShader = d->shaderLoader.loadDisplayShader(d->displayFilter, useHiQualityFiltering); d->displayShaderCompiledWithDisplayFilterSupport = d->displayFilter; } catch (const ShaderLoaderException &e) { reportFailedShaderCompilation(e.what()); } } /** * Displays a message box telling the user that * shader compilation failed and turns off OpenGL. */ void KisOpenGLCanvas2::reportFailedShaderCompilation(const QString &context) { KisConfig cfg; qDebug() << "Shader Compilation Failure: " << context; QMessageBox::critical(this, i18nc("@title:window", "Krita"), QString(i18n("Krita could not initialize the OpenGL canvas:\n\n%1\n\n Krita will disable OpenGL and close now.")).arg(context), QMessageBox::Close); cfg.setUseOpenGL(false); cfg.setCanvasState("OPENGL_FAILED"); } void KisOpenGLCanvas2::resizeGL(int width, int height) { coordinatesConverter()->setCanvasWidgetSize(QSize(width, height)); paintGL(); } void KisOpenGLCanvas2::paintGL() { if (!OPENGL_SUCCESS) { KisConfig cfg; cfg.writeEntry("canvasState", "OPENGL_PAINT_STARTED"); } KisOpenglCanvasDebugger::instance()->nofityPaintRequested(); renderCanvasGL(); if (d->glSyncObject) { Sync::deleteSync(d->glSyncObject); } d->glSyncObject = Sync::getSync(); QPainter gc(this); renderDecorations(&gc); gc.end(); if (!OPENGL_SUCCESS) { KisConfig cfg; cfg.writeEntry("canvasState", "OPENGL_SUCCESS"); OPENGL_SUCCESS = true; } } void KisOpenGLCanvas2::paintToolOutline(const QPainterPath &path) { if (!d->solidColorShader->bind()) { return; } // setup the mvp transformation QMatrix4x4 projectionMatrix; projectionMatrix.setToIdentity(); projectionMatrix.ortho(0, width(), height(), 0, NEAR_VAL, FAR_VAL); // Set view/projection matrices QMatrix4x4 modelMatrix(coordinatesConverter()->flakeToWidgetTransform()); modelMatrix.optimize(); modelMatrix = projectionMatrix * modelMatrix; d->solidColorShader->setUniformValue(d->solidColorShader->location(Uniform::ModelViewProjection), modelMatrix); glHint(GL_LINE_SMOOTH_HINT, GL_NICEST); glEnable(GL_COLOR_LOGIC_OP); #ifndef Q_OS_OSX if (d->glFn201) { d->glFn201->glLogicOp(GL_XOR); } #else glLogicOp(GL_XOR); #endif KisConfig cfg; QColor cursorColor = cfg.getCursorMainColor(); d->solidColorShader->setUniformValue( d->solidColorShader->location(Uniform::FragmentColor), QVector4D(cursorColor.redF(), cursorColor.greenF(), cursorColor.blueF(), 1.0f)); // Paint the tool outline if (KisOpenGL::hasOpenGL3()) { d->outlineVAO.bind(); d->lineBuffer.bind(); } // Convert every disjointed subpath to a polygon and draw that polygon QList subPathPolygons = path.toSubpathPolygons(); for (int i = 0; i < subPathPolygons.size(); i++) { const QPolygonF& polygon = subPathPolygons.at(i); QVector vertices; vertices.resize(polygon.count()); for (int j = 0; j < polygon.count(); j++) { QPointF p = polygon.at(j); vertices[j].setX(p.x()); vertices[j].setY(p.y()); } if (KisOpenGL::hasOpenGL3()) { d->lineBuffer.allocate(vertices.constData(), 3 * vertices.size() * sizeof(float)); } else { d->solidColorShader->enableAttributeArray(PROGRAM_VERTEX_ATTRIBUTE); d->solidColorShader->setAttributeArray(PROGRAM_VERTEX_ATTRIBUTE, vertices.constData()); } glDrawArrays(GL_LINE_STRIP, 0, vertices.size()); } if (KisOpenGL::hasOpenGL3()) { d->lineBuffer.release(); d->outlineVAO.release(); } glDisable(GL_COLOR_LOGIC_OP); d->solidColorShader->release(); } bool KisOpenGLCanvas2::isBusy() const { const bool isBusyStatus = Sync::syncStatus(d->glSyncObject) == Sync::Unsignaled; KisOpenglCanvasDebugger::instance()->nofitySyncStatus(isBusyStatus); return isBusyStatus; } void KisOpenGLCanvas2::drawCheckers() { if (!d->checkerShader) { return; } KisCoordinatesConverter *converter = coordinatesConverter(); QTransform textureTransform; QTransform modelTransform; QRectF textureRect; QRectF modelRect; QRectF viewportRect = !d->wrapAroundMode ? converter->imageRectInViewportPixels() : converter->widgetToViewport(this->rect()); converter->getOpenGLCheckersInfo(viewportRect, &textureTransform, &modelTransform, &textureRect, &modelRect, d->scrollCheckers); textureTransform *= QTransform::fromScale(d->checkSizeScale / KisOpenGLImageTextures::BACKGROUND_TEXTURE_SIZE, d->checkSizeScale / KisOpenGLImageTextures::BACKGROUND_TEXTURE_SIZE); if (!d->checkerShader->bind()) { qWarning() << "Could not bind checker shader"; return; } QMatrix4x4 projectionMatrix; projectionMatrix.setToIdentity(); projectionMatrix.ortho(0, width(), height(), 0, NEAR_VAL, FAR_VAL); // Set view/projection matrices QMatrix4x4 modelMatrix(modelTransform); modelMatrix.optimize(); modelMatrix = projectionMatrix * modelMatrix; d->checkerShader->setUniformValue(d->checkerShader->location(Uniform::ModelViewProjection), modelMatrix); QMatrix4x4 textureMatrix(textureTransform); d->checkerShader->setUniformValue(d->checkerShader->location(Uniform::TextureMatrix), textureMatrix); //Setup the geometry for rendering if (KisOpenGL::hasOpenGL3()) { rectToVertices(d->vertices, modelRect); d->quadBuffers[0].bind(); d->quadBuffers[0].write(0, d->vertices, 3 * 6 * sizeof(float)); rectToTexCoords(d->texCoords, textureRect); d->quadBuffers[1].bind(); d->quadBuffers[1].write(0, d->texCoords, 2 * 6 * sizeof(float)); } else { rectToVertices(d->vertices, modelRect); d->checkerShader->enableAttributeArray(PROGRAM_VERTEX_ATTRIBUTE); d->checkerShader->setAttributeArray(PROGRAM_VERTEX_ATTRIBUTE, d->vertices); rectToTexCoords(d->texCoords, textureRect); d->checkerShader->enableAttributeArray(PROGRAM_TEXCOORD_ATTRIBUTE); d->checkerShader->setAttributeArray(PROGRAM_TEXCOORD_ATTRIBUTE, d->texCoords); } // render checkers glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, d->openGLImageTextures->checkerTexture()); glDrawArrays(GL_TRIANGLES, 0, 6); glBindTexture(GL_TEXTURE_2D, 0); d->checkerShader->release(); glBindBuffer(GL_ARRAY_BUFFER, 0); } void KisOpenGLCanvas2::drawGrid() { if (!d->solidColorShader->bind()) { return; } QMatrix4x4 projectionMatrix; projectionMatrix.setToIdentity(); projectionMatrix.ortho(0, width(), height(), 0, NEAR_VAL, FAR_VAL); // Set view/projection matrices QMatrix4x4 modelMatrix(coordinatesConverter()->imageToWidgetTransform()); modelMatrix.optimize(); modelMatrix = projectionMatrix * modelMatrix; d->solidColorShader->setUniformValue(d->solidColorShader->location(Uniform::ModelViewProjection), modelMatrix); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - KisConfig cfg; - QColor gridColor = cfg.getPixelGridColor(); d->solidColorShader->setUniformValue( d->solidColorShader->location(Uniform::FragmentColor), - QVector4D(gridColor.redF(), gridColor.greenF(), gridColor.blueF(), 0.5f)); + QVector4D(d->gridColor.redF(), d->gridColor.greenF(), d->gridColor.blueF(), 0.5f)); if (KisOpenGL::hasOpenGL3()) { d->outlineVAO.bind(); d->lineBuffer.bind(); } QRectF widgetRect(0,0, width(), height()); QRectF widgetRectInImagePixels = coordinatesConverter()->documentToImage(coordinatesConverter()->widgetToDocument(widgetRect)); QRect wr = widgetRectInImagePixels.toAlignedRect(); if (!d->wrapAroundMode) { wr &= d->openGLImageTextures->storedImageBounds(); } QPoint topLeftCorner = wr.topLeft(); QPoint bottomRightCorner = wr.bottomRight() + QPoint(1, 1); QVector grid; for (int i = topLeftCorner.x(); i <= bottomRightCorner.x(); ++i) { grid.append(QVector3D(i, topLeftCorner.y(), 0)); grid.append(QVector3D(i, bottomRightCorner.y(), 0)); } for (int i = topLeftCorner.y(); i <= bottomRightCorner.y(); ++i) { grid.append(QVector3D(topLeftCorner.x(), i, 0)); grid.append(QVector3D(bottomRightCorner.x(), i, 0)); } if (KisOpenGL::hasOpenGL3()) { d->lineBuffer.allocate(grid.constData(), 3 * grid.size() * sizeof(float)); } else { d->solidColorShader->enableAttributeArray(PROGRAM_VERTEX_ATTRIBUTE); d->solidColorShader->setAttributeArray(PROGRAM_VERTEX_ATTRIBUTE, grid.constData()); } glDrawArrays(GL_LINES, 0, grid.size()); if (KisOpenGL::hasOpenGL3()) { d->lineBuffer.release(); d->outlineVAO.release(); } d->solidColorShader->release(); } void KisOpenGLCanvas2::drawImage() { if (!d->displayShader) { return; } glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); KisCoordinatesConverter *converter = coordinatesConverter(); d->displayShader->bind(); QMatrix4x4 projectionMatrix; projectionMatrix.setToIdentity(); projectionMatrix.ortho(0, width(), height(), 0, NEAR_VAL, FAR_VAL); // Set view/projection matrices QMatrix4x4 modelMatrix(converter->imageToWidgetTransform()); modelMatrix.optimize(); modelMatrix = projectionMatrix * modelMatrix; d->displayShader->setUniformValue(d->displayShader->location(Uniform::ModelViewProjection), modelMatrix); QMatrix4x4 textureMatrix; textureMatrix.setToIdentity(); d->displayShader->setUniformValue(d->displayShader->location(Uniform::TextureMatrix), textureMatrix); QRectF widgetRect(0,0, width(), height()); QRectF widgetRectInImagePixels = converter->documentToImage(converter->widgetToDocument(widgetRect)); qreal scaleX, scaleY; converter->imageScale(&scaleX, &scaleY); d->displayShader->setUniformValue(d->displayShader->location(Uniform::ViewportScale), (GLfloat) scaleX); d->displayShader->setUniformValue(d->displayShader->location(Uniform::TexelSize), (GLfloat) d->openGLImageTextures->texelSize()); QRect ir = d->openGLImageTextures->storedImageBounds(); QRect wr = widgetRectInImagePixels.toAlignedRect(); if (!d->wrapAroundMode) { // if we don't want to paint wrapping images, just limit the // processing area, and the code will handle all the rest wr &= ir; } int firstColumn = d->xToColWithWrapCompensation(wr.left(), ir); int lastColumn = d->xToColWithWrapCompensation(wr.right(), ir); int firstRow = d->yToRowWithWrapCompensation(wr.top(), ir); int lastRow = d->yToRowWithWrapCompensation(wr.bottom(), ir); int minColumn = d->openGLImageTextures->xToCol(ir.left()); int maxColumn = d->openGLImageTextures->xToCol(ir.right()); int minRow = d->openGLImageTextures->yToRow(ir.top()); int maxRow = d->openGLImageTextures->yToRow(ir.bottom()); int imageColumns = maxColumn - minColumn + 1; int imageRows = maxRow - minRow + 1; for (int col = firstColumn; col <= lastColumn; col++) { for (int row = firstRow; row <= lastRow; row++) { int effectiveCol = col; int effectiveRow = row; QPointF tileWrappingTranslation; if (effectiveCol > maxColumn || effectiveCol < minColumn) { int translationStep = floor(qreal(col) / imageColumns); int originCol = translationStep * imageColumns; effectiveCol = col - originCol; tileWrappingTranslation.rx() = translationStep * ir.width(); } if (effectiveRow > maxRow || effectiveRow < minRow) { int translationStep = floor(qreal(row) / imageRows); int originRow = translationStep * imageRows; effectiveRow = row - originRow; tileWrappingTranslation.ry() = translationStep * ir.height(); } KisTextureTile *tile = d->openGLImageTextures->getTextureTileCR(effectiveCol, effectiveRow); if (!tile) { warnUI << "OpenGL: Trying to paint texture tile but it has not been created yet."; continue; } /* * We create a float rect here to workaround Qt's * "history reasons" in calculation of right() * and bottom() coordinates of integer rects. */ QRectF textureRect(tile->tileRectInTexturePixels()); QRectF modelRect(tile->tileRectInImagePixels().translated(tileWrappingTranslation.x(), tileWrappingTranslation.y())); //Setup the geometry for rendering if (KisOpenGL::hasOpenGL3()) { rectToVertices(d->vertices, modelRect); d->quadBuffers[0].bind(); d->quadBuffers[0].write(0, d->vertices, 3 * 6 * sizeof(float)); rectToTexCoords(d->texCoords, textureRect); d->quadBuffers[1].bind(); d->quadBuffers[1].write(0, d->texCoords, 2 * 6 * sizeof(float)); } else { rectToVertices(d->vertices, modelRect); d->displayShader->enableAttributeArray(PROGRAM_VERTEX_ATTRIBUTE); d->displayShader->setAttributeArray(PROGRAM_VERTEX_ATTRIBUTE, d->vertices); rectToTexCoords(d->texCoords, textureRect); d->displayShader->enableAttributeArray(PROGRAM_TEXCOORD_ATTRIBUTE); d->displayShader->setAttributeArray(PROGRAM_TEXCOORD_ATTRIBUTE, d->texCoords); } if (d->displayFilter) { glActiveTexture(GL_TEXTURE0 + 1); glBindTexture(GL_TEXTURE_3D, d->displayFilter->lutTexture()); d->displayShader->setUniformValue(d->displayShader->location(Uniform::Texture1), 1); } int currentLodPlane = tile->currentLodPlane(); if (d->displayShader->location(Uniform::FixedLodLevel) >= 0) { d->displayShader->setUniformValue(d->displayShader->location(Uniform::FixedLodLevel), (GLfloat) currentLodPlane); } glActiveTexture(GL_TEXTURE0); tile->bindToActiveTexture(); if (currentLodPlane > 0) { glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_NEAREST); } else if (SCALE_MORE_OR_EQUAL_TO(scaleX, scaleY, 2.0)) { glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); } else { glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); switch(d->filterMode) { case KisOpenGL::NearestFilterMode: glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); break; case KisOpenGL::BilinearFilterMode: glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); break; case KisOpenGL::TrilinearFilterMode: glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); break; case KisOpenGL::HighQualityFiltering: if (SCALE_LESS_THAN(scaleX, scaleY, 0.5)) { glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_NEAREST); } else { glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); } break; } } glDrawArrays(GL_TRIANGLES, 0, 6); } } glBindTexture(GL_TEXTURE_2D, 0); d->displayShader->release(); glBindBuffer(GL_ARRAY_BUFFER, 0); } void KisOpenGLCanvas2::slotConfigChanged() { KisConfig cfg; d->checkSizeScale = KisOpenGLImageTextures::BACKGROUND_TEXTURE_CHECK_SIZE / static_cast(cfg.checkSize()); d->scrollCheckers = cfg.scrollCheckers(); d->openGLImageTextures->generateCheckerTexture(createCheckersImage(cfg.checkSize())); d->openGLImageTextures->updateConfig(cfg.useOpenGLTextureBuffer(), cfg.numMipmapLevels()); d->filterMode = (KisOpenGL::FilterMode) cfg.openGLFilteringMode(); + d->pixelGridDrawingThreshold = cfg.getPixelGridDrawingThreshold(); + d->pixelGridEnabled = cfg.pixelGridEnabled(); + d->gridColor = cfg.getPixelGridColor(); + notifyConfigChanged(); } QVariant KisOpenGLCanvas2::inputMethodQuery(Qt::InputMethodQuery query) const { return processInputMethodQuery(query); } void KisOpenGLCanvas2::inputMethodEvent(QInputMethodEvent *event) { processInputMethodEvent(event); } void KisOpenGLCanvas2::renderCanvasGL() { // Draw the border (that is, clear the whole widget to the border color) QColor widgetBackgroundColor = borderColor(); glClearColor(widgetBackgroundColor.redF(), widgetBackgroundColor.greenF(), widgetBackgroundColor.blueF(), 1.0); glClear(GL_COLOR_BUFFER_BIT); if ((d->displayFilter && d->displayFilter->updateShader()) || (bool(d->displayFilter) != d->displayShaderCompiledWithDisplayFilterSupport)) { KIS_SAFE_ASSERT_RECOVER_NOOP(d->canvasInitialized); d->canvasInitialized = false; // TODO: check if actually needed? initializeDisplayShader(); d->canvasInitialized = true; } if (KisOpenGL::hasOpenGL3()) { d->quadVAO.bind(); } drawCheckers(); drawImage(); - KisConfig cfg; - if ((coordinatesConverter()->effectiveZoom() > cfg.getPixelGridDrawingThreshold() - 0.00001) && cfg.pixelGridEnabled()) { + if ((coordinatesConverter()->effectiveZoom() > d->pixelGridDrawingThreshold - 0.00001) && d->pixelGridEnabled) { drawGrid(); } if (KisOpenGL::hasOpenGL3()) { d->quadVAO.release(); } } void KisOpenGLCanvas2::renderDecorations(QPainter *painter) { QRect boundingRect = coordinatesConverter()->imageRectInWidgetPixels().toAlignedRect(); drawDecorations(*painter, boundingRect); } void KisOpenGLCanvas2::setDisplayProfile(KisDisplayColorConverter *colorConverter) { d->openGLImageTextures->setMonitorProfile(colorConverter->monitorProfile(), colorConverter->renderingIntent(), colorConverter->conversionFlags()); } void KisOpenGLCanvas2::channelSelectionChanged(const QBitArray &channelFlags) { d->openGLImageTextures->setChannelFlags(channelFlags); } void KisOpenGLCanvas2::finishResizingImage(qint32 w, qint32 h) { if (d->canvasInitialized) { d->openGLImageTextures->slotImageSizeChanged(w, h); } } KisUpdateInfoSP KisOpenGLCanvas2::startUpdateCanvasProjection(const QRect & rc, const QBitArray &channelFlags) { d->openGLImageTextures->setChannelFlags(channelFlags); if (canvas()->proofingConfigUpdated()) { d->openGLImageTextures->setProofingConfig(canvas()->proofingConfiguration()); canvas()->setProofingConfigUpdated(false); } return d->openGLImageTextures->updateCache(rc); } QRect KisOpenGLCanvas2::updateCanvasProjection(KisUpdateInfoSP info) { // See KisQPainterCanvas::updateCanvasProjection for more info bool isOpenGLUpdateInfo = dynamic_cast(info.data()); if (isOpenGLUpdateInfo) { d->openGLImageTextures->recalculateCache(info); } #ifdef Q_OS_OSX /** * There is a bug on OSX: if we issue frame redraw before the tiles finished * uploading, the tiles will become corrupted. Depending on the GPU/driver * version either the tile itself, or its mipmaps will become totally * transparent. */ glFinish(); #endif return QRect(); // FIXME: Implement dirty rect for OpenGL } bool KisOpenGLCanvas2::callFocusNextPrevChild(bool next) { return focusNextPrevChild(next); } KisOpenGLImageTexturesSP KisOpenGLCanvas2::openGLImageTextures() const { return d->openGLImageTextures; } diff --git a/libs/ui/widgets/KisVisualColorSelectorShape.cpp b/libs/ui/widgets/KisVisualColorSelectorShape.cpp index 104d39a4f3..cfdda136c4 100644 --- a/libs/ui/widgets/KisVisualColorSelectorShape.cpp +++ b/libs/ui/widgets/KisVisualColorSelectorShape.cpp @@ -1,604 +1,620 @@ /* * Copyright (C) Wolthera van Hovell tot Westerflier , (C) 2016 * * 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 "KisVisualColorSelectorShape.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include "KoColorConversions.h" #include "KoColorDisplayRendererInterface.h" #include "KoChannelInfo.h" #include #include #include "kis_signal_compressor.h" #include "kis_debug.h" struct KisVisualColorSelectorShape::Private { QImage gradient; QImage fullSelector; bool imagesNeedUpdate {true}; QPointF currentCoordinates; Dimensions dimension; ColorModel model; const KoColorSpace *colorSpace; KoColor currentColor; int channel1; int channel2; KisSignalCompressor *updateTimer {0}; bool mousePressActive = false; const KoColorDisplayRendererInterface *displayRenderer = 0; qreal hue = 0.0; qreal sat = 0.0; qreal tone = 0.0; + bool usesOCIO = false; + bool isRGBA = false; + bool is8Bit = false; }; KisVisualColorSelectorShape::KisVisualColorSelectorShape(QWidget *parent, KisVisualColorSelectorShape::Dimensions dimension, KisVisualColorSelectorShape::ColorModel model, const KoColorSpace *cs, int channel1, int channel2, const KoColorDisplayRendererInterface *displayRenderer): QWidget(parent), m_d(new Private) { m_d->dimension = dimension; m_d->model = model; m_d->colorSpace = cs; + + // TODO: The following is done because the IDs are actually strings. Ideally, in the future, we + // refactor everything so that the IDs are actually proper enums or something faster. + if (m_d->displayRenderer + && (m_d->colorSpace->colorDepthId() == Float16BitsColorDepthID + || m_d->colorSpace->colorDepthId() == Float32BitsColorDepthID + || m_d->colorSpace->colorDepthId() == Float64BitsColorDepthID) + && m_d->colorSpace->colorModelId() != LABAColorModelID + && m_d->colorSpace->colorModelId() != CMYKAColorModelID) { + m_d->usesOCIO = true; + } else { + m_d->usesOCIO = false; + } + if (m_d->colorSpace->colorModelId() == RGBAColorModelID) { + m_d->isRGBA = true; + } else { + m_d->isRGBA = false; + } + if (m_d->colorSpace->colorDepthId() == Integer8BitsColorDepthID) { + m_d->is8Bit = true; + } else { + m_d->is8Bit = false; + } m_d->currentColor = KoColor(); m_d->currentColor.setOpacity(1.0); m_d->currentColor.convertTo(cs); int maxchannel = m_d->colorSpace->colorChannelCount()-1; m_d->channel1 = qBound(0, channel1, maxchannel); m_d->channel2 = qBound(0, channel2, maxchannel); this->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); // HACK: the updateTimer isn't connected to anything, we only check whether it's still active // and running in order to determine whether we will emit a certain signal. m_d->updateTimer = new KisSignalCompressor(100 /* ms */, KisSignalCompressor::POSTPONE, this); setDisplayRenderer(displayRenderer); show(); } KisVisualColorSelectorShape::~KisVisualColorSelectorShape() { } void KisVisualColorSelectorShape::updateCursor() { QPointF point1 = convertKoColorToShapeCoordinate(m_d->currentColor); if (point1 != m_d->currentCoordinates) { m_d->currentCoordinates = point1; } } QPointF KisVisualColorSelectorShape::getCursorPosition() { return m_d->currentCoordinates; } void KisVisualColorSelectorShape::setColor(KoColor c) { //qDebug() << this << "KisVisualColorSelectorShape::setColor"; if (c.colorSpace() != m_d->colorSpace) { c.convertTo(m_d->colorSpace); } m_d->currentColor = c; updateCursor(); m_d->imagesNeedUpdate = true; update(); } void KisVisualColorSelectorShape::setColorFromSibling(KoColor c) { //qDebug() << this << "setColorFromSibling"; if (c.colorSpace() != m_d->colorSpace) { c.convertTo(m_d->colorSpace); } m_d->currentColor = c; Q_EMIT sigNewColor(c); m_d->imagesNeedUpdate = true; update(); } void KisVisualColorSelectorShape::setDisplayRenderer (const KoColorDisplayRendererInterface *displayRenderer) { if (displayRenderer) { if (m_d->displayRenderer) { m_d->displayRenderer->disconnect(this); } m_d->displayRenderer = displayRenderer; } else { m_d->displayRenderer = KoDumbColorDisplayRenderer::instance(); } connect(m_d->displayRenderer, SIGNAL(displayConfigurationChanged()), SLOT(updateFromChangedDisplayRenderer()), Qt::UniqueConnection); } void KisVisualColorSelectorShape::updateFromChangedDisplayRenderer() { //qDebug() << this << "updateFromChangedDisplayRenderer();"; m_d->imagesNeedUpdate = true; updateCursor(); //m_d->currentColor = convertShapeCoordinateToKoColor(getCursorPosition()); update(); } void KisVisualColorSelectorShape::forceImageUpdate() { //qDebug() << this << "forceImageUpdate"; m_d->imagesNeedUpdate = true; } QColor KisVisualColorSelectorShape::getColorFromConverter(KoColor c){ QColor col; KoColor color = c; if (m_d->displayRenderer) { color.convertTo(m_d->displayRenderer->getPaintingColorSpace()); col = m_d->displayRenderer->toQColor(c); } else { col = c.toQColor(); } return col; } void KisVisualColorSelectorShape::slotSetActiveChannels(int channel1, int channel2) { //qDebug() << this << "slotSetActiveChannels"; int maxchannel = m_d->colorSpace->colorChannelCount()-1; m_d->channel1 = qBound(0, channel1, maxchannel); m_d->channel2 = qBound(0, channel2, maxchannel); m_d->imagesNeedUpdate = true; update(); } bool KisVisualColorSelectorShape::imagesNeedUpdate() const { return m_d->imagesNeedUpdate; } QImage KisVisualColorSelectorShape::getImageMap() { //qDebug() << this << ">>>>>>>>> getImageMap()" << m_d->imagesNeedUpdate; if (m_d->imagesNeedUpdate == true) { m_d->gradient = QImage(width(), height(), QImage::Format_ARGB32); m_d->gradient.fill(Qt::transparent); // KoColor c = m_d->currentColor; // Fill a buffer with the right kocolors quint8 *data = new quint8[width() * height() * height()]; quint8 *dataPtr = data; for (int y = 0; y < m_d->gradient.height(); y++) { for (int x=0; x < m_d->gradient.width(); x++) { QPointF newcoordinate = convertWidgetCoordinateToShapeCoordinate(QPoint(x, y)); KoColor c = convertShapeCoordinateToKoColor(newcoordinate); memcpy(dataPtr, c.data(), m_d->currentColor.colorSpace()->pixelSize()); dataPtr += m_d->currentColor.colorSpace()->pixelSize(); } } // Convert the buffer to a qimage if (m_d->displayRenderer) { m_d->gradient = m_d->displayRenderer->convertToQImage(m_d->currentColor.colorSpace(), data, width(), height()); } else { m_d->gradient = m_d->currentColor.colorSpace()->convertToQImage(data, width(), height(), 0, KoColorConversionTransformation::internalRenderingIntent(), KoColorConversionTransformation::internalConversionFlags()); } delete[] data; m_d->imagesNeedUpdate = false; } return m_d->gradient; } KoColor KisVisualColorSelectorShape::convertShapeCoordinateToKoColor(QPointF coordinates, bool cursor) { //qDebug() << this << ">>>>>>>>> convertShapeCoordinateToKoColor()" << coordinates; KoColor c = m_d->currentColor; QVector channelValues (c.colorSpace()->channelCount()); channelValues.fill(1.0); c.colorSpace()->normalisedChannelsValue(c.data(), channelValues); QVector channelValuesDisplay = channelValues; QVector maxvalue(c.colorSpace()->channelCount()); maxvalue.fill(1.0); - if (m_d->displayRenderer - && (m_d->colorSpace->colorDepthId() == Float16BitsColorDepthID - || m_d->colorSpace->colorDepthId() == Float32BitsColorDepthID - || m_d->colorSpace->colorDepthId() == Float64BitsColorDepthID) - && m_d->colorSpace->colorModelId() != LABAColorModelID - && m_d->colorSpace->colorModelId() != CMYKAColorModelID) { + if (m_d->usesOCIO == true) { for (int ch = 0; ch < maxvalue.size(); ch++) { KoChannelInfo *channel = m_d->colorSpace->channels()[ch]; maxvalue[ch] = m_d->displayRenderer->maxVisibleFloatValue(channel); channelValues[ch] = channelValues[ch]/(maxvalue[ch]); channelValuesDisplay[KoChannelInfo::displayPositionToChannelIndex(ch, m_d->colorSpace->channels())] = channelValues[ch]; } } else { for (int i =0; i < channelValues.size();i++) { channelValuesDisplay[KoChannelInfo::displayPositionToChannelIndex(i, m_d->colorSpace->channels())] = qBound((float)0.0,channelValues[i], (float)1.0); } } qreal huedivider = 1.0; qreal huedivider2 = 1.0; if (m_d->channel1 == 0) { huedivider = 360.0; } if (m_d->channel2 == 0) { huedivider2 = 360.0; } - if (m_d->model != ColorModel::Channel && c.colorSpace()->colorModelId().id() == "RGBA") { + if (m_d->model != ColorModel::Channel && m_d->isRGBA == true) { if (m_d->model == ColorModel::HSV) { /* * RGBToHSV has a undefined hue possibility. This means that hue will be -1. * This can be annoying for dealing with a selector, but I understand it is being * used for the KoColorSelector... For now implement a qMax here. */ QVector inbetween(3); RGBToHSV(channelValuesDisplay[0],channelValuesDisplay[1], channelValuesDisplay[2], &inbetween[0], &inbetween[1], &inbetween[2]); inbetween = convertvectorqrealTofloat(getHSX(convertvectorfloatToqreal(inbetween))); inbetween[m_d->channel1] = coordinates.x()*huedivider; if (m_d->dimension == Dimensions::twodimensional) { inbetween[m_d->channel2] = coordinates.y()*huedivider2; } if (cursor) { setHSX(convertvectorfloatToqreal(inbetween)); Q_EMIT sigHSXchange(); } HSVToRGB(qMax(inbetween[0],(float)0.0), inbetween[1], inbetween[2], &channelValuesDisplay[0], &channelValuesDisplay[1], &channelValuesDisplay[2]); } else if (m_d->model == ColorModel::HSL) { /* * HSLToRGB can give negative values on the grey. I fixed the fromNormalisedChannel function to clamp, * but you might want to manually clamp for floating point values. */ QVector inbetween(3); RGBToHSL(channelValuesDisplay[0],channelValuesDisplay[1], channelValuesDisplay[2], &inbetween[0], &inbetween[1], &inbetween[2]); inbetween = convertvectorqrealTofloat(getHSX(convertvectorfloatToqreal(inbetween))); inbetween[m_d->channel1] = fmod(coordinates.x()*huedivider, 360.0); if (m_d->dimension == Dimensions::twodimensional) { inbetween[m_d->channel2] = coordinates.y()*huedivider2; } if (cursor) { setHSX(convertvectorfloatToqreal(inbetween)); Q_EMIT sigHSXchange(); } HSLToRGB(qMax(inbetween[0], (float)0.0), inbetween[1], inbetween[2], &channelValuesDisplay[0], &channelValuesDisplay[1], &channelValuesDisplay[2]); } else if (m_d->model == ColorModel::HSI) { /* * HSI is a modified HSY function. */ QVector chan2 = convertvectorfloatToqreal(channelValuesDisplay); QVector inbetween(3); RGBToHSI(chan2[0],chan2[1], chan2[2], &inbetween[0], &inbetween[1], &inbetween[2]); inbetween = getHSX(inbetween); inbetween[m_d->channel1] = coordinates.x(); if (m_d->dimension == Dimensions::twodimensional) { inbetween[m_d->channel2] = coordinates.y(); } if (cursor) { setHSX(inbetween); Q_EMIT sigHSXchange(); } HSIToRGB(inbetween[0], inbetween[1], inbetween[2],&chan2[0],&chan2[1], &chan2[2]); channelValuesDisplay = convertvectorqrealTofloat(chan2); } else /*if (m_d->model == ColorModel::HSY)*/ { /* * HSY is pretty slow to render due being a pretty over-the-top function. * Might be worth investigating whether HCY can be used instead, but I have had * some weird results with that. */ QVector luma= m_d->colorSpace->lumaCoefficients(); QVector chan2 = convertvectorfloatToqreal(channelValuesDisplay); QVector inbetween(3); RGBToHSY(chan2[0],chan2[1], chan2[2], &inbetween[0], &inbetween[1], &inbetween[2], luma[0], luma[1], luma[2]); inbetween = getHSX(inbetween); inbetween[m_d->channel1] = coordinates.x(); if (m_d->dimension == Dimensions::twodimensional) { inbetween[m_d->channel2] = coordinates.y(); } if (cursor) { setHSX(inbetween); Q_EMIT sigHSXchange(); } HSYToRGB(inbetween[0], inbetween[1], inbetween[2],&chan2[0],&chan2[1], &chan2[2], luma[0], luma[1], luma[2]); channelValuesDisplay = convertvectorqrealTofloat(chan2); } } else { channelValuesDisplay[m_d->channel1] = coordinates.x(); if (m_d->dimension == Dimensions::twodimensional) { channelValuesDisplay[m_d->channel2] = coordinates.y(); } } for (int i=0; icolorSpace->channels())]*(maxvalue[i]); } c.colorSpace()->fromNormalisedChannelsValue(c.data(), channelValues); return c; } QPointF KisVisualColorSelectorShape::convertKoColorToShapeCoordinate(KoColor c) { ////qDebug() << this << ">>>>>>>>> convertKoColorToShapeCoordinate()"; if (c.colorSpace() != m_d->colorSpace) { c.convertTo(m_d->colorSpace); } QVector channelValues (m_d->currentColor.colorSpace()->channelCount()); channelValues.fill(1.0); m_d->colorSpace->normalisedChannelsValue(c.data(), channelValues); QVector channelValuesDisplay = channelValues; QVector maxvalue(c.colorSpace()->channelCount()); maxvalue.fill(1.0); - if (m_d->displayRenderer - && (m_d->colorSpace->colorDepthId() == Float16BitsColorDepthID - || m_d->colorSpace->colorDepthId() == Float32BitsColorDepthID - || m_d->colorSpace->colorDepthId() == Float64BitsColorDepthID) - && m_d->colorSpace->colorModelId() != LABAColorModelID - && m_d->colorSpace->colorModelId() != CMYKAColorModelID) { + if (m_d->usesOCIO == true) { for (int ch = 0; chcolorSpace->channels()[ch]; maxvalue[ch] = m_d->displayRenderer->maxVisibleFloatValue(channel); channelValues[ch] = channelValues[ch]/(maxvalue[ch]); channelValuesDisplay[KoChannelInfo::displayPositionToChannelIndex(ch, m_d->colorSpace->channels())] = channelValues[ch]; } } else { for (int i =0; icolorSpace->channels())] = qBound((float)0.0,channelValues[i], (float)1.0); } } QPointF coordinates(0.0,0.0); qreal huedivider = 1.0; qreal huedivider2 = 1.0; if (m_d->channel1==0) { huedivider = 360.0; } if (m_d->channel2==0) { huedivider2 = 360.0; } - if (m_d->model != ColorModel::Channel && c.colorSpace()->colorModelId().id() == "RGBA") { - if (c.colorSpace()->colorModelId().id() == "RGBA") { + if (m_d->model != ColorModel::Channel && m_d->isRGBA == true) { + if (m_d->isRGBA == true) { if (m_d->model == ColorModel::HSV){ QVector inbetween(3); RGBToHSV(channelValuesDisplay[0],channelValuesDisplay[1], channelValuesDisplay[2], &inbetween[0], &inbetween[1], &inbetween[2]); inbetween = convertvectorqrealTofloat(getHSX(convertvectorfloatToqreal(inbetween))); coordinates.setX(inbetween[m_d->channel1]/huedivider); if (m_d->dimension == Dimensions::twodimensional) { coordinates.setY(inbetween[m_d->channel2]/huedivider2); } } else if (m_d->model == ColorModel::HSL) { QVector inbetween(3); RGBToHSL(channelValuesDisplay[0],channelValuesDisplay[1], channelValuesDisplay[2], &inbetween[0], &inbetween[1], &inbetween[2]); inbetween = convertvectorqrealTofloat(getHSX(convertvectorfloatToqreal(inbetween))); coordinates.setX(inbetween[m_d->channel1]/huedivider); if (m_d->dimension == Dimensions::twodimensional) { coordinates.setY(inbetween[m_d->channel2]/huedivider2); } } else if (m_d->model == ColorModel::HSI) { QVector chan2 = convertvectorfloatToqreal(channelValuesDisplay); QVector inbetween(3); RGBToHSI(channelValuesDisplay[0],channelValuesDisplay[1], channelValuesDisplay[2], &inbetween[0], &inbetween[1], &inbetween[2]); inbetween = getHSX(inbetween); coordinates.setX(inbetween[m_d->channel1]); if (m_d->dimension == Dimensions::twodimensional) { coordinates.setY(inbetween[m_d->channel2]); } } else if (m_d->model == ColorModel::HSY) { QVector luma = m_d->colorSpace->lumaCoefficients(); QVector chan2 = convertvectorfloatToqreal(channelValuesDisplay); QVector inbetween(3); RGBToHSY(channelValuesDisplay[0],channelValuesDisplay[1], channelValuesDisplay[2], &inbetween[0], &inbetween[1], &inbetween[2], luma[0], luma[1], luma[2]); inbetween = getHSX(inbetween); coordinates.setX(inbetween[m_d->channel1]); if (m_d->dimension == Dimensions::twodimensional) { coordinates.setY(inbetween[m_d->channel2]); } } } } else { coordinates.setX(qBound((float)0.0, channelValuesDisplay[m_d->channel1], (float)1.0)); if (m_d->dimension == Dimensions::twodimensional) { coordinates.setY(qBound((float)0.0, channelValuesDisplay[m_d->channel2], (float)1.0)); } } return coordinates; } QVector KisVisualColorSelectorShape::convertvectorqrealTofloat(QVector real) { QVector vloat(real.size()); for (int i=0; i KisVisualColorSelectorShape::convertvectorfloatToqreal(QVector vloat) { QVector real(vloat.size()); for (int i=0; ibutton()==Qt::LeftButton) { m_d->mousePressActive = true; QPointF coordinates = convertWidgetCoordinateToShapeCoordinate(e->pos()); KoColor col = convertShapeCoordinateToKoColor(coordinates, true); setColor(col); Q_EMIT sigNewColor(col); m_d->updateTimer->start(); } } void KisVisualColorSelectorShape::mouseMoveEvent(QMouseEvent *e) { if (m_d->mousePressActive==true && this->mask().contains(e->pos())) { QPointF coordinates = convertWidgetCoordinateToShapeCoordinate(e->pos()); quint8* oldData = m_d->currentColor.data(); KoColor col = convertShapeCoordinateToKoColor(coordinates, true); QRect offsetrect(this->geometry().topLeft()+QPoint(7.0,7.0), this->geometry().bottomRight()-QPoint(7.0,7.0)); if (offsetrect.contains(e->pos()) || (m_d->colorSpace->difference(col.data(), oldData)>5)) { setColor(col); if (!m_d->updateTimer->isActive()) { Q_EMIT sigNewColor(col); m_d->updateTimer->start(); } } } else { e->ignore(); } } void KisVisualColorSelectorShape::mouseReleaseEvent(QMouseEvent *) { m_d->mousePressActive = false; } void KisVisualColorSelectorShape::paintEvent(QPaintEvent*) { QPainter painter(this); //check if old and new colors differ. if (m_d->imagesNeedUpdate) { setMask(getMaskMap()); } drawCursor(); painter.drawImage(0,0,m_d->fullSelector); } KisVisualColorSelectorShape::Dimensions KisVisualColorSelectorShape::getDimensions() { return m_d->dimension; } KisVisualColorSelectorShape::ColorModel KisVisualColorSelectorShape::getColorModel() { return m_d->model; } void KisVisualColorSelectorShape::setFullImage(QImage full) { m_d->fullSelector = full; } KoColor KisVisualColorSelectorShape::getCurrentColor() { return m_d->currentColor; } QVector KisVisualColorSelectorShape::getHSX(QVector hsx, bool wrangler) { QVector ihsx = hsx; if (!wrangler){ //Ok, so this docker will not update luminosity if there's not at the least 3% more variation. //This is necessary for 8bit. - if (m_d->colorSpace->colorDepthId()==Integer8BitsColorDepthID){ + if (m_d->is8Bit == true){ if (hsx[2]>m_d->tone-0.03 && hsx[2]tone+0.03) { ihsx[2] = m_d->tone; } } else { if (hsx[2]>m_d->tone-0.005 && hsx[2]tone+0.005) { ihsx[2] = m_d->tone; } } if (m_d->model==HSV){ if (hsx[2]<=0.0) { ihsx[1] = m_d->sat; } } else { if ((hsx[2]<=0.0 || hsx[2]>=1.0)) { ihsx[1] = m_d->sat; } } if ((hsx[1]<=0.0 || hsx[0]<0.0)){ ihsx[0]=m_d->hue; } } else { ihsx[0]=m_d->hue; ihsx[1]=m_d->sat; ihsx[2]=m_d->tone; } return ihsx; } void KisVisualColorSelectorShape::setHSX(QVector hsx, bool wrangler) { if (wrangler){ m_d->tone = hsx[2]; m_d->sat = hsx[1]; m_d->hue = hsx[0]; } else { if (m_d->channel1==2 || m_d->channel2==2){ m_d->tone=hsx[2]; } if (m_d->model==HSV){ if (hsx[2]>0.0) { m_d->sat = hsx[1]; } } else { if ((hsx[2]>0.0 || hsx[2]<1.0)) { m_d->sat = hsx[1]; } } if ((hsx[1]>0.0 && hsx[0]>=0.0)){ m_d->hue = hsx[0]; } } } QVector KisVisualColorSelectorShape::getChannels() { QVector channels(2); channels[0] = m_d->channel1; channels[1] = m_d->channel2; return channels; } diff --git a/libs/ui/widgets/KisVisualEllipticalSelectorShape.cpp b/libs/ui/widgets/KisVisualEllipticalSelectorShape.cpp index c70f6d2480..66a6ff7ae3 100644 --- a/libs/ui/widgets/KisVisualEllipticalSelectorShape.cpp +++ b/libs/ui/widgets/KisVisualEllipticalSelectorShape.cpp @@ -1,240 +1,244 @@ /* * Copyright (C) Wolthera van Hovell tot Westerflier , (C) 2016 * * 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 "KisVisualEllipticalSelectorShape.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include "KoColorConversions.h" #include "KoColorDisplayRendererInterface.h" #include "KoChannelInfo.h" #include #include #include "kis_signal_compressor.h" #include "kis_debug.h" +#include "kis_global.h" KisVisualEllipticalSelectorShape::KisVisualEllipticalSelectorShape(QWidget *parent, Dimensions dimension, ColorModel model, const KoColorSpace *cs, int channel1, int channel2, const KoColorDisplayRendererInterface *displayRenderer, int barWidth, singelDTypes d) : KisVisualColorSelectorShape(parent, dimension, model, cs, channel1, channel2, displayRenderer) { //qDebug() << "creating KisVisualEllipticalSelectorShape" << this; m_type = d; m_barWidth = barWidth; } KisVisualEllipticalSelectorShape::~KisVisualEllipticalSelectorShape() { //qDebug() << "deleting KisVisualEllipticalSelectorShape" << this; } QSize KisVisualEllipticalSelectorShape::sizeHint() const { return QSize(180,180); } void KisVisualEllipticalSelectorShape::setBorderWidth(int width) { m_barWidth = width; } QRect KisVisualEllipticalSelectorShape::getSpaceForSquare(QRect geom) { int sizeValue = qMin(width(),height()); QRect b(geom.left(), geom.top(), sizeValue, sizeValue); QLineF radius(b.center(), QPointF(b.left()+m_barWidth, b.center().y()) ); radius.setAngle(135); QPointF tl = radius.p2(); radius.setAngle(315); QPointF br = radius.p2(); QRect r(tl.toPoint(), br.toPoint()); return r; } QRect KisVisualEllipticalSelectorShape::getSpaceForCircle(QRect geom) { int sizeValue = qMin(width(),height()); QRect b(geom.left(), geom.top(), sizeValue, sizeValue); QPointF tl = QPointF (b.topLeft().x()+m_barWidth, b.topLeft().y()+m_barWidth); QPointF br = QPointF (b.bottomRight().x()-m_barWidth, b.bottomRight().y()-m_barWidth); QRect r(tl.toPoint(), br.toPoint()); return r; } QRect KisVisualEllipticalSelectorShape::getSpaceForTriangle(QRect geom) { int sizeValue = qMin(width(),height()); QRect b(geom.left(), geom.top(), sizeValue, sizeValue); QLineF radius(b.center(), QPointF(b.left()+m_barWidth, b.center().y()) ); radius.setAngle(90);//point at yellowgreen :) QPointF t = radius.p2(); radius.setAngle(330);//point to purple :) QPointF br = radius.p2(); radius.setAngle(210);//point to cerulean :) QPointF bl = radius.p2(); QPointF tl = QPoint(bl.x(),t.y()); QRect r(tl.toPoint(), br.toPoint()); return r; } QPointF KisVisualEllipticalSelectorShape::convertShapeCoordinateToWidgetCoordinate(QPointF coordinate) { qreal x; qreal y; qreal offset=7.0; qreal a = (qreal)width()*0.5; QPointF center(a, a); QLineF line(center, QPoint((m_barWidth*0.5),a)); qreal angle = coordinate.x()*360.0; angle = fmod(angle+180.0,360.0); angle = 180.0-angle; angle = angle+180.0; if (m_type==KisVisualEllipticalSelectorShape::borderMirrored) { angle = (coordinate.x()/2)*360.0; - angle = fmod((angle+90.0), 360.0); + angle = fmod((angle+270.0), 360.0); } line.setAngle(angle); if (getDimensions()!=KisVisualColorSelectorShape::onedimensional) { line.setLength(qMin(coordinate.y()*(a-offset), a-offset)); } x = qRound(line.p2().x()); y = qRound(line.p2().y()); return QPointF(x,y); } QPointF KisVisualEllipticalSelectorShape::convertWidgetCoordinateToShapeCoordinate(QPoint coordinate) { //default implementation: qreal x = 0.5; qreal y = 1.0; qreal offset = 7.0; - QRect total(0, 0, width(), height()); - QLineF line(total.center(), coordinate); - qreal a = (total.width()/2); - qreal angle; + QPointF center = QRectF(QPointF(0.0, 0.0), this->size()).center(); + qreal a = (this->width()/2); + qreal xRel = center.x()-coordinate.x(); + qreal yRel = center.y()-coordinate.y(); + qreal angle = atan2(xRel, yRel); + qreal radius = sqrt(xRel*xRel+yRel*yRel); + angle = kisRadiansToDegrees(angle); if (m_type!=KisVisualEllipticalSelectorShape::borderMirrored){ - angle = fmod((line.angle()+180.0), 360.0); + angle = fmod(angle-90, 360.0); angle = 180.0-angle; angle = angle+180.0; x = angle/360.0; if (getDimensions()==KisVisualColorSelectorShape::twodimensional) { - y = qBound(0.0,line.length()/(a-offset), 1.0); + y = qBound(0.0,radius/(a-offset), 1.0); } } else { - angle = fmod((line.angle()+270.0), 360.0); + angle = fmod(angle+180, 360.0); if (angle>180.0) { angle = 180.0-angle; angle = angle+180; } x = (angle/360.0)*2; if (getDimensions()==KisVisualColorSelectorShape::twodimensional) { - y = qBound(0.0,(line.length()+offset)/a, 1.0); + y = qBound(0.0,(radius+offset)/a, 1.0); } } return QPointF(x, y); } QRegion KisVisualEllipticalSelectorShape::getMaskMap() { QRegion mask = QRegion(0,0,width(),height(), QRegion::Ellipse); if (getDimensions()==KisVisualColorSelectorShape::onedimensional) { mask = mask.subtracted(QRegion(m_barWidth, m_barWidth, width()-(m_barWidth*2), height()-(m_barWidth*2), QRegion::Ellipse)); } return mask; } void KisVisualEllipticalSelectorShape::resizeEvent(QResizeEvent *) { //qDebug() << this << "KisVisualEllipticalSelectorShape::resizeEvent"; forceImageUpdate(); } void KisVisualEllipticalSelectorShape::drawCursor() { //qDebug() << this << "KisVisualEllipticalSelectorShape::drawCursor: image needs update" << imagesNeedUpdate(); QPointF cursorPoint = convertShapeCoordinateToWidgetCoordinate(getCursorPosition()); QImage fullSelector = getImageMap(); QColor col = getColorFromConverter(getCurrentColor()); QPainter painter; painter.begin(&fullSelector); painter.setRenderHint(QPainter::Antialiasing); QRect innerRect(m_barWidth, m_barWidth, width()-(m_barWidth*2), height()-(m_barWidth*2)); painter.save(); painter.setCompositionMode(QPainter::CompositionMode_Clear); QPen pen; pen.setWidth(5); painter.setPen(pen); painter.drawEllipse(QRect(0,0,width(),height())); if (getDimensions()==KisVisualColorSelectorShape::onedimensional) { painter.setBrush(Qt::SolidPattern); painter.drawEllipse(innerRect); } painter.restore(); QBrush fill; fill.setStyle(Qt::SolidPattern); int cursorwidth = 5; if (m_type==KisVisualEllipticalSelectorShape::borderMirrored) { painter.setPen(Qt::white); fill.setColor(Qt::white); painter.setBrush(fill); painter.drawEllipse(cursorPoint, cursorwidth, cursorwidth); QPoint mirror(innerRect.center().x()+(innerRect.center().x()-cursorPoint.x()),cursorPoint.y()); painter.drawEllipse(mirror, cursorwidth, cursorwidth); fill.setColor(col); painter.setPen(Qt::black); painter.setBrush(fill); painter.drawEllipse(cursorPoint, cursorwidth-1, cursorwidth-1); painter.drawEllipse(mirror, cursorwidth-1, cursorwidth-1); } else { painter.setPen(Qt::white); fill.setColor(Qt::white); painter.setBrush(fill); painter.drawEllipse(cursorPoint, cursorwidth, cursorwidth); fill.setColor(col); painter.setPen(Qt::black); painter.setBrush(fill); painter.drawEllipse(cursorPoint, cursorwidth-1.0, cursorwidth-1.0); } painter.end(); setFullImage(fullSelector); } diff --git a/libs/ui/widgets/KisVisualTriangleSelectorShape.cpp b/libs/ui/widgets/KisVisualTriangleSelectorShape.cpp index a47d068b60..e1284c4f17 100644 --- a/libs/ui/widgets/KisVisualTriangleSelectorShape.cpp +++ b/libs/ui/widgets/KisVisualTriangleSelectorShape.cpp @@ -1,202 +1,207 @@ /* * Copyright (C) Wolthera van Hovell tot Westerflier , (C) 2016 * * 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 "KisVisualTriangleSelectorShape.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include "KoColorConversions.h" #include "KoColorDisplayRendererInterface.h" #include "KoChannelInfo.h" #include #include #include "kis_signal_compressor.h" #include "kis_debug.h" +#include "kis_global.h" KisVisualTriangleSelectorShape::KisVisualTriangleSelectorShape(QWidget *parent, Dimensions dimension, ColorModel model, const KoColorSpace *cs, int channel1, int channel2, const KoColorDisplayRendererInterface *displayRenderer, int barwidth) : KisVisualColorSelectorShape(parent, dimension, model, cs, channel1, channel2, displayRenderer) { //qDebug() << "creating KisVisualTriangleSelectorShape" << this; m_barWidth = barwidth; setTriangle(); } KisVisualTriangleSelectorShape::~KisVisualTriangleSelectorShape() { //qDebug() << "deleting KisVisualTriangleSelectorShape" << this; } void KisVisualTriangleSelectorShape::setBorderWidth(int width) { m_barWidth = width; } QRect KisVisualTriangleSelectorShape::getSpaceForSquare(QRect geom) { return geom; } QRect KisVisualTriangleSelectorShape::getSpaceForCircle(QRect geom) { return geom; } QRect KisVisualTriangleSelectorShape::getSpaceForTriangle(QRect geom) { return geom; } void KisVisualTriangleSelectorShape::setTriangle() { QPoint apex = QPoint (width()*0.5,0); QPolygon triangle; triangle<< QPoint(0,height()) << apex << QPoint(width(),height()) << QPoint(0,height()); m_triangle = triangle; QLineF a(triangle.at(0),triangle.at(1)); QLineF b(triangle.at(0),triangle.at(2)); QLineF ap(triangle.at(2), a.pointAt(0.5)); QLineF bp(triangle.at(1), b.pointAt(0.5)); QPointF intersect; ap.intersect(bp,&intersect); m_center = intersect; QLineF r(triangle.at(0), intersect); m_radius = r.length(); } QPointF KisVisualTriangleSelectorShape::convertShapeCoordinateToWidgetCoordinate(QPointF coordinate) { qreal offset=7.0;//the offset is so we get a nice little border that allows selecting extreme colors better. - qreal y = qMin(coordinate.y()*(height()-offset*2-5.0)+offset+5.0, (qreal)height()-offset); + qreal yOffset = (cos(kisDegreesToRadians(30))*offset)*2; + qreal xOffset = qFloor(sin(kisDegreesToRadians(30))*offset); + qreal y = qMax(qMin((coordinate.y()*(height()-yOffset-offset))+yOffset, (qreal)height()-offset),yOffset); qreal triWidth = width(); - qreal horizontalLineLength = y*(2./sqrt(3.)); - qreal horizontalLineStart = triWidth/2.-horizontalLineLength/2.; - qreal relativeX = coordinate.x()*(horizontalLineLength-offset*2); - qreal x = qMin(relativeX + horizontalLineStart + offset, (qreal)width()-offset*2); - if (y * Copyright (C) 2005 C. Boemann * Copyright (C) 2007 Boudewijn Rempt * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "widgets/kis_custom_image_widget.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kis_config.h" #include "KisPart.h" #include "kis_clipboard.h" #include "KisDocument.h" #include "widgets/kis_cmb_idlist.h" #include "widgets/squeezedcombobox.h" KisCustomImageWidget::KisCustomImageWidget(QWidget* parent, qint32 defWidth, qint32 defHeight, double resolution, const QString& defColorModel, const QString& defColorDepth, const QString& defColorProfile, const QString& imageName) : WdgNewImage(parent) { setObjectName("KisCustomImageWidget"); m_openPane = qobject_cast(parent); Q_ASSERT(m_openPane); txtName->setText(imageName); m_widthUnit = KoUnit(KoUnit::Pixel, resolution); doubleWidth->setValue(defWidth); doubleWidth->setDecimals(0); m_width = m_widthUnit.fromUserValue(defWidth); cmbWidthUnit->addItems(KoUnit::listOfUnitNameForUi(KoUnit::ListAll)); cmbWidthUnit->setCurrentIndex(m_widthUnit.indexInListForUi(KoUnit::ListAll)); m_heightUnit = KoUnit(KoUnit::Pixel, resolution); doubleHeight->setValue(defHeight); doubleHeight->setDecimals(0); m_height = m_heightUnit.fromUserValue(defHeight); cmbHeightUnit->addItems(KoUnit::listOfUnitNameForUi(KoUnit::ListAll)); cmbHeightUnit->setCurrentIndex(m_heightUnit.indexInListForUi(KoUnit::ListAll)); doubleResolution->setValue(72.0 * resolution); doubleResolution->setDecimals(0); imageGroupSpacer->changeSize(0, 0, QSizePolicy::Fixed, QSizePolicy::Fixed); grpClipboard->hide(); sliderOpacity->setRange(0, 100, 0); sliderOpacity->setValue(100); sliderOpacity->setSuffix("%"); connect(cmbPredefined, SIGNAL(activated(int)), SLOT(predefinedClicked(int))); connect(doubleResolution, SIGNAL(valueChanged(double)), this, SLOT(resolutionChanged(double))); connect(cmbWidthUnit, SIGNAL(activated(int)), this, SLOT(widthUnitChanged(int))); connect(doubleWidth, SIGNAL(valueChanged(double)), this, SLOT(widthChanged(double))); connect(cmbHeightUnit, SIGNAL(activated(int)), this, SLOT(heightUnitChanged(int))); connect(doubleHeight, SIGNAL(valueChanged(double)), this, SLOT(heightChanged(double))); connect(createButton, SIGNAL(clicked()), this, SLOT(createImage())); createButton->setDefault(true); bnPortrait->setIcon(KisIconUtils::loadIcon("portrait")); connect(bnPortrait, SIGNAL(clicked()), SLOT(setPortrait())); connect(bnLandscape, SIGNAL(clicked()), SLOT(setLandscape())); bnLandscape->setIcon(KisIconUtils::loadIcon("landscape")); connect(doubleWidth, SIGNAL(valueChanged(double)), this, SLOT(switchPortraitLandscape())); connect(doubleHeight, SIGNAL(valueChanged(double)), this, SLOT(switchPortraitLandscape())); connect(bnSaveAsPredefined, SIGNAL(clicked()), this, SLOT(saveAsPredefined())); colorSpaceSelector->setCurrentColorModel(KoID(defColorModel)); colorSpaceSelector->setCurrentColorDepth(KoID(defColorDepth)); colorSpaceSelector->setCurrentProfile(defColorProfile); + connect(colorSpaceSelector, SIGNAL(colorSpaceChanged(const KoColorSpace*)), this, SLOT(changeDocumentInfoLabel())); //connect(chkFromClipboard,SIGNAL(stateChanged(int)),this,SLOT(clipboardDataChanged())); connect(QApplication::clipboard(), SIGNAL(dataChanged()), this, SLOT(clipboardDataChanged())); connect(QApplication::clipboard(), SIGNAL(selectionChanged()), this, SLOT(clipboardDataChanged())); connect(QApplication::clipboard(), SIGNAL(changed(QClipboard::Mode)), this, SLOT(clipboardDataChanged())); connect(colorSpaceSelector, SIGNAL(selectionChanged(bool)), createButton, SLOT(setEnabled(bool))); KisConfig cfg; intNumLayers->setValue(cfg.numDefaultLayers()); KoColor bcol(KoColorSpaceRegistry::instance()->rgb8()); bcol.fromQColor(cfg.defaultBackgroundColor()); cmbColor->setColor(bcol); setBackgroundOpacity(cfg.defaultBackgroundOpacity()); KisConfig::BackgroundStyle bgStyle = cfg.defaultBackgroundStyle(); if (bgStyle == KisConfig::LAYER) { radioBackgroundAsLayer->setChecked(true); } else { radioBackgroundAsProjection->setChecked(true); } fillPredefined(); switchPortraitLandscape(); // this makes the portrait and landscape buttons more // obvious what is selected by changing the higlight color QPalette p = QApplication::palette(); QPalette palette_highlight(p ); QColor c = p.color(QPalette::Highlight); palette_highlight.setColor(QPalette::Button, c); bnLandscape->setPalette(palette_highlight); bnPortrait->setPalette(palette_highlight); + changeDocumentInfoLabel(); } void KisCustomImageWidget::showEvent(QShowEvent *) { fillPredefined(); this->createButton->setFocus(); this->createButton->setEnabled(true); } KisCustomImageWidget::~KisCustomImageWidget() { m_predefined.clear(); } void KisCustomImageWidget::resolutionChanged(double res) { if (m_widthUnit.type() == KoUnit::Pixel) { m_widthUnit.setFactor(res / 72.0); m_width = m_widthUnit.fromUserValue(doubleWidth->value()); } if (m_heightUnit.type() == KoUnit::Pixel) { m_heightUnit.setFactor(res / 72.0); m_height = m_heightUnit.fromUserValue(doubleHeight->value()); } + changeDocumentInfoLabel(); } void KisCustomImageWidget::widthUnitChanged(int index) { doubleWidth->blockSignals(true); m_widthUnit = KoUnit::fromListForUi(index, KoUnit::ListAll); if (m_widthUnit.type() == KoUnit::Pixel) { doubleWidth->setDecimals(0); m_widthUnit.setFactor(doubleResolution->value() / 72.0); } else { doubleWidth->setDecimals(2); } doubleWidth->setValue(KoUnit::ptToUnit(m_width, m_widthUnit)); doubleWidth->blockSignals(false); + changeDocumentInfoLabel(); } void KisCustomImageWidget::widthChanged(double value) { m_width = m_widthUnit.fromUserValue(value); + changeDocumentInfoLabel(); } void KisCustomImageWidget::heightUnitChanged(int index) { doubleHeight->blockSignals(true); m_heightUnit = KoUnit::fromListForUi(index, KoUnit::ListAll); if (m_heightUnit.type() == KoUnit::Pixel) { doubleHeight->setDecimals(0); m_heightUnit.setFactor(doubleResolution->value() / 72.0); } else { doubleHeight->setDecimals(2); } doubleHeight->setValue(KoUnit::ptToUnit(m_height, m_heightUnit)); doubleHeight->blockSignals(false); + changeDocumentInfoLabel(); } void KisCustomImageWidget::heightChanged(double value) { m_height = m_heightUnit.fromUserValue(value); + changeDocumentInfoLabel(); } void KisCustomImageWidget::createImage() { createButton->setEnabled(false); KisDocument *doc = createNewImage(); if (doc) { doc->setModified(false); emit m_openPane->documentSelected(doc); } } KisDocument* KisCustomImageWidget::createNewImage() { const KoColorSpace * cs = colorSpaceSelector->currentColorSpace(); if (cs->colorModelId() == RGBAColorModelID && cs->colorDepthId() == Integer8BitsColorDepthID) { const KoColorProfile *profile = cs->profile(); if (profile->name().contains("linear") || profile->name().contains("scRGB") || profile->info().contains("linear") || profile->info().contains("scRGB")) { int result = QMessageBox::warning(this, i18nc("@title:window", "Krita"), i18n("Linear gamma RGB color spaces are not supposed to be used " "in 8-bit integer modes. It is suggested to use 16-bit integer " "or any floating point colorspace for linear profiles.\n\n" "Press \"Continue\" to create a 8-bit integer linear RGB color space " "or \"Cancel\" to return to the settings dialog."), QMessageBox::Ok | QMessageBox::Cancel, QMessageBox::Cancel); if (result == QMessageBox::Cancel) { dbgKrita << "Model RGB8" << "NOT SUPPORTED"; dbgKrita << ppVar(cs->name()); dbgKrita << ppVar(cs->profile()->name()); dbgKrita << ppVar(cs->profile()->info()); return 0; } } } KisDocument *doc = static_cast(KisPart::instance()->createDocument()); qint32 width, height; double resolution; resolution = doubleResolution->value() / 72.0; // internal resolution is in pixels per pt width = static_cast(0.5 + KoUnit::ptToUnit(m_width, KoUnit(KoUnit::Pixel, resolution))); height = static_cast(0.5 + KoUnit::ptToUnit(m_height, KoUnit(KoUnit::Pixel, resolution))); QColor qc = cmbColor->color().toQColor(); qc.setAlpha(backgroundOpacity()); KoColor bgColor(qc, cs); bool backgroundAsLayer = radioBackgroundAsLayer->isChecked(); doc->newImage(txtName->text(), width, height, cs, bgColor, backgroundAsLayer, intNumLayers->value(), txtDescription->toPlainText(), resolution); KisConfig cfg; cfg.setNumDefaultLayers(intNumLayers->value()); cfg.setDefaultBackgroundOpacity(backgroundOpacity()); cfg.setDefaultBackgroundColor(cmbColor->color().toQColor()); cfg.setDefaultBackgroundStyle(backgroundAsLayer ? KisConfig::LAYER : KisConfig::PROJECTION); return doc; } void KisCustomImageWidget::setNumberOfLayers(int layers) { intNumLayers->setValue(layers); } quint8 KisCustomImageWidget::backgroundOpacity() const { qint32 opacity = sliderOpacity->value(); if (!opacity) return 0; return (opacity * 255) / 100; } void KisCustomImageWidget::setBackgroundOpacity(quint8 value) { sliderOpacity->setValue((value * 100) / 255); } void KisCustomImageWidget::clipboardDataChanged() { } void KisCustomImageWidget::fillPredefined() { cmbPredefined->clear(); m_predefined.clear(); cmbPredefined->addItem(""); QStringList definitions = KoResourcePaths::findAllResources("data", "predefined_image_sizes/*.predefinedimage", KoResourcePaths::Recursive); definitions.sort(); if (!definitions.empty()) { Q_FOREACH (const QString &definition, definitions) { QFile f(definition); f.open(QIODevice::ReadOnly); if (f.exists()) { QString xml = QString::fromUtf8(f.readAll()); KisPropertiesConfigurationSP predefined = new KisPropertiesConfiguration; predefined->fromXML(xml); if (predefined->hasProperty("name") && predefined->hasProperty("width") && predefined->hasProperty("height") && predefined->hasProperty("resolution") && predefined->hasProperty("x-unit") && predefined->hasProperty("y-unit")) { m_predefined << predefined; cmbPredefined->addItem(predefined->getString("name")); } } } } cmbPredefined->setCurrentIndex(0); } void KisCustomImageWidget::predefinedClicked(int index) { if (index < 1 || index > m_predefined.size()) return; KisPropertiesConfigurationSP predefined = m_predefined[index - 1]; txtPredefinedName->setText(predefined->getString("name")); doubleResolution->setValue(predefined->getDouble("resolution")); cmbWidthUnit->setCurrentIndex(predefined->getInt("x-unit")); cmbHeightUnit->setCurrentIndex(predefined->getInt("y-unit")); widthUnitChanged(cmbWidthUnit->currentIndex()); heightUnitChanged(cmbHeightUnit->currentIndex()); doubleWidth->setValue(predefined->getDouble("width")); doubleHeight->setValue(predefined->getDouble("height")); + changeDocumentInfoLabel(); } void KisCustomImageWidget::saveAsPredefined() { QString fileName = txtPredefinedName->text(); if (fileName.isEmpty()) { return; } QString saveLocation = KoResourcePaths::saveLocation("data", "predefined_image_sizes/", true); QFile f(saveLocation + '/' + fileName.replace(' ', '_').replace('(', '_').replace(')', '_').replace(':', '_') + ".predefinedimage"); f.open(QIODevice::WriteOnly | QIODevice::Truncate); KisPropertiesConfigurationSP predefined = new KisPropertiesConfiguration(); predefined->setProperty("name", txtPredefinedName->text()); predefined->setProperty("width", doubleWidth->value()); predefined->setProperty("height", doubleHeight->value()); predefined->setProperty("resolution", doubleResolution->value()); predefined->setProperty("x-unit", cmbWidthUnit->currentIndex()); predefined->setProperty("y-unit", cmbHeightUnit->currentIndex()); QString xml = predefined->toXML(); f.write(xml.toUtf8()); f.flush(); f.close(); int i = 0; bool found = false; Q_FOREACH (KisPropertiesConfigurationSP pr, m_predefined) { if (pr->getString("name") == txtPredefinedName->text()) { found = true; break; } ++i; } if (found) { m_predefined[i] = predefined; } else { m_predefined.append(predefined); cmbPredefined->addItem(txtPredefinedName->text()); } } void KisCustomImageWidget::setLandscape() { if (doubleWidth->value() < doubleHeight->value()) { switchWidthHeight(); } } void KisCustomImageWidget::setPortrait() { if (doubleWidth->value() > doubleHeight->value()) { switchWidthHeight(); } } void KisCustomImageWidget::switchWidthHeight() { double width = doubleWidth->value(); double height = doubleHeight->value(); doubleHeight->blockSignals(true); doubleWidth->blockSignals(true); cmbWidthUnit->blockSignals(true); cmbHeightUnit->blockSignals(true); doubleWidth->setValue(height); doubleHeight->setValue(width); cmbWidthUnit->setCurrentIndex(m_heightUnit.indexInListForUi(KoUnit::ListAll)); cmbHeightUnit->setCurrentIndex(m_widthUnit.indexInListForUi(KoUnit::ListAll)); doubleHeight->blockSignals(false); doubleWidth->blockSignals(false); cmbWidthUnit->blockSignals(false); cmbHeightUnit->blockSignals(false); switchPortraitLandscape(); widthChanged(doubleWidth->value()); heightChanged(doubleHeight->value()); + changeDocumentInfoLabel(); } void KisCustomImageWidget::switchPortraitLandscape() { if(doubleWidth->value() > doubleHeight->value()) bnLandscape->setChecked(true); else bnPortrait->setChecked(true); } +void KisCustomImageWidget::changeDocumentInfoLabel() +{ + int layerSize = doubleWidth->value()*doubleHeight->value(); + const KoColorSpace *cs = colorSpaceSelector->currentColorSpace(); + int bitSize = 8*cs->pixelSize(); //pixelsize is in bytes. + layerSize = layerSize*cs->pixelSize(); + QString byte = "bytes"; + if (layerSize>1000) { + layerSize/=1000; + byte = "kB"; + } + if (layerSize>1000) { + layerSize/=1000; + byte = "mB"; + } + if (layerSize>1000) { + layerSize/=1000; + byte = "gB"; + } + QString text = QString("This document will be %1x%2 px %4, which means the pixel size is %3 bit, a single paint layer will thus take up %5 %6 of ram.") + .arg(doubleWidth->value()) + .arg(doubleHeight->value()) + .arg(bitSize) + .arg(cs->name()) + .arg(layerSize) + .arg(byte); + lblDocumentInfo->setText(text); +} + diff --git a/libs/ui/widgets/kis_custom_image_widget.h b/libs/ui/widgets/kis_custom_image_widget.h index 48d7167756..e57f9ff2c6 100644 --- a/libs/ui/widgets/kis_custom_image_widget.h +++ b/libs/ui/widgets/kis_custom_image_widget.h @@ -1,100 +1,101 @@ /* This file is part of the Calligra project * Copyright (C) 2005 Thomas Zander * Copyright (C) 2005 C. Boemann * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KIS_CUSTOM_IMAGE_WIDGET_H #define KIS_CUSTOM_IMAGE_WIDGET_H #include "kis_global.h" #include "KoUnit.h" #include "kis_properties_configuration.h" #include "KisOpenPane.h" #include class KisDocument; class KisDocument; enum CustomImageWidgetType { CUSTOM_DOCUMENT, NEW_IMG_FROM_CB }; class WdgNewImage : public QWidget, public Ui::WdgNewImage { Q_OBJECT public: WdgNewImage(QWidget *parent) : QWidget(parent) { setupUi(this); } }; /** * The 'Custom Document' widget in the Krita startup widget. * This class embeds the image size and colorspace to allow the user to select the image properties * for a new empty image document. */ class KisCustomImageWidget : public WdgNewImage { Q_OBJECT public: /** * Constructor. Please note that this class is being used/created by KisDoc. * @param parent the parent widget * @param doc the document that wants to be altered */ KisCustomImageWidget(QWidget *parent, qint32 defWidth, qint32 defHeight, double resolution, const QString & defColorModel, const QString & defColorDepth, const QString & defColorProfile, const QString & imageName); ~KisCustomImageWidget() override; private Q_SLOTS: void widthUnitChanged(int index); void widthChanged(double value); void heightUnitChanged(int index); void heightChanged(double value); void resolutionChanged(double value); void clipboardDataChanged(); void predefinedClicked(int index); void saveAsPredefined(); void setLandscape(); void setPortrait(); void switchWidthHeight(); void createImage(); void switchPortraitLandscape(); + void changeDocumentInfoLabel(); protected: KisDocument *createNewImage(); /// Set the number of layers that will be created void setNumberOfLayers(int layers); KisOpenPane *m_openPane; private: double m_width, m_height; quint8 backgroundOpacity() const; void setBackgroundOpacity(quint8 value); void fillPredefined(); void showEvent(QShowEvent *) override; KoUnit m_widthUnit, m_heightUnit; QList m_predefined; }; #endif diff --git a/plugins/extensions/pykrita/plugin/plugins/CMakeLists.txt b/plugins/extensions/pykrita/plugin/plugins/CMakeLists.txt index 22983b8e2a..78d87465fc 100644 --- a/plugins/extensions/pykrita/plugin/plugins/CMakeLists.txt +++ b/plugins/extensions/pykrita/plugin/plugins/CMakeLists.txt @@ -1,105 +1,108 @@ # Copyright (C) 2012, 2013 Shaheed Haque # Copyright (C) 2013 Alex Turbov # Copyright (C) 2014-2016 Boudewijn Rempt # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions # are met: # # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES # OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. # IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT # NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. include(CMakeParseArguments) # # Simple helper function to install plugin and related files # having only a name of the plugin... # (just to reduce syntactic noise when a lot of plugins get installed) # function(install_pykrita_plugin name) set(_options) set(_one_value_args) set(_multi_value_args PATTERNS FILE) cmake_parse_arguments(install_pykrita_plugin "${_options}" "${_one_value_args}" "${_multi_value_args}" ${ARGN}) if(NOT name) message(FATAL_ERROR "Plugin filename is not given") endif() if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/${name}.py) install(FILES kritapykrita_${name}.desktop DESTINATION ${DATA_INSTALL_DIR}/krita/pykrita) foreach(_f ${name}.py ${name}.ui ${install_pykrita_plugin_FILE}) if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/${_f}) install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/${_f} DESTINATION ${DATA_INSTALL_DIR}/krita/pykrita) endif() endforeach() elseif(IS_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/${name}) install(FILES ${name}/kritapykrita_${name}.desktop DESTINATION ${DATA_INSTALL_DIR}/krita/pykrita) install( DIRECTORY ${name} DESTINATION ${DATA_INSTALL_DIR}/krita/pykrita FILES_MATCHING PATTERN "*.py" PATTERN "*.ui" + PATTERN "*.txt" + PATTERN "*.csv" PATTERN "__pycache__*" EXCLUDE ) # TODO Is there any way to form a long PATTERN options string # and use it in a single install() call? # NOTE Install specified patterns one-by-one... foreach(_pattern ${install_pykrita_plugin_PATTERNS}) install( DIRECTORY ${name} DESTINATION ${DATA_INSTALL_DIR}/krita/pykrita FILES_MATCHING PATTERN "${_pattern}" PATTERN "__pycache__*" EXCLUDE ) endforeach() else() message(FATAL_ERROR "Do not know what to do with ${name}") endif() endfunction() install_pykrita_plugin(hello) install_pykrita_plugin(assignprofiledialog) install_pykrita_plugin(scripter) install_pykrita_plugin(colorspace) install_pykrita_plugin(documenttools) install_pykrita_plugin(filtermanager) install_pykrita_plugin(exportlayers) #install_pykrita_plugin(highpass) install_pykrita_plugin(tenbrushes) install( FILES tenbrushes/tenbrushes.action DESTINATION ${DATA_INSTALL_DIR}/krita/actions) install_pykrita_plugin(palette_docker) install_pykrita_plugin(quick_settings_docker) install_pykrita_plugin(lastdocumentsdocker) install_pykrita_plugin(scriptdocker) +install_pykrita_plugin(comics_project_management_tools) # if(PYTHON_VERSION_MAJOR VERSION_EQUAL 3) # install_pykrita_plugin(cmake_utils) # install_pykrita_plugin(js_utils PATTERNS "*.json") # install_pykrita_plugin(expand PATTERNS "*.expand" "templates/*.tpl") # endif() install( DIRECTORY libkritapykrita DESTINATION ${DATA_INSTALL_DIR}/krita/pykrita FILES_MATCHING PATTERN "*.py" PATTERN "__pycache__*" EXCLUDE ) diff --git a/plugins/extensions/pykrita/plugin/plugins/comics_project_management_tools/.gitignore b/plugins/extensions/pykrita/plugin/plugins/comics_project_management_tools/.gitignore new file mode 100644 index 0000000000..b51e37e8f2 --- /dev/null +++ b/plugins/extensions/pykrita/plugin/plugins/comics_project_management_tools/.gitignore @@ -0,0 +1,4 @@ +__pycache__ +*pyc +.directory +README.md.backup diff --git a/plugins/extensions/pykrita/plugin/plugins/comics_project_management_tools/LicenseList.csv b/plugins/extensions/pykrita/plugin/plugins/comics_project_management_tools/LicenseList.csv new file mode 100644 index 0000000000..3ee29704b7 --- /dev/null +++ b/plugins/extensions/pykrita/plugin/plugins/comics_project_management_tools/LicenseList.csv @@ -0,0 +1,6 @@ +Proprietary,Default Copyright. +Other,Use for whenever the work falls under another license. +CC-BY-4.0,"Allows reuse, commercial and derivatives, as long as the original author is credited." +CC-BY-SA-4.0,"Allows reuse, commercial and derivatives, as long as the original author is credited and the derivatives are also licensed like this." +CC-BY-SA-NC-4.0,"Allows reuse and derivatives, as long as the original author is credited and the derivatives are also licensed like this, and no money is made off the work." +CC-0,Creative Commons Public Domain Dedication: Waive all rights on the work. diff --git a/plugins/extensions/pykrita/plugin/plugins/comics_project_management_tools/README.md b/plugins/extensions/pykrita/plugin/plugins/comics_project_management_tools/README.md new file mode 100644 index 0000000000..f71e43fd7b --- /dev/null +++ b/plugins/extensions/pykrita/plugin/plugins/comics_project_management_tools/README.md @@ -0,0 +1,185 @@ +Comics Project Management Tools +=============================== + +This is the Comics Project Management Tools python plugin for Krita. + +CPMT aims to simplify comics creation by: + +* Giving the artist a way to organize and quickly access their pages. +* Helping the artist(s) deal with the boring bits meta data bits of a comic project by giving a meta-data editor that gives suggestions, explanation and occasionally a dab of humor. +* Making export set-and-forget type of affair where a single click can export to multiple formats with proper meta-data. + +Export-wise, CPMT aims to support: + +* Advanced Comic Book Format - An open comics format that has detailed markup as well as support for translations. +* CBZ - the most popular comic file format, with the following meta-data schemes: + * ACBF - as above. + * CoMet.xml + * ComicBookInfo (Spec is unclear so not 100% certain) + * ComicInfo.xml(Comic Rack) +* Epub - The epub publishing format. Not the most ideal format for handling comics, but most readers can open epub. + +Usage - quick-start guide: +------------------------- + +First, get the comic manager docker(settings → dockers → comic Management Docker). There, select *New Project*. + +It will show a dialog asking for: + +* the project directory. This is where everything will be written to. +* a concept, so a simple sentence explaining what you want to write the comic about. This concept is just for you. +* a project name. This is not the title, but more of a code name which will be used to create pages. For the impatient artist there is even a generator that produces code names. +* the main language +* whether to make a new project directory inside the selected directory. This allows you to have a generic comics directory that you always select and that CPMT will make directories named with the project name inside. +* the name for the directory to store the pages. This is where new pages are placed. +* the name for the directory to store the export. This is where the comic will be exported to. +* the name for the directory to store the template. This is where the page templates get stored. + +It will also allow you to edit meta data if you'd want already, but this is not mandatory. + +Then after you finish, select *Open Project*, go to the location where you have stored your comics project. There should be a "comicsConfig.json" file there, next to the new folders for the pages, templates and export. Open that. + +Now, click *Add Page* to add your first page. You will get a dialog asking for the template. Here you can generate one, or import one. CPMT will remember this as the default one. + +Double click the new page to open in Krita. + +The second column in the docker allows you to see the "subject" line in the document info if it's filled in. + +You can press the arrow next to *Add Page* to get more features, like *Add Existing Page*, *Remove Page*, or *Batch Resize*. + +Usage - Meta Data +------------------ +You can edit the meta data by clicking the dropdown next to *Project Settings* and selecting *Meta Data*. + +There's quite a few fields here, because there's quite a few different types of meta data. Hover over the fields to get an idea of what needs to be typed. + +The meta data is intended to be filled out over the course of the project, so don't worry too much if you cannot instantly think of what a certain entry should be. + +The meta data fields have auto completion wherever sensible. You can add your own meta data fields as noted in the following section: + +### Adding extra auto-completion keys. +First, you need to go to project settings, and there point the extra keys to a folder where extra keys can be found. + +It will search that extra folder for the following folders: + +* key_genre +* key_format +* key_rating +* key_characters +* key_author_roles +* key_other + +You can add extra auto-completion keys by adding a text file with each new key on a separate line to one of the "key" folders. The name of the text file doesn't matter. This way you can add characters by universe, or archive specific keywords by archive name. + +So for example, the following file has three superhero names on different lines, nothing more, nothing less. + +``` + Spider-Man + Hawkeye + Jean Grey +``` + +When you then store it as marvel.txt put into the directory "key_characters", Krita will use the names from the list as suggestion for the character field in the meta-data. + +The exception is the key_ratings, which uses CSV files, using the top row to determine the title, and then has the rating in the first column, and the description on the second. This allows the description to show up as tool-tips. + +### The Author list +The author list is a table containing all the authors of the project. It allows a distinction between given, family, middle and nickname, as well as role, email and homepage. + +You can rearrange the author list by drag and dropping the number at the left, as well as adding and removing authors. + +Adding an author will always add "John Doe". You can double click the names and cells to change their contents. For the role, there are auto completion keys, so to encourage using standardized ways to describe their roles. + +In the main docker, there's an option under the pages actions called *Scrape Authors*, this will make the comics project docker search the pages in the pages list for author info and append that to the author list. It will not attempt to check for duplicates, so be sure to the list afterwards. + +Usage - Project Settings +----------------------- +The project settings allows you to change all the technical details of the project: + +* the project name +* the concept +* the location of pages, export and templates +* the default template. +* the location of the extra auto-completion keys(see metadata) + +Usage - Pages +------------- +There's several other things you can do with pages. You can either access these feature by clicking the drop-down next to *Add Page* or right-clicking the pages list. + +**Adding pages** + You can add pages by pressing the *Add Page* button. The first time you press this, it'll ask for a template. After you create or select a template it will use this as the default. You can set the default in the project settings. +**Adding pages from template:** + Adding pages from a template will always give the template dialog. This will allow you to have several different templates in the templates directory(it will show all the kra files in the templates directory), so that you can have spread, coverpages and other pages at your finger tips. The create template dialog will allow you to make a simple two layer image with a white background, and rulers for the bleeds and guides. Import template will copy selected templates to the template directory, keeping all the necessary files inside the comics project. +**Remove a page** + This allows you to remove the selected page in the list from the pages list. It does NOT delete the page from the disk. +**Adding existing pages:** + This is for when you wish to add existing pages, either because you removed the page from the list, or because you already have a project going and wish to add the pages to the list. +**Batch Resize** + This will show a window with resize options. After selecting the right options, all the pages will be resized as such. A progress dialog will pop up showing you which pages have been done and how long it will take based on the passed time. +**View Page in Window** + This will pop up a dialog with the selected page's mergedimage.png. The dialog will update when doing this for the image of another page. This is so that you can have a quick reference for a single page in the event your other referencing tools cannot open kra files. +**Scrape Authors** + This searches all the files from the pages list for author information and adds that to the author list. It will not check for copies, so you will need to clean up the author list yourself. +**Rearranging pages** + You can rearrange pages by moving the number on the left of the page up or down. + +Usage - Copy Location +-------------------- +Copy location, the button underneath the export button, allows you to copy the current project location to clipboard. Just press it, and paste somewhere else. This is useful when using multiple programs and reference tools and you just want to quickly navigate to the project directory. + +Usage - Export +-------------- +CPMT will not allow export without any export methods set. + +You can configure the export settings by going to the drop-down next to *Project Settings* and selecting *Export Settings*. + +Here you can define... + +* how much a page needs to be cropped +* which layers to remove by layer color-label +* to which formats to export, in what file-format and how to resize. + +Once you've done that, press export. Krita will pop up a progress bar for you with the estimated time and progress, so you can estimate how long you will have to wait. + +CPMT will store the resized files and meta data in separate folders in the export folder. This is so that you can perform optimization methods afterwards and update everything quickly. + +TODO: +====== +Things I still want to do: + +* Krita: + - Allow selecting the text layer for acbf. (Requires text api, preferably with html option :) ) + - Allow selecting the panel layer for acbf. (Requires vector api) + - Generate text from the author list. (Requires text api) + - batch export broken. (bug) + - save as doesn't work on a new file. (bug) +* comment code better. (only the exporter really needs to be commented) +* clean up path relativeness. (Not sure how much better this can be done) +* Make label removal just a list? (unsure) +* For translation, figure out a way to translate certain roles/genres to the acbf defaults. (probably will need to use a list) + +ACBF list: + +* Support fading mechanisms by using the keywords field in the metadata. + - acbf:none + - acbf:fade + - acbf:blend + - acbf:horz + - acbf:vert + - acbf:title (Will use this page's title as a table of contents entry.) +* support getting text info from the vector layers. + - users can specify a name for text layers. + - last two/five characters are used to determine language. + - maybe text-class can be used to determine type? + + Speech (speech, dialogue) + + Commentary (caption in american comics) + + Formal (For justified aligned text, like big chunks of text.) + + Letter (Like a letter in a comic) + + Code (Monospaced font) + + Heading (Chapter title) + + Audio (Only meant for audio devices) + + Thought (Thought bubbles and the like) + + Sign (For signs on buildings and the like.) + + Inverted (Whether or not this should be treated as inverted text) + + transparent(For a transparent wordballoon.) + + Question: Where is general sound effects? Like, if we make a distinction between speech and thought, why are general sound effects missing? (Admittedly, I'd prefer if we could allow putting sound effects and such as a base64 reffed bit.) \ No newline at end of file diff --git a/plugins/extensions/pykrita/plugin/plugins/comics_project_management_tools/__init__.py b/plugins/extensions/pykrita/plugin/plugins/comics_project_management_tools/__init__.py new file mode 100644 index 0000000000..32e2746a2a --- /dev/null +++ b/plugins/extensions/pykrita/plugin/plugins/comics_project_management_tools/__init__.py @@ -0,0 +1,2 @@ +# let's make a module +from .comics_project_manager_docker import * diff --git a/plugins/extensions/pykrita/plugin/plugins/comics_project_management_tools/comics_export_dialog.py b/plugins/extensions/pykrita/plugin/plugins/comics_project_management_tools/comics_export_dialog.py new file mode 100644 index 0000000000..d1052919a5 --- /dev/null +++ b/plugins/extensions/pykrita/plugin/plugins/comics_project_management_tools/comics_export_dialog.py @@ -0,0 +1,397 @@ +""" +Part of the comics project management tools (CPMT). + +A dialog for editing the exporter settings. +""" + +from PyQt5.QtGui import QStandardItem, QStandardItemModel, QColor +from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QGroupBox, QFormLayout, QCheckBox, QComboBox, QSpinBox, QWidget, QVBoxLayout, QTabWidget, QPushButton, QLineEdit, QLabel, QListView +from PyQt5.QtCore import Qt, QUuid +from krita import * + +""" +A generic widget to make selecting size easier. +It works by initialising with a config name(like "scale"), and then optionally setting the config with a dictionary. +Then, afterwards, you get the config with a dictionary, with the config name being the entry the values are under. +""" + + +class comic_export_resize_widget(QGroupBox): + configName = "" + + def __init__(self, configName, batch=False, fileType=True): + super().__init__() + self.configName = configName + self.setTitle("Adjust Workingfile") + formLayout = QFormLayout() + self.setLayout(formLayout) + self.crop = QCheckBox(i18n("Crop files before resize.")) + self.cmbFile = QComboBox() + self.cmbFile.addItems(["png", "jpg", "webp"]) + self.resizeMethod = QComboBox() + self.resizeMethod.addItems([i18n("Percentage"), i18n("DPI"), i18n("Maximum Width"), i18n("Maximum Height")]) + self.resizeMethod.currentIndexChanged.connect(self.slot_set_enabled) + self.spn_DPI = QSpinBox() + self.spn_DPI.setMaximum(1200) + self.spn_DPI.setSuffix(i18n(" DPI")) + self.spn_DPI.setValue(72) + self.spn_PER = QSpinBox() + if batch is True: + self.spn_PER.setMaximum(1000) + else: + self.spn_PER.setMaximum(100) + self.spn_PER.setSuffix(" %") + self.spn_PER.setValue(100) + self.spn_width = QSpinBox() + self.spn_width.setMaximum(99999) + self.spn_width.setSuffix(" px") + self.spn_width.setValue(800) + self.spn_height = QSpinBox() + self.spn_height.setMaximum(99999) + self.spn_height.setSuffix(" px") + self.spn_height.setValue(800) + + if batch is False: + formLayout.addRow("", self.crop) + if fileType is True and configName != "TIFF": + formLayout.addRow(i18n("File Type"), self.cmbFile) + formLayout.addRow(i18n("Method:"), self.resizeMethod) + formLayout.addRow(i18n("DPI:"), self.spn_DPI) + formLayout.addRow(i18n("Percentage:"), self.spn_PER) + formLayout.addRow(i18n("Width:"), self.spn_width) + formLayout.addRow(i18n("Height:"), self.spn_height) + self.slot_set_enabled() + + def slot_set_enabled(self): + method = self.resizeMethod.currentIndex() + self.spn_DPI.setEnabled(False) + self.spn_PER.setEnabled(False) + self.spn_width.setEnabled(False) + self.spn_height.setEnabled(False) + + if method is 0: + self.spn_PER.setEnabled(True) + if method is 1: + self.spn_DPI.setEnabled(True) + if method is 2: + self.spn_width.setEnabled(True) + if method is 3: + self.spn_height.setEnabled(True) + + def set_config(self, config): + if self.configName in config.keys(): + mConfig = config[self.configName] + if "Method" in mConfig.keys(): + self.resizeMethod.setCurrentIndex(mConfig["Method"]) + if "FileType" in mConfig.keys(): + self.cmbFile.setCurrentText(mConfig["FileType"]) + if "Crop" in mConfig.keys(): + self.crop.setChecked(mConfig["Crop"]) + if "DPI" in mConfig.keys(): + self.spn_DPI.setValue(mConfig["DPI"]) + if "Percentage" in mConfig.keys(): + self.spn_PER.setValue(mConfig["Percentage"]) + if "Width" in mConfig.keys(): + self.spn_width.setValue(mConfig["Width"]) + if "Height" in mConfig.keys(): + self.spn_height.setValue(mConfig["Height"]) + self.slot_set_enabled() + + def get_config(self, config): + mConfig = {} + mConfig["Method"] = self.resizeMethod.currentIndex() + if self.configName == "TIFF": + mConfig["FileType"] = "tiff" + else: + mConfig["FileType"] = self.cmbFile.currentText() + mConfig["Crop"] = self.crop.isChecked() + mConfig["DPI"] = self.spn_DPI.value() + mConfig["Percentage"] = self.spn_PER.value() + mConfig["Width"] = self.spn_width.value() + mConfig["Height"] = self.spn_height.value() + config[self.configName] = mConfig + return config + + +""" +Quick combobox for selecting the color label. +""" + + +class labelSelector(QComboBox): + def __init__(self): + super(labelSelector, self).__init__() + lisOfColors = [] + lisOfColors.append(Qt.transparent) + lisOfColors.append(QColor(91, 173, 220)) + lisOfColors.append(QColor(151, 202, 63)) + lisOfColors.append(QColor(247, 229, 61)) + lisOfColors.append(QColor(255, 170, 63)) + lisOfColors.append(QColor(177, 102, 63)) + lisOfColors.append(QColor(238, 50, 51)) + lisOfColors.append(QColor(191, 106, 209)) + lisOfColors.append(QColor(118, 119, 114)) + + self.itemModel = QStandardItemModel() + for color in lisOfColors: + item = QStandardItem() + item.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled) + item.setCheckState(Qt.Unchecked) + item.setText(" ") + item.setData(color, Qt.BackgroundColorRole) + self.itemModel.appendRow(item) + self.setModel(self.itemModel) + + def getLabels(self): + listOfIndexes = [] + for i in range(self.itemModel.rowCount()): + index = self.itemModel.index(i, 0) + item = self.itemModel.itemFromIndex(index) + if item.checkState(): + listOfIndexes.append(i) + return listOfIndexes + + def setLabels(self, listOfIndexes): + for i in listOfIndexes: + index = self.itemModel.index(i, 0) + item = self.itemModel.itemFromIndex(index) + item.setCheckState(True) + + +""" +The comic export settings dialog will allow configuring the export. + +This config consists of... + +* Crop settings. for removing bleeds. +* Selecting layer labels to remove. +* Choosing which formats to export to. + * Choosing how to resize these + * Whether to crop. + * Which file type to use. + +And for ACBF, it gives the ability to edit acbf document info. + +""" + + +class comic_export_setting_dialog(QDialog): + + def __init__(self): + super().__init__() + self.setLayout(QVBoxLayout()) + self.setWindowTitle(i18n("Export settings")) + buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) + + buttons.accepted.connect(self.accept) + buttons.rejected.connect(self.reject) + mainWidget = QTabWidget() + self.layout().addWidget(mainWidget) + self.layout().addWidget(buttons) + + # Set basic crop settings + # Set which layers to remove before export. + mainExportSettings = QWidget() + mainExportSettings.setLayout(QVBoxLayout()) + groupExportCrop = QGroupBox(i18n("Crop settings")) + formCrop = QFormLayout() + groupExportCrop.setLayout(formCrop) + self.chk_toOutmostGuides = QCheckBox(i18n("Crop to outmost guides")) + self.chk_toOutmostGuides.setChecked(True) + self.chk_toOutmostGuides.setToolTip(i18n("This will crop to the outmost guides if possible and otherwise use the underlying crop settings.")) + formCrop.addRow("", self.chk_toOutmostGuides) + btn_fromSelection = QPushButton(i18n("Set margins from active selection")) + btn_fromSelection.clicked.connect(self.slot_set_margin_from_selection) + # This doesn't work. + formCrop.addRow("", btn_fromSelection) + self.spn_marginLeft = QSpinBox() + self.spn_marginLeft.setMaximum(99999) + self.spn_marginLeft.setSuffix(" px") + formCrop.addRow(i18n("Left:"), self.spn_marginLeft) + self.spn_marginTop = QSpinBox() + self.spn_marginTop.setMaximum(99999) + self.spn_marginTop.setSuffix(" px") + formCrop.addRow(i18n("Top:"), self.spn_marginTop) + self.spn_marginRight = QSpinBox() + self.spn_marginRight.setMaximum(99999) + self.spn_marginRight.setSuffix(" px") + formCrop.addRow(i18n("Right:"), self.spn_marginRight) + self.spn_marginBottom = QSpinBox() + self.spn_marginBottom.setMaximum(99999) + self.spn_marginBottom.setSuffix(" px") + formCrop.addRow(i18n("Bottom:"), self.spn_marginBottom) + groupExportLayers = QGroupBox(i18n("Layers")) + formLayers = QFormLayout() + groupExportLayers.setLayout(formLayers) + self.cmbLabelsRemove = labelSelector() + formLayers.addRow(i18n("Label for removal:"), self.cmbLabelsRemove) + + mainExportSettings.layout().addWidget(groupExportCrop) + mainExportSettings.layout().addWidget(groupExportLayers) + mainWidget.addTab(mainExportSettings, i18n("General")) + + # CBZ, crop, resize, which metadata to add. + CBZexportSettings = QWidget() + CBZexportSettings.setLayout(QVBoxLayout()) + self.CBZactive = QCheckBox(i18n("Export to CBZ")) + CBZexportSettings.layout().addWidget(self.CBZactive) + self.CBZgroupResize = comic_export_resize_widget("CBZ") + CBZexportSettings.layout().addWidget(self.CBZgroupResize) + self.CBZactive.clicked.connect(self.CBZgroupResize.setEnabled) + CBZgroupMeta = QGroupBox(i18n("Metadata to add")) + # CBZexportSettings.layout().addWidget(CBZgroupMeta) + CBZgroupMeta.setLayout(QFormLayout()) + + mainWidget.addTab(CBZexportSettings, "CBZ") + + # ACBF, crop, resize, creator name, version history, panel layer, text layers. + ACBFExportSettings = QWidget() + ACBFform = QFormLayout() + ACBFExportSettings.setLayout(QVBoxLayout()) + ACBFdocInfo = QGroupBox() + ACBFdocInfo.setTitle(i18n("ACBF Document Info")) + ACBFdocInfo.setLayout(ACBFform) + self.lnACBFAuthor = QLineEdit() + self.lnACBFAuthor.setToolTip(i18n("The person responsible for the generation of the CBZ.")) + self.lnACBFSource = QLineEdit() + self.lnACBFSource.setToolTip(i18n("Whether the acbf file is an adaption of an existing source, and if so, how to find information about that source. So for example, for an adapted webcomic, the official website url should go here.")) + self.lnACBFID = QLabel() + self.lnACBFID.setToolTip(i18n("By default this will be filled with a generated universal unique identifier. The ID by itself is merely so that comic book library management programs can figure out if this particular comic is already in their database and whether it has been rated. Of course, the UUID can be changed into something else by manually changing the json, but this is advanced usage.")) + self.spnACBFVersion = QSpinBox() + self.ACBFhistoryModel = QStandardItemModel() + acbfHistoryList = QListView() + acbfHistoryList.setModel(self.ACBFhistoryModel) + btn_add_history = QPushButton(i18n("Add history entry")) + btn_add_history.clicked.connect(self.slot_add_history_item) + + ACBFform.addRow(i18n("Author-name:"), self.lnACBFAuthor) + ACBFform.addRow(i18n("Source:"), self.lnACBFSource) + ACBFform.addRow(i18n("ACBF UID:"), self.lnACBFID) + ACBFform.addRow(i18n("Version:"), self.spnACBFVersion) + ACBFform.addRow(i18n("Version History:"), acbfHistoryList) + ACBFform.addRow("", btn_add_history) + + ACBFExportSettings.layout().addWidget(ACBFdocInfo) + mainWidget.addTab(ACBFExportSettings, "ACBF") + + # Epub export, crop, resize, other questions. + EPUBexportSettings = QWidget() + EPUBexportSettings.setLayout(QVBoxLayout()) + self.EPUBactive = QCheckBox(i18n("Export to EPUB")) + EPUBexportSettings.layout().addWidget(self.EPUBactive) + self.EPUBgroupResize = comic_export_resize_widget("EPUB") + EPUBexportSettings.layout().addWidget(self.EPUBgroupResize) + self.EPUBactive.clicked.connect(self.EPUBgroupResize.setEnabled) + mainWidget.addTab(EPUBexportSettings, "EPUB") + + # For Print. Crop, no resize. + TIFFExportSettings = QWidget() + TIFFExportSettings.setLayout(QVBoxLayout()) + self.TIFFactive = QCheckBox(i18n("Export to TIFF")) + TIFFExportSettings.layout().addWidget(self.TIFFactive) + self.TIFFgroupResize = comic_export_resize_widget("TIFF") + TIFFExportSettings.layout().addWidget(self.TIFFgroupResize) + self.TIFFactive.clicked.connect(self.TIFFgroupResize.setEnabled) + mainWidget.addTab(TIFFExportSettings, "TIFF") + + # SVG, crop, resize, embed vs link. + #SVGExportSettings = QWidget() + + #mainWidget.addTab(SVGExportSettings, "SVG") + + """ + Add a history item to the acbf version history list. + """ + + def slot_add_history_item(self): + newItem = QStandardItem() + newItem.setText("v" + str(self.spnACBFVersion.value()) + "-" + i18n("in this version...")) + self.ACBFhistoryModel.appendRow(newItem) + + """ + Get the margins by treating the active selection in a document as the trim area. + This allows people to snap selections to a vector or something, and then get the margins. + """ + + def slot_set_margin_from_selection(self): + doc = Application.activeDocument() + if doc is not None: + if doc.selection() is not None: + self.spn_marginLeft.setValue(doc.selection().x()) + self.spn_marginTop.setValue(doc.selection().y()) + self.spn_marginRight.setValue(doc.width() - (doc.selection().x() + doc.selection().width())) + self.spn_marginBottom.setValue(doc.height() - (doc.selection().y() + doc.selection().height())) + + """ + Load the UI values from the config dictionary given. + """ + + def setConfig(self, config): + if "cropToGuides" in config.keys(): + self.chk_toOutmostGuides.setChecked(config["cropToGuides"]) + if "cropLeft" in config.keys(): + self.spn_marginLeft.setValue(config["cropLeft"]) + if "cropTop" in config.keys(): + self.spn_marginTop.setValue(config["cropTop"]) + if "cropRight" in config.keys(): + self.spn_marginRight.setValue(config["cropRight"]) + if "cropBottom" in config.keys(): + self.spn_marginBottom.setValue(config["cropBottom"]) + if "labelsToRemove" in config.keys(): + self.cmbLabelsRemove.setLabels(config["labelsToRemove"]) + self.CBZgroupResize.set_config(config) + if "CBZactive" in config.keys(): + self.CBZactive.setChecked(config["CBZactive"]) + self.EPUBgroupResize.set_config(config) + if "EPUBactive" in config.keys(): + self.EPUBactive.setChecked(config["EPUBactive"]) + self.TIFFgroupResize.set_config(config) + if "TIFFactive" in config.keys(): + self.TIFFactive.setChecked(config["TIFFactive"]) + if "acbfAuthor" in config.keys(): + self.lnACBFAuthor.setText(config["acbfAuthor"]) + if "acbfSource" in config.keys(): + self.lnACBFSource.setText(config["acbfSource"]) + if "acbfID" in config.keys(): + self.lnACBFID.setText(config["acbfID"]) + else: + self.lnACBFID.setText(QUuid.createUuid().toString()) + if "acbfVersion" in config.keys(): + self.spnACBFVersion.setValue(config["acbfVersion"]) + if "acbfHistory" in config.keys(): + for h in config["acbfHistory"]: + item = QStandardItem() + item.setText(h) + self.ACBFhistoryModel.appendRow(item) + self.CBZgroupResize.setEnabled(self.CBZactive.isChecked()) + + """ + Store the GUI values into the config dictionary given. + + @return the config diactionary filled with new values. + """ + + def getConfig(self, config): + + config["cropToGuides"] = self.chk_toOutmostGuides.isChecked() + config["cropLeft"] = self.spn_marginLeft.value() + config["cropTop"] = self.spn_marginTop.value() + config["cropBottom"] = self.spn_marginRight.value() + config["cropRight"] = self.spn_marginBottom.value() + config["labelsToRemove"] = self.cmbLabelsRemove.getLabels() + config["CBZactive"] = self.CBZactive.isChecked() + config = self.CBZgroupResize.get_config(config) + config["EPUBactive"] = self.EPUBactive.isChecked() + config = self.EPUBgroupResize.get_config(config) + config["TIFFactive"] = self.TIFFactive.isChecked() + config = self.TIFFgroupResize.get_config(config) + config["acbfAuthor"] = self.lnACBFAuthor.text() + config["acbfSource"] = self.lnACBFSource.text() + config["acbfID"] = self.lnACBFID.text() + config["acbfVersion"] = self.spnACBFVersion.value() + versionList = [] + for r in range(self.ACBFhistoryModel.rowCount()): + index = self.ACBFhistoryModel.index(r, 0) + versionList.append(self.ACBFhistoryModel.data(index, Qt.DisplayRole)) + config["acbfHistory"] = versionList + return config diff --git a/plugins/extensions/pykrita/plugin/plugins/comics_project_management_tools/comics_exporter.py b/plugins/extensions/pykrita/plugin/plugins/comics_project_management_tools/comics_exporter.py new file mode 100644 index 0000000000..dab2b683ab --- /dev/null +++ b/plugins/extensions/pykrita/plugin/plugins/comics_project_management_tools/comics_exporter.py @@ -0,0 +1,1183 @@ +""" +Part of the comics project management tools (CPMT). + +An exporter that take the comicsConfig and uses it to generate several files. +""" +import sys +import os +from pathlib import Path +import json +import zipfile +import xml.etree.ElementTree as ET +import shutil +from PyQt5.QtWidgets import QLabel, QProgressDialog # For the progress dialog. +from PyQt5.QtCore import QElapsedTimer, QDateTime, QLocale, Qt +from krita import * + +""" +The sizesCalculator is a convenience class for interpretting the resize configuration +from the export settings dialog. It is also used for batch resize. +""" + + +class sizesCalculator(): + + def __init__(self): + pass + + def get_scale_from_resize_config(self, config, listSizes): + listScaleTo = listSizes + oldWidth = listSizes[0] + oldHeight = listSizes[1] + oldXDPI = listSizes[2] + oldYDPI = listSizes[3] + if "Method" in config.keys(): + method = config["Method"] + if method is 0: + # percentage + percentage = config["Percentage"] / 100 + listScaleTo[0] = round(oldWidth * percentage) + listScaleTo[1] = round(oldHeight * percentage) + if method is 1: + # dpi + DPI = config["DPI"] + listScaleTo[0] = round((oldWidth / oldXDPI) * DPI) + listScaleTo[1] = round((oldHeight / oldYDPI) * DPI) + listScaleTo[2] = DPI + listScaleTo[3] = DPI + if method is 2: + # maximum width + width = config["Width"] + listScaleTo[0] = width + listScaleTo[1] = round((oldHeight / oldWidth) * width) + if method is 3: + # maximum height + height = config["Height"] + listScaleTo[1] = height + listScaleTo[0] = round((oldWidth / oldHeight) * height) + return listScaleTo + + +""" +The comicsExporter is a class that batch exports to all the requested formats. +Make it, set_config with the right data, and then call up "export". + +The majority of the functions are meta-data encoding functions. +""" + + +class comicsExporter(): + acbfLocation = str() + cometLocation = str() + comicRackInfo = str() + comic_book_info_json_dump = str() + pagesLocationList = {} + + def __init__(self): + pass + + """ + The the configuration of the exporter. + + @param config: A dictionary containing all the config. + + @param projectUrl: the main location of the project folder. + """ + + def set_config(self, config, projectURL): + self.configDictionary = config + self.projectURL = projectURL + self.pagesLocationList = {} + self.acbfLocation = str() + self.cometLocation = str() + self.comicRackInfo = str() + self.comic_book_info_json_dump = str() + + """ + Export everything according to config and get yourself a coffee. + This won't work if the config hasn't been set. + """ + + def export(self): + export_success = False + + path = Path(self.projectURL) + if path.exists(): + # Make a meta-data folder so we keep the export folder nice and clean. + exportPath = path / self.configDictionary["exportLocation"] + if Path(exportPath / "metadata").exists() is False: + Path(exportPath / "metadata").mkdir() + + # Get to which formats to export, and set the sizeslist. + sizesList = {} + if "CBZ" in self.configDictionary.keys(): + if self.configDictionary["CBZactive"]: + sizesList["CBZ"] = self.configDictionary["CBZ"] + if "EPUB" in self.configDictionary.keys(): + if self.configDictionary["EPUBactive"]: + sizesList["EPUB"] = self.configDictionary["EPUB"] + if "TIFF" in self.configDictionary.keys(): + if self.configDictionary["TIFFactive"]: + sizesList["TIFF"] = self.configDictionary["TIFF"] + # Export the pngs according to the sizeslist. + export_success = self.save_out_pngs(sizesList) + + # Export acbf metadata. + if export_success: + export_success = self.export_to_acbf() + + # Export and package CBZ and Epub. + if export_success: + if "CBZ" in sizesList.keys(): + export_success = self.export_to_cbz() + print("CPMT: Exported to CBZ", export_success) + if "EPUB" in sizesList.keys(): + export_success = self.export_to_epub() + print("CPMT: Exported to EPUB", export_success) + else: + print("CPMT: Nothing to export, url not set.") + + return export_success + + """ + This calls up all the functions necessary for making a acbf. + """ + + def export_to_acbf(self): + self.write_acbf_meta_data() + return True + + """ + This calls up all the functions necessary for making a cbz. + """ + + def export_to_cbz(self): + export_success = self.write_comet_meta_data() + export_success = self.write_comic_rack_info() + export_success = self.write_comic_book_info_json() + self.package_cbz() + return export_success + + """ + Create an epub folder, finally, package to a epubzip. + """ + + def export_to_epub(self): + path = Path(os.path.join(self.projectURL, self.configDictionary["exportLocation"])) + exportPath = path / "EPUB-files" + metaInf = exportPath / "META-INF" + oebps = exportPath / "OEBPS" + imagePath = oebps / "Images" + stylesPath = oebps / "Styles" + textPath = oebps / "Text" + if exportPath.exists() is False: + exportPath.mkdir() + metaInf.mkdir() + oebps.mkdir() + imagePath.mkdir() + stylesPath.mkdir() + textPath.mkdir() + + mimetype = open(str(Path(exportPath / "mimetype")), mode="w") + mimetype.write("application/epub+zip") + mimetype.close() + + container = ET.ElementTree() + cRoot = ET.Element("container") + cRoot.set("version", "1.0") + cRoot.set("xmlns", "urn:oasis:names:tc:opendocument:xmlns:container") + container._setroot(cRoot) + rootFiles = ET.Element("rootfiles") + rootfile = ET.Element("rootfile") + rootfile.set("full-path", "OEBPS/content.opf") + rootfile.set("media-type", "application/oebps-package+xml") + rootFiles.append(rootfile) + cRoot.append(rootFiles) + container.write(str(Path(metaInf / "container.xml")), encoding="utf-8", xml_declaration=True) + + # copyimages to images + pagesList = [] + if "EPUB" in self.pagesLocationList.keys(): + coverNumber = self.configDictionary["pages"].index(self.configDictionary["cover"]) + for p in self.pagesLocationList["EPUB"]: + if os.path.exists(p): + shutil.copy2(p, str(imagePath)) + pagesList.append(str(Path(imagePath / os.path.basename(p)))) + if len(self.pagesLocationList["EPUB"]) >= coverNumber: + coverpageurl = pagesList[coverNumber] + else: + print("CPMT: Couldn't find the location for the epub files.") + return False + + # for each image, make an xml file + htmlFiles = [] + for i in range(len(pagesList)): + pageName = "Page" + str(i) + ".xhtml" + doc = ET.ElementTree() + html = ET.Element("html") + doc._setroot(html) + html.set("xmlns", "http://www.w3.org/1999/xhtml") + html.set("xmlns:epub", "http://www.idpf.org/2007/ops") + + head = ET.Element("head") + html.append(head) + + body = ET.Element("body") + + img = ET.Element("img") + img.set("src", os.path.relpath(pagesList[i], str(textPath))) + body.append(img) + + if pagesList[i] != coverpageurl: + pagenumber = ET.Element("p") + pagenumber.text = "Page " + str(i) + body.append(pagenumber) + html.append(body) + + filename = str(Path(textPath / pageName)) + doc.write(filename, encoding="utf-8", xml_declaration=True) + if pagesList[i] == coverpageurl: + coverpagehtml = os.path.relpath(filename, str(oebps)) + htmlFiles.append(filename) + + # opf file + opfFile = ET.ElementTree() + opfRoot = ET.Element("package") + opfRoot.set("version", "3.0") + opfRoot.set("unique-identifier", "BookId") + opfRoot.set("xmlns", "http://www.idpf.org/2007/opf") + opfFile._setroot(opfRoot) + + # metadata + opfMeta = ET.Element("metadata") + opfMeta.set("xmlns:dc", "http://purl.org/dc/elements/1.1/") + + if "language" in self.configDictionary.keys(): + bookLang = ET.Element("dc:language") + bookLang.text = self.configDictionary["language"] + opfMeta.append(bookLang) + bookTitle = ET.Element("dc:title") + if "title" in self.configDictionary.keys(): + bookTitle.text = str(self.configDictionary["title"]) + else: + bookTitle.text = "Comic with no Name" + opfMeta.append(bookTitle) + if "authorList" in self.configDictionary.keys(): + for authorE in range(len(self.configDictionary["authorList"])): + authorDict = self.configDictionary["authorList"][authorE] + authorType = "dc:creator" + if "role" in authorDict.keys(): + if str(authorDict["role"]).lower() in ["editor", "assistant editor", "proofreader", "beta"]: + authorType = "dc:contributor" + author = ET.Element(authorType) + authorName = [] + if "last-name" in authorDict.keys(): + authorName.append(authorDict["last-name"]) + if "first-name" in authorDict.keys(): + authorName.append(authorDict["first-name"]) + if "initials" in authorDict.keys(): + authorName.append(authorDict["initials"]) + if "nickname" in authorDict.keys(): + authorName.append("(" + authorDict["nickname"] + ")") + author.text = ", ".join(authorName) + opfMeta.append(author) + if "role" in authorDict.keys(): + author.set("id", "cre" + str(authorE)) + role = ET.Element("meta") + role.set("refines", "cre" + str(authorE)) + role.set("scheme", "marc:relators") + role.set("property", "role") + role.text = str(authorDict["role"]) + opfMeta.append(role) + + if "publishingDate" in self.configDictionary.keys(): + date = ET.Element("dc:date") + date.text = self.configDictionary["publishingDate"] + opfMeta.append(date) + description = ET.Element("dc:description") + if "summary" in self.configDictionary.keys(): + description.text = self.configDictionary["summary"] + else: + description.text = "There was no summary upon generation of this file." + opfMeta.append(description) + + type = ET.Element("dc:type") + type.text = "Comic" + opfMeta.append(type) + if "publisherName" in self.configDictionary.keys(): + publisher = ET.Element("dc:publisher") + publisher.text = self.configDictionary["publisherName"] + opfMeta.append(publisher) + if "isbn-number" in self.configDictionary.keys(): + publishISBN = ET.Element("dc:identifier") + publishISBN.text = str("urn:isbn:") + self.configDictionary["isbn-number"] + opfMeta.append(publishISBN) + if "license" in self.configDictionary.keys(): + rights = ET.Element("dc:rights") + rights.text = self.configDictionary["license"] + opfMeta.append(rights) + + if "genre" in self.configDictionary.keys(): + for g in self.configDictionary["genre"]: + subject = ET.Element("dc:subject") + subject.text = g + opfMeta.append(subject) + if "characters" in self.configDictionary.keys(): + for name in self.configDictionary["characters"]: + char = ET.Element("dc:subject") + char.text = name + opfMeta.append(char) + if "format" in self.configDictionary.keys(): + for format in self.configDictionary["format"]: + f = ET.Element("dc:subject") + f.text = format + opfMeta.append(f) + if "otherKeywords" in self.configDictionary.keys(): + for key in self.configDictionary["otherKeywords"]: + word = ET.Element("dc:subject") + word.text = key + opfMeta.append(word) + + opfRoot.append(opfMeta) + + opfManifest = ET.Element("manifest") + toc = ET.Element("item") + toc.set("id", "ncx") + toc.set("href", "toc.ncx") + toc.set("media-type", "application/x-dtbncx+xml") + opfManifest.append(toc) + for p in htmlFiles: + item = ET.Element("item") + item.set("id", os.path.basename(p)) + item.set("href", os.path.relpath(p, str(oebps))) + item.set("media-type", "application/xhtml+xml") + opfManifest.append(item) + for p in pagesList: + item = ET.Element("item") + item.set("id", os.path.basename(p)) + item.set("href", os.path.relpath(p, str(oebps))) + item.set("media-type", "image/png") + if os.path.basename(p) == os.path.basename(coverpageurl): + item.set("properties", "cover-image") + opfManifest.append(item) + + opfRoot.append(opfManifest) + + opfSpine = ET.Element("spine") + opfSpine.set("toc", "ncx") + for p in htmlFiles: + item = ET.Element("itemref") + item.set("idref", os.path.basename(p)) + opfSpine.append(item) + opfRoot.append(opfSpine) + + opfGuide = ET.Element("guide") + if coverpagehtml is not None and coverpagehtml.isspace() is False and len(coverpagehtml) > 0: + item = ET.Element("reference") + item.set("type", "cover") + item.set("title", "Cover") + item.set("href", coverpagehtml) + opfRoot.append(opfGuide) + + opfFile.write(str(Path(oebps / "content.opf")), encoding="utf-8", xml_declaration=True) + # toc + tocDoc = ET.ElementTree() + ncx = ET.Element("ncx") + ncx.set("version", "2005-1") + ncx.set("xmlns", "http://www.daisy.org/z3986/2005/ncx/") + tocDoc._setroot(ncx) + + tocHead = ET.Element("head") + metaID = ET.Element("meta") + metaID.set("content", "ID_UNKNOWN") + metaID.set("name", "dtb:uid") + tocHead.append(metaID) + metaDepth = ET.Element("meta") + metaDepth.set("content", str(0)) + metaDepth.set("name", "dtb:depth") + tocHead.append(metaDepth) + metaTotal = ET.Element("meta") + metaTotal.set("content", str(0)) + metaTotal.set("name", "dtb:totalPageCount") + tocHead.append(metaTotal) + metaMax = ET.Element("meta") + metaMax.set("content", str(0)) + metaMax.set("name", "dtb:maxPageNumber") + tocHead.append(metaDepth) + ncx.append(tocHead) + + docTitle = ET.Element("docTitle") + text = ET.Element("text") + if "title" in self.configDictionary.keys(): + text.text = str(self.configDictionary["title"]) + else: + text.text = "Comic with no Name" + docTitle.append(text) + ncx.append(docTitle) + + navmap = ET.Element("navMap") + navPoint = ET.Element("navPoint") + navPoint.set("id", "navPoint-1") + navPoint.set("playOrder", "1") + navLabel = ET.Element("navLabel") + navLabelText = ET.Element("text") + navLabelText.text = "Start" + navLabel.append(navLabelText) + navContent = ET.Element("content") + navContent.set("src", os.path.relpath(htmlFiles[0], str(oebps))) + navPoint.append(navLabel) + navPoint.append(navContent) + navmap.append(navPoint) + ncx.append(navmap) + + tocDoc.write(str(Path(oebps / "toc.ncx")), encoding="utf-8", xml_declaration=True) + + self.package_epub() + return True + + def save_out_pngs(self, sizesList): + # A small fix to ensure crop to guides is set. + if "cropToGuides" not in self.configDictionary.keys(): + self.configDictionary["cropToGuides"] = False + + # Check if we have pages at all... + if "pages" in self.configDictionary.keys(): + + # Check if there's export methods, and if so make sure the appropriate dictionaries are initialised. + if len(sizesList.keys()) < 1: + print("CPMT: Export failed because there's no export methods set.") + return False + else: + for key in sizesList.keys(): + self.pagesLocationList[key] = [] + + # Get the appropriate paths. + path = Path(self.projectURL) + exportPath = path / self.configDictionary["exportLocation"] + pagesList = self.configDictionary["pages"] + fileName = str(exportPath) + + # Create a progress dialog. + progress = QProgressDialog("Preparing export.", str(), 0, len(pagesList)) + progress.setWindowTitle("Exporting comic...") + progress.setCancelButton(None) + timer = QElapsedTimer() + timer.start() + + for p in range(0, len(pagesList)): + + # Update the label in the progress dialog. + progress.setValue(p) + timePassed = timer.elapsed() + if (p > 0): + timeEstimated = (len(pagesList) - p) * (timePassed / p) + passedString = str(int(timePassed / 60000)) + ":" + format(int(timePassed / 1000), "02d") + ":" + format(timePassed % 1000, "03d") + estimatedString = str(int(timeEstimated / 60000)) + ":" + format(int(timeEstimated / 1000), "02d") + ":" + format(int(timeEstimated % 1000), "03d") + progress.setLabelText(str(i18n("{pages} of {pagesTotal} done. \nTime passed: {passedString}:\n Estimated:{estimated}")).format(pages=p, pagesTotal=len(pagesList), passedString=passedString, estimated=estimatedString)) + + # Get the appropriate url and open the page. + url = os.path.join(self.projectURL, pagesList[p]) + page = Application.openDocument(url) + + # remove layers and flatten. + labelList = self.configDictionary["labelsToRemove"] + root = page.rootNode() + self.removeLayers(labelList, node=root) + page.refreshProjection() + page.flatten() + while page.isIdle() is False: + page.waitForDone() + + # Start making the format specific copy. + if page.isIdle(): + for key in sizesList.keys(): + w = sizesList[key] + # copy over data + projection = Application.createDocument(page.width(), page.height(), page.name(), page.colorModel(), page.colorDepth(), page.colorProfile()) + batchsave = Application.batchmode() + Application.setBatchmode(True) + projection.activeNode().setPixelData(page.pixelData(0, 0, page.width(), page.height()), 0, 0, page.width(), page.height()) + + # Crop. Cropping per guide only happens if said guides have been found. + if w["Crop"] is True: + listHGuides = page.horizontalGuides() + listHGuides.sort() + listVGuides = page.verticalGuides() + listVGuides.sort() + if self.configDictionary["cropToGuides"] and len(listVGuides) > 1: + cropx = listVGuides[0] + cropw = listVGuides[-1] - cropx + else: + cropx = self.configDictionary["cropLeft"] + cropw = page.width() - self.configDictionary["cropRight"] - cropx + if self.configDictionary["cropToGuides"] and len(listHGuides) > 1: + cropy = listHGuides[0] + croph = listHGuides[-1] - cropy + else: + cropy = self.configDictionary["cropTop"] + croph = page.height() - self.configDictionary["cropBottom"] - cropy + projection.crop(cropx, cropy, cropw, croph) + projection.waitForDone() + + # resize appropriately + res = page.resolution() + listScales = [projection.width(), projection.height(), res, res] + sizesCalc = sizesCalculator() + listScales = sizesCalc.get_scale_from_resize_config(config=w, listSizes=listScales) + projection.scaleImage(listScales[0], listScales[1], listScales[2], listScales[3], "bicubic") + projection.waitForDone() + + # png, gif and other webformats should probably be in 8bit srgb at maximum. + if key is not "TIFF": + if projection.colorModel() is not "RGBA" or projection.colorModel() is not "GRAYA" or projection.colorDepth() is not "U8": + projection.setColorSpace("RGBA", "U8", "sRGB built-in") + projection.refreshProjection() + else: + # Tiff on the other hand can handle all the colormodels, but can only handle integer bit depths. + # Tiff is intended for print output, and 16 bit integer will be sufficient. + if projection.colorDepth() is not "U8" or projection.colorDepth() is not "U16": + projection.setColorSpace(page.colorModel(), "U16", page.colorProfile()) + projection.refreshProjection() + + # save + # Make sure the folder name for this export exists. It'll allow us to keep the + # export folders nice and clean. + folderName = str(key + "-" + w["FileType"]) + if Path(exportPath / folderName).exists() is False: + Path.mkdir(exportPath / folderName) + # Get a nice and descriptive fle name. + fn = os.path.join(str(Path(exportPath / folderName)), "page_" + format(p, "03d") + "_" + str(listScales[0]) + "x" + str(listScales[1]) + "." + w["FileType"]) + # Finally save and add the page to a list of pages. This will make it easy for the packaging function to + # find the pages and store them. + if projection.isIdle(): + projection.activeNode().save(fn, projection.resolution(), projection.resolution()) + projection.waitForDone() + self.pagesLocationList[key].append(fn) + + # close + Application.setBatchmode(batchsave) + projection.close() + page.close() + progress.setValue(len(pagesList)) + # TODO: Check what or whether memory leaks are still caused and otherwise remove the entry below. + print("CPMT: Export has finished. There are memory leaks, but the source is not obvious due wild times on git master. Last attempt to fix was august 2017") + return True + print("CPMT: Export not happening because there aren't any pages.") + return False + + """ + Function to remove layers when they have the given labels. + + If not, but the node does have children, check those too. + """ + + def removeLayers(self, labels, node): + if node.colorLabel() in labels: + node.remove() + else: + if len(node.childNodes()) > 0: + for child in node.childNodes(): + self.removeLayers(labels, node=child) + + """ + Write the Advanced Comic Book Data xml file. + + http://acbf.wikia.com/wiki/ACBF_Specifications + + """ + + def write_acbf_meta_data(self): + acbfGenreList = ["science_fiction", "fantasy", "adventure", "horror", "mystery", "crime", "military", "real_life", "superhero", "humor", "western", "manga", "politics", "caricature", "sports", "history", "biography", "education", "computer", "religion", "romance", "children", "non-fiction", "adult", "alternative", "other"] + acbfAuthorRolesList = ["Writer", "Adapter", "Artist", "Penciller", "Inker", "Colorist", "Letterer", "Cover Artist", "Photographer", "Editor", "Assistant Editor", "Translator", "Other"] + title = self.configDictionary["projectName"] + if "title" in self.configDictionary.keys(): + title = self.configDictionary["title"] + location = str(os.path.join(self.projectURL, self.configDictionary["exportLocation"], "metadata", title + ".acbf")) + document = ET.ElementTree() + root = ET.Element("ACBF") + root.set("xmlns", "http://www.fictionbook-lib.org/xml/acbf/1.0") + document._setroot(root) + + meta = ET.Element("meta-data") + + bookInfo = ET.Element("book-info") + if "authorList" in self.configDictionary.keys(): + for authorE in range(len(self.configDictionary["authorList"])): + author = ET.Element("author") + authorDict = self.configDictionary["authorList"][authorE] + if "first-name" in authorDict.keys(): + authorN = ET.Element("first-name") + authorN.text = str(authorDict["first-name"]) + author.append(authorN) + if "last-name" in authorDict.keys(): + authorN = ET.Element("last-name") + authorN.text = str(authorDict["last-name"]) + author.append(authorN) + if "initials" in authorDict.keys(): + authorN = ET.Element("middle-name") + authorN.text = str(authorDict["initials"]) + author.append(authorN) + if "nickname" in authorDict.keys(): + authorN = ET.Element("nickname") + authorN.text = str(authorDict["nickname"]) + author.append(authorN) + if "homepage" in authorDict.keys(): + authorN = ET.Element("homepage") + authorN.text = str(authorDict["homepage"]) + author.append(authorN) + if "email" in authorDict.keys(): + authorN = ET.Element("email") + authorN.text = str(authorDict["email"]) + author.append(authorN) + if "role" in authorDict.keys(): + if str(authorDict["role"]).title() in acbfAuthorRolesList: + author.set("activity", str(authorDict["role"])) + bookInfo.append(author) + bookTitle = ET.Element("book-title") + if "title" in self.configDictionary.keys(): + bookTitle.text = str(self.configDictionary["title"]) + else: + bookTitle.text = "Comic with no Name" + bookInfo.append(bookTitle) + extraGenres = [] + if "genre" in self.configDictionary.keys(): + for genre in self.configDictionary["genre"]: + genreModified = str(genre).lower() + genreModified.replace(" ", "_") + if genreModified in acbfGenreList: + bookGenre = ET.Element("genre") + bookGenre.text = genreModified + bookInfo.append(bookGenre) + else: + extraGenres.append(genre) + annotation = ET.Element("annotation") + if "summary" in self.configDictionary.keys(): + paragraphList = str(self.configDictionary["summary"]).split("\n") + for para in paragraphList: + p = ET.Element("p") + p.text = para + annotation.append(p) + else: + p = ET.Element("p") + p.text = "There was no summary upon generation of this file." + annotation.append(p) + bookInfo.append(annotation) + + if "characters" in self.configDictionary.keys(): + character = ET.Element("characters") + for name in self.configDictionary["characters"]: + char = ET.Element("name") + char.text = name + character.append(char) + bookInfo.append(character) + + keywords = ET.Element("keywords") + stringKeywordsList = [] + for key in extraGenres: + stringKeywordsList.append(str(key)) + if "otherKeywords" in self.configDictionary.keys(): + for key in self.configDictionary["otherKeywords"]: + stringKeywordsList.append(str(key)) + if "format" in self.configDictionary.keys(): + for key in self.configDictionary["format"]: + stringKeywordsList.append(str(key)) + keywords.text = ", ".join(stringKeywordsList) + bookInfo.append(keywords) + + coverpageurl = "" + coverpage = ET.Element("coverpage") + if "pages" in self.configDictionary.keys(): + if "cover" in self.configDictionary.keys(): + pageList = [] + pageList = self.configDictionary["pages"] + coverNumber = pageList.index(self.configDictionary["cover"]) + image = ET.Element("image") + if len(self.pagesLocationList["CBZ"]) >= coverNumber: + coverpageurl = self.pagesLocationList["CBZ"][coverNumber] + image.set("href", os.path.basename(coverpageurl)) + coverpage.append(image) + bookInfo.append(coverpage) + + if "language" in self.configDictionary.keys(): + language = ET.Element("languages") + textlayer = ET.Element("text-layer") + textlayer.set("lang", self.configDictionary["language"]) + textlayer.set("show", "False") + language.append(textlayer) + bookInfo.append(language) + #database = ET.Element("databaseref") + # bookInfo.append(database) + + if "seriesName" in self.configDictionary.keys(): + sequence = ET.Element("sequence") + sequence.set("title", self.configDictionary["seriesName"]) + if "seriesVolume" in self.configDictionary.keys(): + sequence.set("volume", str(self.configDictionary["seriesVolume"])) + if "seriesNumber" in self.configDictionary.keys(): + sequence.text = str(self.configDictionary["seriesNumber"]) + else: + sequence.text = 0 + bookInfo.append(sequence) + contentrating = ET.Element("content-rating") + + if "rating" in self.configDictionary.keys(): + contentrating.text = self.configDictionary["rating"] + else: + contentrating.text = "Unrated." + if "ratingSystem" in self.configDictionary.keys(): + contentrating.set("type", self.configDictionary["ratingSystem"]) + bookInfo.append(contentrating) + meta.append(bookInfo) + + publisherInfo = ET.Element("publish-info") + if "publisherName" in self.configDictionary.keys(): + publisherName = ET.Element("publisher") + publisherName.text = self.configDictionary["publisherName"] + publisherInfo.append(publisherName) + if "publishingDate" in self.configDictionary.keys(): + publishingDate = ET.Element("publish-date") + publishingDate.set("value", self.configDictionary["publishingDate"]) + publishingDate.text = QDate.fromString(self.configDictionary["publishingDate"], Qt.ISODate).toString(Qt.SystemLocaleLongDate) + publisherInfo.append(publishingDate) + if "publisherCity" in self.configDictionary.keys(): + publishCity = ET.Element("city") + publishCity.text = self.configDictionary["publisherCity"] + publisherInfo.append(publishCity) + if "isbn-number" in self.configDictionary.keys(): + publishISBN = ET.Element("isbn") + publishISBN.text = self.configDictionary["isbn-number"] + publisherInfo.append(publishISBN) + if "license" in self.configDictionary.keys(): + license = self.configDictionary["license"] + if license.isspace() is False and len(license) > 0: + publishLicense = ET.Element("license") + publishLicense.text = self.configDictionary["license"] + publisherInfo.append(publishLicense) + + meta.append(publisherInfo) + + documentInfo = ET.Element("document-info") + acbfAuthor = ET.Element("author") + if "acbfAuthor" in self.configDictionary.keys(): + acbfAuthor.text = self.configDictionary["acbfAuthor"] + else: + acbfAuthor.text = "Anon" + documentInfo.append(acbfAuthor) + + acbfDate = ET.Element("creation-date") + now = QDate.currentDate() + acbfDate.set("value", now.toString(Qt.ISODate)) + acbfDate.text = now.toString(Qt.SystemLocaleLongDate) + documentInfo.append(acbfDate) + + acbfSource = ET.Element("source") + if "acbfSource" in self.configDictionary.keys(): + acbfSource.text = self.configDictionary["acbfSource"] + documentInfo.append(acbfSource) + + acbfID = ET.Element("id") + if "acbfID" in self.configDictionary.keys(): + acbfID.text = self.configDictionary["acbfID"] + documentInfo.append(acbfID) + + acbfVersion = ET.Element("version") + if "acbfVersion" in self.configDictionary.keys(): + acbfVersion.text = str(self.configDictionary["acbfVersion"]) + documentInfo.append(acbfVersion) + + acbfHistory = ET.Element("history") + if "acbfHistory" in self.configDictionary.keys(): + for h in self.configDictionary["acbfHistory"]: + p = ET.Element("p") + p.text = h + acbfHistory.append(p) + documentInfo.append(acbfHistory) + meta.append(documentInfo) + + root.append(meta) + + body = ET.Element("body") + + for page in self.pagesLocationList["CBZ"]: + if page is not coverpageurl: + pg = ET.Element("page") + image = ET.Element("image") + image.set("href", os.path.basename(page)) + pg.append(image) + body.append(pg) + + root.append(body) + + document.write(location, encoding="UTF-8", xml_declaration=True) + self.acbfLocation = location + return True + + """ + Write a CoMet xml file to url + """ + + def write_comet_meta_data(self): + title = self.configDictionary["projectName"] + if "title" in self.configDictionary.keys(): + title = self.configDictionary["title"] + location = str(os.path.join(self.projectURL, self.configDictionary["exportLocation"], "metadata", title + " CoMet.xml")) + document = ET.ElementTree() + root = ET.Element("comet") + root.set("xmlns:comet", "http://www.denvog.com/comet/") + root.set("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance") + root.set("xsi:schemaLocation", "http://www.denvog.com http://www.denvog.com/comet/comet.xsd") + document._setroot(root) + + title = ET.Element("title") + if "title" in self.configDictionary.keys(): + title.text = self.configDictionary["title"] + else: + title.text = "Untitled Comic" + root.append(title) + description = ET.Element("description") + if "summary" in self.configDictionary.keys(): + description.text = self.configDictionary["summary"] + else: + description.text = "There was no summary upon generation of this file." + root.append(description) + if "seriesName" in self.configDictionary.keys(): + series = ET.Element("series") + series.text = self.configDictionary["seriesName"] + root.append(series) + if "seriesNumber" in self.configDictionary.keys(): + issue = ET.Element("issue") + issue.text = str(self.configDictionary["seriesName"]) + root.append(issue) + if "seriesVolume" in self.configDictionary.keys(): + volume = ET.Element("volume") + volume.text = str(self.configDictionary["seriesVolume"]) + root.append(volume) + + if "publisherName" in self.configDictionary.keys(): + pubisher = ET.Element("publisher") + pubisher.text = self.configDictionary["publisherName"] + root.append(pubisher) + + if "publishingDate" in self.configDictionary.keys(): + date = ET.Element("date") + date.text = self.configDictionary["publishingDate"] + root.append(date) + + if "genre" in self.configDictionary.keys(): + for genreE in self.configDictionary["genre"]: + genre = ET.Element("genre") + genre.text = genreE + root.append(genre) + + if "characters" in self.configDictionary.keys(): + for char in self.configDictionary["characters"]: + character = ET.Element("character") + character.text = char + root.append(character) + + if "format" in self.configDictionary.keys(): + format = ET.Element("format") + format.text = ",".join(self.configDictionary["format"]) + root.append(format) + + if "language" in self.configDictionary.keys(): + language = ET.Element("language") + language.text = self.configDictionary["language"] + root.append(language) + if "rating" in self.configDictionary.keys(): + rating = ET.Element("rating") + rating.text = self.configDictionary["rating"] + root.append(rating) + #rights = ET.Element("rights") + if "pages" in self.configDictionary.keys(): + pages = ET.Element("pages") + pages.text = str(len(self.configDictionary["pages"])) + root.append(pages) + + if "isbn-number" in self.configDictionary.keys(): + identifier = ET.Element("identifier") + identifier.text = self.configDictionary["isbn-number"] + root.append(identifier) + + if "authorList" in self.configDictionary.keys(): + for authorE in range(len(self.configDictionary["authorList"])): + author = ET.Element("creator") + authorDict = self.configDictionary["authorList"][authorE] + if "role" in authorDict.keys(): + if str(authorDict["role"]).lower() in ["writer", "penciller", "editor", "assistant editor", "cover artist", "letterer", "inker", "colorist"]: + if str(authorDict["role"]).lower() is "cover artist": + author = ET.Element("coverDesigner") + elif str(authorDict["role"]).lower() is "assistant editor": + author = ET.Element("editor") + else: + author = ET.Element(str(authorDict["role"]).lower()) + stringName = [] + if "last-name" in authorDict.keys(): + stringName.append(authorDict["last-name"]) + if "first-name" in authorDict.keys(): + stringName.append(authorDict["first-name"]) + if "last-name" in authorDict.keys(): + stringName.append("(" + authorDict["nickname"] + ")") + author.text = ",".join(stringName) + root.append(author) + + if "pages" in self.configDictionary.keys(): + if "cover" in self.configDictionary.keys(): + pageList = [] + pageList = self.configDictionary["pages"] + coverNumber = pageList.index(self.configDictionary["cover"]) + if len(self.pagesLocationList["CBZ"]) >= coverNumber: + coverImage = ET.Element("coverImage") + coverImage.text = os.path.basename(self.pagesLocationList["CBZ"][coverNumber]) + root.append(coverImage) + readingDirection = ET.Element("readingDirection") + readingDirection.text = "ltr" + if "readingDirection" in self.configDictionary.keys(): + if self.configDictionary["readingDirection"] is "rightToLeft": + readingDirection.text = "rtl" + root.append(readingDirection) + + document.write(location, encoding="UTF-8", xml_declaration=True) + self.cometLocation = location + return True + """ + The comicrack information is sorta... incomplete, so no idea if the following is right... + I can't check in any case: It is a windows application. + """ + + def write_comic_rack_info(self): + location = str(os.path.join(self.projectURL, self.configDictionary["exportLocation"], "metadata", "ComicInfo.xml")) + document = ET.ElementTree() + root = ET.Element("ComicInfo") + root.set("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance") + root.set("xmlns:xsd", "http://www.w3.org/2001/XMLSchema") + + title = ET.Element("Title") + if "title" in self.configDictionary.keys(): + title.text = self.configDictionary["title"] + else: + title.text = "Untitled Comic" + root.append(title) + description = ET.Element("Summary") + if "summary" in self.configDictionary.keys(): + description.text = self.configDictionary["summary"] + else: + description.text = "There was no summary upon generation of this file." + root.append(description) + if "seriesNumber" in self.configDictionary.keys(): + number = ET.Element("Number") + number.text = str(self.configDictionary["seriesNumber"]) + root.append(number) + + if "publishingDate" in self.configDictionary.keys(): + date = QDate.fromString(self.configDictionary["publishingDate"], Qt.ISODate) + publishYear = ET.Element("Year") + publishYear.text = str(date.year()) + publishMonth = ET.Element("Month") + publishMonth.text = str(date.month()) + root.append(publishYear) + root.append(publishMonth) + + if "format" in self.configDictionary.keys(): + for form in self.configDictionary["format"]: + formattag = ET.Element("Format") + formattag.text = str(form) + root.append(formattag) + if "otherKeywords" in self.configDictionary.keys(): + tags = ET.Element("Tags") + tags.text = ", ".join(self.configDictionary["otherKeywords"]) + root.append(tags) + + if "authorList" in self.configDictionary.keys(): + for authorE in range(len(self.configDictionary["authorList"])): + author = ET.Element("Writer") + authorDict = self.configDictionary["authorList"][authorE] + if "role" in authorDict.keys(): + if str(authorDict["role"]).lower() in ["writer", "penciller", "editor", "assistant editor", "cover artist", "letterer", "inker", "colorist"]: + if str(authorDict["role"]).lower() is "cover artist": + author = ET.Element("CoverArtist") + elif str(authorDict["role"]).lower() is "assistant editor": + author = ET.Element("Editor") + else: + author = ET.Element(str(authorDict["role"]).title()) + stringName = [] + if "last-name" in authorDict.keys(): + stringName.append(authorDict["last-name"]) + if "first-name" in authorDict.keys(): + stringName.append(authorDict["first-name"]) + if "last-name" in authorDict.keys(): + stringName.append("(" + authorDict["nickname"] + ")") + author.text = ",".join(stringName) + root.append(author) + if "publisherName" in self.configDictionary.keys(): + publisher = ET.Element("Publisher") + publisher.text = self.configDictionary["publisherName"] + root.append(publisher) + + if "genre" in self.configDictionary.keys(): + for genreE in self.configDictionary["genre"]: + genre = ET.Element("Genre") + genre.text = genreE + root.append(genre) + blackAndWhite = ET.Element("BlackAndWhite") + blackAndWhite.text = "No" + root.append(blackAndWhite) + readingDirection = ET.Element("Manga") + readingDirection.text = "No" + if "readingDirection" in self.configDictionary.keys(): + if self.configDictionary["readingDirection"] is "rightToLeft": + readingDirection.text = "Yes" + root.append(readingDirection) + + if "characters" in self.configDictionary.keys(): + for char in self.configDictionary["characters"]: + character = ET.Element("Character") + character.text = char + root.append(character) + if "pages" in self.configDictionary.keys(): + pagecount = ET.Element("PageCount") + pagecount.text = str(len(self.configDictionary["pages"])) + root.append(pagecount) + pages = ET.Element("Pages") + covernumber = 0 + if "pages" in self.configDictionary.keys() and "cover" in self.configDictionary.keys(): + covernumber = self.configDictionary["pages"].index(self.configDictionary["cover"]) + for i in range(len(self.pagesLocationList["CBZ"])): + page = ET.Element("Page") + page.set("Image", str(i)) + if i is covernumber: + page.set("Type", "FrontCover") + pages.append(page) + root.append(pages) + document._setroot(root) + document.write(location, encoding="UTF-8", xml_declaration=True) + self.comicRackInfo = location + return True + """ + Another metadata format but then a json dump stored into the zipfile comment. + Doesn't seem to be supported much. :/ + https://code.google.com/archive/p/comicbookinfo/wikis/Example.wiki + """ + + def write_comic_book_info_json(self): + self.comic_book_info_json_dump = str() + + basedata = {} + metadata = {} + authorList = [] + taglist = [] + + if "authorList" in self.configDictionary.keys(): + for authorE in range(len(self.configDictionary["authorList"])): + author = {} + + authorDict = self.configDictionary["authorList"][authorE] + stringName = [] + if "last-name" in authorDict.keys(): + stringName.append(authorDict["last-name"]) + if "first-name" in authorDict.keys(): + stringName.append(authorDict["first-name"]) + if "nickname" in authorDict.keys(): + stringName.append("(" + authorDict["nickname"] + ")") + author["person"] = ",".join(stringName) + if "role" in authorDict.keys(): + author["role"] = str(authorDict["role"]).title() + authorList.append(author) + if "characters" in self.configDictionary.keys(): + for character in self.configDictionary["characters"]: + taglist.append(character) + if "format" in self.configDictionary.keys(): + for item in self.configDictionary["format"]: + taglist.append(item) + if "otherKeywords" in self.configDictionary.keys(): + for item in self.configDictionary["otherKeywords"]: + taglist.append(item) + + if "seriesName" in self.configDictionary.keys(): + metadata["series"] = self.configDictionary["seriesName"] + if "title" in self.configDictionary.keys(): + metadata["title"] = self.configDictionary["title"] + else: + metadata["title"] = "Unnamed comic" + if "publisherName" in self.configDictionary.keys(): + metadata["publisher"] = self.configDictionary["publisherName"] + if "publishingDate" in self.configDictionary.keys(): + date = QDate.fromString(self.configDictionary["publishingDate"], Qt.ISODate) + metadata["publicationMonth"] = date.month() + metadata["publicationYear"] = date.year() + if "seriesNumber" in self.configDictionary.keys(): + metadata["issue"] = self.configDictionary["seriesNumber"] + if "seriesVolume" in self.configDictionary.keys(): + metadata["volume"] = self.configDictionary["seriesVolume"] + if "genre" in self.configDictionary.keys(): + metadata["genre"] = self.configDictionary["genre"] + if "language" in self.configDictionary.keys(): + metadata["language"] = QLocale.languageToString(QLocale(self.configDictionary["language"]).language()) + + metadata["credits"] = authorList + + metadata["tags"] = taglist + if "summary" in self.configDictionary.keys(): + metadata["comments"] = self.configDictionary["summary"] + else: + metadata["comments"] = "File generated without summary" + + basedata["appID"] = "Krita" + basedata["lastModified"] = QDateTime.currentDateTimeUtc().toString(Qt.ISODate) + basedata["ComicBookInfo/1.0"] = metadata + + self.comic_book_info_json_dump = json.dumps(basedata) + return True + + """ + package cbz puts all the meta-data and relevant files into an zip file ending with ".cbz" + """ + + def package_cbz(self): + + # Use the project name if there's no title to avoid sillyness with unnamed zipfiles. + title = self.configDictionary["projectName"] + if "title" in self.configDictionary.keys(): + title = self.configDictionary["title"] + + # Get the appropriate path. + url = os.path.join(self.projectURL, self.configDictionary["exportLocation"], title + ".cbz") + + # Create a zip file. + cbzArchive = zipfile.ZipFile(url, mode="w", compression=zipfile.ZIP_STORED) + + # Add all the meta data files. + cbzArchive.write(self.acbfLocation, os.path.basename(self.acbfLocation)) + cbzArchive.write(self.cometLocation, os.path.basename(self.cometLocation)) + cbzArchive.write(self.comicRackInfo, os.path.basename(self.comicRackInfo)) + cbzArchive.comment = self.comic_book_info_json_dump.encode("utf-8") + + # Add the pages. + if "CBZ" in self.pagesLocationList.keys(): + for page in self.pagesLocationList["CBZ"]: + if (os.path.exists(page)): + cbzArchive.write(page, os.path.basename(page)) + + # Close the zip file when done. + cbzArchive.close() + + """ + package epub packages the whole epub folder and renames the zip file to .epub. + """ + + def package_epub(self): + + # Use the project name if there's no title to avoid sillyness with unnamed zipfiles. + title = self.configDictionary["projectName"] + if "title" in self.configDictionary.keys(): + title = self.configDictionary["title"] + + # Get the appropriate paths. + url = os.path.join(self.projectURL, self.configDictionary["exportLocation"], title) + epub = os.path.join(self.projectURL, self.configDictionary["exportLocation"], "EPUB-files") + + # Make the archive. + shutil.make_archive(base_name=url, format="zip", root_dir=epub) + + # Rename the archive to epub. + shutil.move(src=str(url + ".zip"), dst=str(url + ".epub")) diff --git a/plugins/extensions/pykrita/plugin/plugins/comics_project_management_tools/comics_metadata_dialog.py b/plugins/extensions/pykrita/plugin/plugins/comics_project_management_tools/comics_metadata_dialog.py new file mode 100644 index 0000000000..153004abdf --- /dev/null +++ b/plugins/extensions/pykrita/plugin/plugins/comics_project_management_tools/comics_metadata_dialog.py @@ -0,0 +1,596 @@ +""" +Part of the comics project management tools (CPMT). + +This is a metadata editor that helps out setting the proper metadata +""" +import sys +import os # For finding the script location. +import csv +from pathlib import Path # For reading all the files in a directory. +from PyQt5.QtGui import QStandardItem, QStandardItemModel +from PyQt5.QtWidgets import QComboBox, QCompleter, QStyledItemDelegate, QLineEdit, QDialog, QDialogButtonBox, QVBoxLayout, QFormLayout, QTabWidget, QWidget, QPlainTextEdit, QHBoxLayout, QSpinBox, QDateEdit, QPushButton, QLabel, QTableView +from PyQt5.QtCore import QDir, QLocale, QStringListModel, Qt, QDate +""" +mult entry completer cobbled together from the two examples on stackoverflow:3779720 + +This allows us to let people type in comma seperated lists and get completion for those. +""" + + +class multi_entry_completer(QCompleter): + punctuation = "," + + def __init__(self, parent=None): + super(QCompleter, self).__init__(parent) + + def pathFromIndex(self, index): + path = QCompleter.pathFromIndex(self, index) + string = str(self.widget().text()) + split = string.split(self.punctuation) + if len(split) > 1: + path = "%s, %s" % (",".join(split[:-1]), path) + return path + + def splitPath(self, path): + split = str(path.split(self.punctuation)[-1]) + if split.startswith(" "): + split = split[1:] + if split.endswith(" "): + split = split[:-1] + return [split] + + +""" +Language combobox that can take locale codes and get the right language for it and visa-versa. +""" + + +class language_combo_box(QComboBox): + languageList = [] + codesList = [] + + def __init__(self, parent=None): + super(QComboBox, self).__init__(parent) + mainP = os.path.dirname(__file__) + languageP = os.path.join(mainP, "isoLanguagesList.csv") + if (os.path.exists(languageP)): + file = open(languageP, "r", newline="", encoding="utf8") + languageReader = csv.reader(file) + for row in languageReader: + self.languageList.append(row[0]) + self.codesList.append(row[1]) + self.addItem(row[0]) + file.close() + + def codeForCurrentEntry(self): + if self.currentText() in self.languageList: + return self.codesList[self.languageList.index(self.currentText())] + + def setEntryToCode(self, code): + if (code == "C" and "en" in self.codesList): + self.setCurrentIndex(self.codesList.index("en")) + if code in self.codesList: + self.setCurrentIndex(self.codesList.index(code)) + + +""" +A combobox that fills up with licenses from a CSV, and also sets tooltips from that +csv. +""" + + +class license_combo_box(QComboBox): + def __init__(self, parent=None): + super(QComboBox, self).__init__(parent) + mainP = os.path.dirname(__file__) + languageP = os.path.join(mainP, "LicenseList.csv") + model = QStandardItemModel() + if (os.path.exists(languageP)): + file = open(languageP, "r", newline="", encoding="utf8") + languageReader = csv.reader(file) + for row in languageReader: + license = QStandardItem(row[0]) + license.setToolTip(row[1]) + model.appendRow(license) + file.close() + self.setModel(model) + + +""" +Allows us to set completers on the author roles. +""" + + +class author_delegate(QStyledItemDelegate): + completerStrings = [] + completerColumn = 0 + + def __init__(self, parent=None): + super(QStyledItemDelegate, self).__init__(parent) + + def setCompleterData(self, completerStrings, completerColumn): + self.completerStrings = completerStrings + self.completerColumn = completerColumn + + def createEditor(self, parent, option, index): + editor = QLineEdit(parent) + if index.column() == self.completerColumn: + editor.setCompleter(QCompleter(self.completerStrings)) + editor.completer().setCaseSensitivity(False) + return editor + + +""" +A comic project metadata editing dialog that can take our config diactionary and set all the relevant information. + +To help our user, the dialog loads up lists of keywords to populate several autocompletion methods. +""" + + +class comic_meta_data_editor(QDialog): + configGroup = "ComicsProjectManagementTools" + + def __init__(self): + super().__init__() + # Get the keys for the autocompletion. + self.genreKeysList = [] + self.characterKeysList = [] + self.ratingKeysList = {} + self.formatKeysList = [] + self.otherKeysList = [] + self.authorRoleList = [] + mainP = Path(os.path.abspath(__file__)).parent + self.get_auto_completion_keys(mainP) + extraKeyP = Path(QDir.homePath()) / Application.readSetting(self.configGroup, "extraKeysLocation", str()) + self.get_auto_completion_keys(extraKeyP) + + # Setup the dialog. + self.setLayout(QVBoxLayout()) + mainWidget = QTabWidget() + self.layout().addWidget(mainWidget) + self.setWindowTitle(i18n("Comic Metadata")) + buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) + self.layout().addWidget(buttons) + buttons.accepted.connect(self.accept) + buttons.rejected.connect(self.reject) + + # Title, concept, summary, genre, characters, format, rating, language, series, other keywords + metadataPage = QWidget() + mformLayout = QFormLayout() + metadataPage.setLayout(mformLayout) + + self.lnTitle = QLineEdit() + self.lnTitle.setToolTip(i18n("The proper title of the comic.")) + + self.teSummary = QPlainTextEdit() + self.teSummary.setToolTip(i18n("What will you tell others to entice them to read your comic?")) + + self.lnGenre = QLineEdit() + genreCompletion = multi_entry_completer() + genreCompletion.setModel(QStringListModel(self.genreKeysList)) + self.lnGenre.setCompleter(genreCompletion) + genreCompletion.setCaseSensitivity(False) + self.lnGenre.setToolTip(i18n("The genre of the work. Prefilled values are from the ACBF, but you can fill in your own. Seperate genres with commas. Try to limit the amount to about two or three")) + + self.lnCharacters = QLineEdit() + characterCompletion = multi_entry_completer() + characterCompletion.setModel(QStringListModel(self.characterKeysList)) + characterCompletion.setCaseSensitivity(False) + characterCompletion.setFilterMode(Qt.MatchContains) # So that if there is a list of names with last names, people can type in a last name. + self.lnCharacters.setCompleter(characterCompletion) + self.lnCharacters.setToolTip(i18n("The names of the characters that this comic revolves around. Comma seperate.")) + + self.lnFormat = QLineEdit() + formatCompletion = multi_entry_completer() + formatCompletion.setModel(QStringListModel(self.formatKeysList)) + formatCompletion.setCaseSensitivity(False) + self.lnFormat.setCompleter(formatCompletion) + + ratingLayout = QHBoxLayout() + self.cmbRatingSystem = QComboBox() + self.cmbRatingSystem.addItems(self.ratingKeysList.keys()) + self.cmbRatingSystem.setEditable(True) + self.cmbRating = QComboBox() + self.cmbRating.setEditable(True) + self.cmbRatingSystem.currentIndexChanged.connect(self.slot_refill_ratings) + ratingLayout.addWidget(self.cmbRatingSystem) + ratingLayout.addWidget(self.cmbRating) + + self.lnSeriesName = QLineEdit() + self.lnSeriesName.setToolTip(i18n("If this is part of a series, enter the name of the series and the number.")) + self.spnSeriesNumber = QSpinBox() + self.spnSeriesNumber.setPrefix("No. ") + self.spnSeriesVol = QSpinBox() + self.spnSeriesVol.setPrefix("Vol. ") + seriesLayout = QHBoxLayout() + seriesLayout.addWidget(self.lnSeriesName) + seriesLayout.addWidget(self.spnSeriesVol) + seriesLayout.addWidget(self.spnSeriesNumber) + + otherCompletion = multi_entry_completer() + otherCompletion.setModel(QStringListModel(self.otherKeysList)) + otherCompletion.setCaseSensitivity(False) + otherCompletion.setFilterMode(Qt.MatchContains) + self.lnOtherKeywords = QLineEdit() + self.lnOtherKeywords.setCompleter(otherCompletion) + self.lnOtherKeywords.setToolTip(i18n("Other keywords that don't fit in the previously mentioned sets. As always, comma seperate")) + + self.cmbLanguage = language_combo_box() + self.cmbReadingMode = QComboBox() + self.cmbReadingMode.addItem(i18n("Left to Right")) + self.cmbReadingMode.addItem(i18n("Right to Left")) + + self.cmbCoverPage = QComboBox() + self.cmbCoverPage.setToolTip(i18n("Which page is the cover page? This will be empty if there's no pages.")) + + mformLayout.addRow(i18n("Title:"), self.lnTitle) + mformLayout.addRow(i18n("Cover Page:"), self.cmbCoverPage) + mformLayout.addRow(i18n("Summary:"), self.teSummary) + mformLayout.addRow(i18n("Language:"), self.cmbLanguage) + mformLayout.addRow(i18n("Reading Direction:"), self.cmbReadingMode) + mformLayout.addRow(i18n("Genre:"), self.lnGenre) + mformLayout.addRow(i18n("Characters:"), self.lnCharacters) + mformLayout.addRow(i18n("Format:"), self.lnFormat) + mformLayout.addRow(i18n("Rating:"), ratingLayout) + mformLayout.addRow(i18n("Series:"), seriesLayout) + mformLayout.addRow(i18n("Other:"), self.lnOtherKeywords) + + mainWidget.addTab(metadataPage, i18n("Work")) + + # The page for the authors. + authorPage = QWidget() + authorPage.setLayout(QVBoxLayout()) + explaination = QLabel(i18n("The following is a table of the authors that contributed to this comic. You can set their nickname, proper names (first, middle, last), Role (Penciller, Inker, etc), email and homepage.")) + explaination.setWordWrap(True) + self.authorModel = QStandardItemModel(0, 7) + labels = [i18n("Nick Name"), i18n("Given Name"), i18n("Middle Name"), i18n("Family Name"), i18n("Role"), i18n("Email"), i18n("Homepage")] + self.authorModel.setHorizontalHeaderLabels(labels) + self.authorTable = QTableView() + self.authorTable.setModel(self.authorModel) + self.authorTable.verticalHeader().setDragEnabled(True) + self.authorTable.verticalHeader().setDropIndicatorShown(True) + self.authorTable.verticalHeader().setSectionsMovable(True) + self.authorTable.verticalHeader().sectionMoved.connect(self.slot_reset_author_row_visual) + delegate = author_delegate() + delegate.setCompleterData(self.authorRoleList, 4) + self.authorTable.setItemDelegate(delegate) + author_button_layout = QWidget() + author_button_layout.setLayout(QHBoxLayout()) + btn_add_author = QPushButton(i18n("Add Author")) + btn_add_author.clicked.connect(self.slot_add_author) + btn_remove_author = QPushButton(i18n("Remove Author")) + btn_remove_author.clicked.connect(self.slot_remove_author) + author_button_layout.layout().addWidget(btn_add_author) + author_button_layout.layout().addWidget(btn_remove_author) + authorPage.layout().addWidget(explaination) + authorPage.layout().addWidget(self.authorTable) + authorPage.layout().addWidget(author_button_layout) + mainWidget.addTab(authorPage, i18n("Authors")) + + # The page with publisher information. + publisherPage = QWidget() + publisherLayout = QFormLayout() + publisherPage.setLayout(publisherLayout) + self.publisherName = QLineEdit() + self.publisherName.setToolTip(i18n("The name of the company, group or person who is responsible for the final version the reader gets.")) + publishDateLayout = QHBoxLayout() + self.publishDate = QDateEdit() + self.publishDate.setDisplayFormat(QLocale().system().dateFormat()) + currentDate = QPushButton(i18n("Set Today")) + currentDate.setToolTip(i18n("Sets the publish date to the current date.")) + currentDate.clicked.connect(self.slot_set_date) + publishDateLayout.addWidget(self.publishDate) + publishDateLayout.addWidget(currentDate) + self.publishCity = QLineEdit() + self.publishCity.setToolTip(i18n("Traditional publishers are always mentioned in source with the city they are located.")) + self.isbn = QLineEdit() + self.license = license_combo_box() # Maybe ought to make this a QLineEdit... + self.license.setEditable(True) + self.license.completer().setCompletionMode(QCompleter.PopupCompletion) + publisherLayout.addRow(i18n("Name:"), self.publisherName) + publisherLayout.addRow(i18n("City:"), self.publishCity) + publisherLayout.addRow(i18n("Date:"), publishDateLayout) + publisherLayout.addRow(i18n("ISBN:"), self.isbn) + publisherLayout.addRow(i18n("License:"), self.license) + + mainWidget.addTab(publisherPage, i18n("Publisher")) + """ + Ensure that the drag and drop of authors doesn't mess up the labels. + """ + + def slot_reset_author_row_visual(self): + headerLabelList = [] + for i in range(self.authorTable.verticalHeader().count()): + headerLabelList.append(str(i)) + for i in range(self.authorTable.verticalHeader().count()): + logicalI = self.authorTable.verticalHeader().logicalIndex(i) + headerLabelList[logicalI] = str(i + 1) + self.authorModel.setVerticalHeaderLabels(headerLabelList) + """ + Set the publish date to the current date. + """ + + def slot_set_date(self): + self.publishDate.setDate(QDate().currentDate()) + + """ + Append keys to autocompletion lists from the directory mainP. + """ + + def get_auto_completion_keys(self, mainP=Path()): + genre = Path(mainP / "key_genre") + characters = Path(mainP / "key_characters") + rating = Path(mainP / "key_rating") + format = Path(mainP / "key_format") + keywords = Path(mainP / "key_other") + authorRole = Path(mainP / "key_author_roles") + if genre.exists(): + for t in list(genre.glob('**/*.txt')): + file = open(str(t), "r", errors="replace") + for l in file: + self.genreKeysList.append(str(l).strip("\n")) + file.close() + if characters.exists(): + for t in list(characters.glob('**/*.txt')): + file = open(str(t), "r", errors="replace") + for l in file: + self.characterKeysList.append(str(l).strip("\n")) + file.close() + if format.exists(): + for t in list(format.glob('**/*.txt')): + file = open(str(t), "r", errors="replace") + for l in file: + self.formatKeysList.append(str(l).strip("\n")) + file.close() + if rating.exists(): + for t in list(rating.glob('**/*.csv')): + file = open(str(t), "r", newline="", encoding="utf-8") + ratings = csv.reader(file) + title = os.path.basename(str(t)) + r = 0 + for row in ratings: + listItem = [] + if r is 0: + title = row[1] + else: + listItem = self.ratingKeysList[title] + item = [] + item.append(row[0]) + item.append(row[1]) + listItem.append(item) + self.ratingKeysList[title] = listItem + r += 1 + file.close() + if keywords.exists(): + for t in list(keywords.glob('**/*.txt')): + file = open(str(t), "r", errors="replace") + for l in file: + self.otherKeysList.append(str(l).strip("\n")) + file.close() + if authorRole.exists(): + for t in list(authorRole.glob('**/*.txt')): + file = open(str(t), "r", errors="replace") + for l in file: + self.authorRoleList.append(str(l).strip("\n")) + file.close() + + """ + Refill the ratings box. + This is called whenever the rating system changes. + """ + + def slot_refill_ratings(self): + if self.cmbRatingSystem.currentText() in self.ratingKeysList.keys(): + self.cmbRating.clear() + model = QStandardItemModel() + for i in self.ratingKeysList[self.cmbRatingSystem.currentText()]: + item = QStandardItem() + item.setText(i[0]) + item.setToolTip(i[1]) + model.appendRow(item) + self.cmbRating.setModel(model) + + """ + Add an author with default values initialised. + """ + + def slot_add_author(self): + listItems = [] + listItems.append(QStandardItem(i18n("Anon"))) # Nick name + listItems.append(QStandardItem(i18n("John"))) # First name + listItems.append(QStandardItem()) # Middle name + listItems.append(QStandardItem(i18n("Doe"))) # Last name + listItems.append(QStandardItem()) # role + listItems.append(QStandardItem()) # email + listItems.append(QStandardItem()) # homepage + self.authorModel.appendRow(listItems) + + """ + Remove the selected author from the author list. + """ + + def slot_remove_author(self): + self.authorModel.removeRow(self.authorTable.currentIndex().row()) + + """ + Load the UI values from the config dictionary given. + """ + + def setConfig(self, config): + + if "title" in config.keys(): + self.lnTitle.setText(config["title"]) + self.teSummary.clear() + if "pages" in config.keys(): + self.cmbCoverPage.clear() + for page in config["pages"]: + self.cmbCoverPage.addItem(page) + if "cover" in config.keys(): + if config["cover"] in config["pages"]: + self.cmbCoverPage.setCurrentText(config["cover"]) + if "summary" in config.keys(): + self.teSummary.appendPlainText(config["summary"]) + if "genre" in config.keys(): + self.lnGenre.setText(", ".join(config["genre"])) + if "characters" in config.keys(): + self.lnCharacters.setText(", ".join(config["characters"])) + if "format" in config.keys(): + self.lnFormat.setText(", ".join(config["format"])) + if "rating" in config.keys(): + self.cmbRating.setCurrentText(config["rating"]) + else: + self.cmbRating.setCurrentText("") + if "ratingSystem" in config.keys(): + self.cmbRatingSystem.setCurrentText(config["ratingSystem"]) + else: + self.cmbRatingSystem.setCurrentText("") + if "otherKeywords" in config.keys(): + self.lnOtherKeywords.setText(", ".join(config["otherKeywords"])) + if "seriesName" in config.keys(): + self.lnSeriesName.setText(config["seriesName"]) + if "seriesVolume" in config.keys(): + self.spnSeriesVol.setValue(config["seriesVolume"]) + if "seriesNumber" in config.keys(): + self.spnSeriesNumber.setValue(config["seriesNumber"]) + if "language" in config.keys(): + code = config["language"] + if "_" in code: + code = code.split("_")[0] + self.cmbLanguage.setEntryToCode(code) + if "readingDirection" in config.keys(): + if config["readingDirection"] is "leftToRight": + self.cmbReadingMode.setCurrentIndex(0) + else: + self.cmbReadingMode.setCurrentIndex(1) + else: + self.cmbReadingMode.setCurrentIndex(QLocale(self.cmbLanguage.codeForCurrentEntry()).textDirection()) + if "publisherName" in config.keys(): + self.publisherName.setText(config["publisherName"]) + if "publisherCity" in config.keys(): + self.publishCity.setText(config["publisherCity"]) + if "publishingDate" in config.keys(): + self.publishDate.setDate(QDate.fromString(config["publishingDate"], Qt.ISODate)) + if "isbn-number" in config.keys(): + self.isbn.setText(config["isbn-number"]) + if "license" in config.keys(): + self.license.setCurrentText(config["license"]) + else: + self.license.setCurrentText("") # I would like to keep it ambiguous whether the artist has thought about the license or not. + if "authorList" in config.keys(): + authorList = config["authorList"] + for i in range(len(authorList)): + author = authorList[i] + if len(author.keys()) > 0: + listItems = [] + authorNickName = QStandardItem() + if "nickname" in author.keys(): + authorNickName.setText(author["nickname"]) + pass + listItems.append(authorNickName) + authorFirstName = QStandardItem() + if "first-name" in author.keys(): + authorFirstName.setText(author["first-name"]) + pass + listItems.append(authorFirstName) + authorMiddleName = QStandardItem() + if "initials" in author.keys(): + authorMiddleName.setText(author["initials"]) + pass + listItems.append(authorMiddleName) + authorLastName = QStandardItem() + if "last-name" in author.keys(): + authorLastName.setText(author["last-name"]) + pass + listItems.append(authorLastName) + authorRole = QStandardItem() + if "role" in author.keys(): + authorRole.setText(author["role"]) + pass + listItems.append(authorRole) + authorEMail = QStandardItem() + if "email" in author.keys(): + authorEMail.setText(author["email"]) + pass + listItems.append(authorEMail) + authorHomePage = QStandardItem() + if "homepage" in author.keys(): + authorHomePage.setText(author["homepage"]) + pass + listItems.append(authorHomePage) + self.authorModel.appendRow(listItems) + else: + self.slot_add_author() + + """ + Store the GUI values into the config dictionary given. + + @return the config diactionary filled with new values. + """ + + def getConfig(self, config): + + text = self.lnTitle.text() + if len(text) > 0 and text.isspace() is False: + config["title"] = text + elif "title" in config.keys(): + config.pop("title") + config["cover"] = self.cmbCoverPage.currentText() + listkeys = self.lnGenre.text() + if len(listkeys) > 0 and listkeys.isspace() is False: + config["genre"] = self.lnGenre.text().split(", ") + elif "genre" in config.keys(): + config.pop("genre") + listkeys = self.lnCharacters.text() + if len(listkeys) > 0 and listkeys.isspace() is False: + config["characters"] = self.lnCharacters.text().split(", ") + elif "characters" in config.keys(): + config.pop("characters") + listkeys = self.lnFormat.text() + if len(listkeys) > 0 and listkeys.isspace() is False: + config["format"] = self.lnFormat.text().split(", ") + elif "format" in config.keys(): + config.pop("format") + config["ratingSystem"] = self.cmbRatingSystem.currentText() + config["rating"] = self.cmbRating.currentText() + listkeys = self.lnOtherKeywords.text() + if len(listkeys) > 0 and listkeys.isspace() is False: + config["otherKeywords"] = self.lnOtherKeywords.text().split(", ") + elif "characters" in config.keys(): + config.pop("otherKeywords") + text = self.teSummary.toPlainText() + if len(text) > 0 and text.isspace() is False: + config["summary"] = text + elif "summary" in config.keys(): + config.pop("summary") + if len(self.lnSeriesName.text()) > 0: + config["seriesName"] = self.lnSeriesName.text() + config["seriesNumber"] = self.spnSeriesNumber.value() + if self.spnSeriesVol.value() > 0: + config["seriesVolume"] = self.spnSeriesVol.value() + config["language"] = str(self.cmbLanguage.codeForCurrentEntry()) + if self.cmbReadingMode is Qt.LeftToRight: + config["readingDirection"] = "leftToRight" + else: + config["readingDirection"] = "rightToLeft" + authorList = [] + for row in range(self.authorTable.verticalHeader().count()): + logicalIndex = self.authorTable.verticalHeader().logicalIndex(row) + listEntries = ["nickname", "first-name", "initials", "last-name", "role", "email", "homepage"] + author = {} + for i in range(len(listEntries)): + entry = self.authorModel.data(self.authorModel.index(logicalIndex, i)) + if entry is None: + entry = " " + if entry.isspace() is False and len(entry) > 0: + author[listEntries[i]] = entry + elif listEntries[i] in author.keys(): + author.pop(listEntries[i]) + authorList.append(author) + config["authorList"] = authorList + config["publisherName"] = self.publisherName.text() + config["publisherCity"] = self.publishCity.text() + config["publishingDate"] = self.publishDate.date().toString(Qt.ISODate) + config["isbn-number"] = self.isbn.text() + config["license"] = self.license.currentText() + + return config diff --git a/plugins/extensions/pykrita/plugin/plugins/comics_project_management_tools/comics_project_manager_docker.py b/plugins/extensions/pykrita/plugin/plugins/comics_project_management_tools/comics_project_manager_docker.py new file mode 100644 index 0000000000..ffc7788b2f --- /dev/null +++ b/plugins/extensions/pykrita/plugin/plugins/comics_project_management_tools/comics_project_manager_docker.py @@ -0,0 +1,679 @@ +""" +Part of the comics project management tools (CPMT). + +This is a docker that helps you organise your comics project. +""" +import sys +import json +import os +import zipfile # quick reading of documents +import shutil +import xml.etree.ElementTree as ET +from PyQt5.QtCore import QElapsedTimer +from PyQt5.QtGui import QStandardItem, QStandardItemModel, QImage, QIcon, QPixmap +from PyQt5.QtWidgets import QHBoxLayout, QVBoxLayout, QTableView, QToolButton, QMenu, QAction, QPushButton, QSpacerItem, QSizePolicy, QWidget, QAbstractItemView, QProgressDialog, QDialog, QFileDialog, QDialogButtonBox, qApp +import math +from krita import * +from . import comics_metadata_dialog, comics_exporter, comics_export_dialog, comics_project_setup_wizard, comics_template_dialog, comics_project_settings_dialog, comics_project_page_viewer + +""" +This is a Krita docker called 'Comics Manager'. + +It allows people to create comics project files, load those files, add pages, remove pages, move pages, manage the metadata, +and finally export the result. + +The logic behind this docker is that it is very easy to get lost in a comics project due to the massive amount of files. +By having a docker that gives the user quick access to the pages and also allows them to do all of the meta-stuff, like +meta data, but also reordering the pages, the chaos of managing the project should take up less time, and more time can be focussed on actual writing and drawing. +""" + + +class comics_project_manager_docker(DockWidget): + setupDictionary = {} + stringName = i18n("Comics Manager") + projecturl = None + + def __init__(self): + super().__init__() + self.setWindowTitle(self.stringName) + + # Setup layout: + self.baseLayout = QHBoxLayout() + widget = QWidget() + widget.setLayout(self.baseLayout) + self.setWidget(widget) + self.buttonLayout = QVBoxLayout() + self.baseLayout.addLayout(self.buttonLayout) + + # Comic page list and pages model + self.comicPageList = QTableView() + self.comicPageList.verticalHeader().setSectionsMovable(True) + self.comicPageList.verticalHeader().setDragEnabled(True) + self.comicPageList.verticalHeader().setDragDropMode(QAbstractItemView.InternalMove) + self.comicPageList.setAcceptDrops(True) + self.pagesModel = QStandardItemModel() + self.comicPageList.doubleClicked.connect(self.slot_open_page) + self.comicPageList.setIconSize(QSize(100, 100)) + # self.comicPageList.itemDelegate().closeEditor.connect(self.slot_write_description) + self.pagesModel.layoutChanged.connect(self.slot_write_config) + self.pagesModel.rowsInserted.connect(self.slot_write_config) + self.pagesModel.rowsRemoved.connect(self.slot_write_config) + self.comicPageList.verticalHeader().sectionMoved.connect(self.slot_write_config) + self.comicPageList.setModel(self.pagesModel) + self.baseLayout.addWidget(self.comicPageList) + + self.btn_project = QToolButton() + self.btn_project.setPopupMode(QToolButton.MenuButtonPopup) + self.btn_project.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) + menu_project = QMenu() + self.action_new_project = QAction(i18n("New Project"), None) + self.action_new_project.triggered.connect(self.slot_new_project) + self.action_load_project = QAction(i18n("Open Project"), None) + self.action_load_project.triggered.connect(self.slot_open_config) + menu_project.addAction(self.action_new_project) + menu_project.addAction(self.action_load_project) + self.btn_project.setMenu(menu_project) + self.btn_project.setDefaultAction(self.action_load_project) + self.buttonLayout.addWidget(self.btn_project) + + # Settings dropdown with actions for the different settings menus. + self.btn_settings = QToolButton() + self.btn_settings.setPopupMode(QToolButton.MenuButtonPopup) + self.btn_settings.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) + self.action_edit_project_settings = QAction(i18n("Project Settings"), None) + self.action_edit_project_settings.triggered.connect(self.slot_edit_project_settings) + self.action_edit_meta_data = QAction(i18n("Meta Data"), None) + self.action_edit_meta_data.triggered.connect(self.slot_edit_meta_data) + self.action_edit_export_settings = QAction(i18n("Export Settings"), None) + self.action_edit_export_settings.triggered.connect(self.slot_edit_export_settings) + menu_settings = QMenu() + menu_settings.addAction(self.action_edit_project_settings) + menu_settings.addAction(self.action_edit_meta_data) + menu_settings.addAction(self.action_edit_export_settings) + self.btn_settings.setDefaultAction(self.action_edit_project_settings) + self.btn_settings.setMenu(menu_settings) + self.buttonLayout.addWidget(self.btn_settings) + self.btn_settings.setDisabled(True) + + # Add page drop down with different page actions. + self.btn_add_page = QToolButton() + self.btn_add_page.setPopupMode(QToolButton.MenuButtonPopup) + self.btn_add_page.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) + self.action_add_page = QAction(i18n("Add Page"), None) + self.action_add_page.triggered.connect(self.slot_add_new_page_single) + self.action_add_template = QAction(i18n("Add Page From Template"), None) + self.action_add_template.triggered.connect(self.slot_add_new_page_from_template) + self.action_add_existing = QAction(i18n("Add Existing Pages"), None) + self.action_add_existing.triggered.connect(self.slot_add_page_from_url) + self.action_remove_selected_page = QAction(i18n("Remove Page"), None) + self.action_remove_selected_page.triggered.connect(self.slot_remove_selected_page) + self.action_resize_all_pages = QAction(i18n("Batch Resize"), None) + self.action_resize_all_pages.triggered.connect(self.slot_batch_resize) + self.btn_add_page.setDefaultAction(self.action_add_page) + self.action_show_page_viewer = QAction(i18n("View Page In Window"), None) + self.action_show_page_viewer.triggered.connect(self.slot_show_page_viewer) + self.action_scrape_authors = QAction(i18n("Scrape Author Info"), None) + self.action_scrape_authors.setToolTip(i18n("Search for author information in documents and add it to the author list. This doesn't check for duplicates.")) + self.action_scrape_authors.triggered.connect(self.slot_scrape_author_list) + actionList = [] + menu_page = QMenu() + actionList.append(self.action_add_page) + actionList.append(self.action_add_template) + actionList.append(self.action_add_existing) + actionList.append(self.action_remove_selected_page) + actionList.append(self.action_resize_all_pages) + actionList.append(self.action_show_page_viewer) + actionList.append(self.action_scrape_authors) + menu_page.addActions(actionList) + self.btn_add_page.setMenu(menu_page) + self.buttonLayout.addWidget(self.btn_add_page) + self.btn_add_page.setDisabled(True) + + self.comicPageList.setContextMenuPolicy(Qt.ActionsContextMenu) + self.comicPageList.addActions(actionList) + + # Export button that... exports. + self.btn_export = QPushButton(i18n("Export Comic")) + self.btn_export.clicked.connect(self.slot_export) + self.buttonLayout.addWidget(self.btn_export) + self.btn_export.setDisabled(True) + + self.btn_project_url = QPushButton(i18n("Copy Location")) + self.btn_project_url.setToolTip(i18n("Copies the path of the project to the clipboard. Useful for quickly copying to a file manager or the like.")) + self.btn_project_url.clicked.connect(self.slot_copy_project_url) + self.btn_project_url.setDisabled(True) + self.buttonLayout.addWidget(self.btn_project_url) + + self.page_viewer_dialog = comics_project_page_viewer.comics_project_page_viewer() + + Application.notifier().imageSaved.connect(self.slot_check_for_page_update) + + self.buttonLayout.addItem(QSpacerItem(0, 0, QSizePolicy.Minimum, QSizePolicy.MinimumExpanding)) + + """ + Open the config file and load the json file into a dictionary. + """ + + def slot_open_config(self): + self.path_to_config = QFileDialog.getOpenFileName(caption=i18n("Please select the json comic config file."), filter=str(i18n("json files") + "(*.json)"))[0] + if os.path.exists(self.path_to_config) is True: + configFile = open(self.path_to_config, "r", newline="", encoding="utf-16") + self.setupDictionary = json.load(configFile) + self.projecturl = os.path.dirname(str(self.path_to_config)) + configFile.close() + self.load_config() + """ + Further config loading. + """ + + def load_config(self): + self.setWindowTitle(self.stringName + ": " + str(self.setupDictionary["projectName"])) + self.fill_pages() + self.btn_settings.setEnabled(True) + self.btn_add_page.setEnabled(True) + self.btn_export.setEnabled(True) + self.btn_project_url.setEnabled(True) + + """ + Fill the pages model with the pages from the pages list. + """ + + def fill_pages(self): + self.loadingPages = True + self.pagesModel.clear() + self.pagesModel.setHorizontalHeaderLabels([i18n("Page"), i18n("Description")]) + pagesList = [] + if "pages" in self.setupDictionary.keys(): + pagesList = self.setupDictionary["pages"] + progress = QProgressDialog() + progress.setMinimum(0) + progress.setMaximum(len(pagesList)) + progress.setWindowTitle(i18n("Loading pages...")) + for url in pagesList: + absurl = os.path.join(self.projecturl, url) + if (os.path.exists(absurl)): + #page = Application.openDocument(absurl) + page = zipfile.ZipFile(absurl, "r") + thumbnail = QImage.fromData(page.read("preview.png")) + pageItem = QStandardItem() + dataList = self.get_description_and_title(page.read("documentinfo.xml")) + if (dataList[0].isspace() or len(dataList[0]) < 1): + dataList[0] = os.path.basename(url) + pageItem.setText(dataList[0]) + pageItem.setDragEnabled(True) + pageItem.setDropEnabled(False) + pageItem.setEditable(False) + pageItem.setIcon(QIcon(QPixmap.fromImage(thumbnail))) + pageItem.setToolTip(url) + page.close() + description = QStandardItem() + description.setText(dataList[1]) + description.setEditable(False) + listItem = [] + listItem.append(pageItem) + listItem.append(description) + self.pagesModel.appendRow(listItem) + self.comicPageList.resizeRowsToContents() + self.comicPageList.resizeColumnToContents(0) + self.comicPageList.setColumnWidth(1, 256) + progress.setValue(progress.value() + 1) + progress.setValue(len(pagesList)) + self.loadingPages = False + + """ + Function that takes the documentinfo.xml and parses it for the title, subject and abstract tags, + to get the title and description. + + @returns a stringlist with the name on 0 and the description on 1. + """ + + def get_description_and_title(self, string): + xmlDoc = ET.fromstring(string) + calligra = str("{http://www.calligra.org/DTD/document-info}") + name = "" + if ET.iselement(xmlDoc[0].find(calligra + 'title')): + name = xmlDoc[0].find(calligra + 'title').text + if name is None: + name = " " + desc = "" + if ET.iselement(xmlDoc[0].find(calligra + 'subject')): + desc = xmlDoc[0].find(calligra + 'subject').text + if desc is None or desc.isspace() or len(desc) < 1: + if ET.iselement(xmlDoc[0].find(calligra + 'abstract')): + desc = str(xmlDoc[0].find(calligra + 'abstract').text) + if desc.startswith(""): + desc = desc[:-len("]]>")] + return [name, desc] + + """ + Scrapes authors from the author data in the document info and puts them into the author list. + Doesn't check for duplicates. + """ + + def slot_scrape_author_list(self): + listOfAuthors = [] + if "authorList" in self.setupDictionary.keys(): + listOfAuthors = self.setupDictionary["authorList"] + if "pages" in self.setupDictionary.keys(): + for relurl in self.setupDictionary["pages"]: + absurl = os.path.join(self.projecturl, relurl) + page = zipfile.ZipFile(absurl, "r") + xmlDoc = ET.fromstring(page.read("documentinfo.xml")) + calligra = str("{http://www.calligra.org/DTD/document-info}") + authorelem = xmlDoc.find(calligra + 'author') + author = {} + if ET.iselement(authorelem.find(calligra + 'full-name')): + author["nickname"] = str(authorelem.find(calligra + 'full-name').text) + if ET.iselement(authorelem.find(calligra + 'email')): + author["email"] = str(authorelem.find(calligra + 'email').text) + if ET.iselement(authorelem.find(calligra + 'position')): + author["role"] = str(authorelem.find(calligra + 'position').text) + listOfAuthors.append(author) + page.close() + self.setupDictionary["authorList"] = listOfAuthors + + """ + Edit the general project settings like the project name, concept, pages location, export location, template location, metadata + """ + + def slot_edit_project_settings(self): + dialog = comics_project_settings_dialog.comics_project_details_editor(self.projecturl) + dialog.setConfig(self.setupDictionary, self.projecturl) + + if dialog.exec_() == QDialog.Accepted: + self.setupDictionary = dialog.getConfig(self.setupDictionary) + self.slot_write_config() + self.setWindowTitle(self.stringName + ": " + str(self.setupDictionary["projectName"])) + + """ + This allows users to select existing pages and add them to the pages list. The pages are currently not copied to the pages folder. Useful for existing projects. + """ + + def slot_add_page_from_url(self): + # get the pages. + urlList = QFileDialog.getOpenFileNames(caption=i18n("Which existing pages to add?"), directory=self.projecturl, filter=str(i18n("Krita files") + "(*.kra)"))[0] + + # get the existing pages list. + pagesList = [] + if "pages" in self.setupDictionary.keys(): + pagesList = self.setupDictionary["pages"] + + # And add each url in the url list to the pages list and the model. + for url in urlList: + if self.projecturl not in urlList: + newUrl = os.path.join(self.projecturl, self.setupDictionary["pagesLocation"], os.path.basename(url)) + shutil.move(url, newUrl) + url = newUrl + relative = os.path.relpath(url, self.projecturl) + if url not in pagesList: + page = zipfile.ZipFile(url, "r") + thumbnail = QImage.fromData(page.read("preview.png")) + dataList = self.get_description_and_title(page.read("documentinfo.xml")) + if (dataList[0].isspace() or len(dataList[0]) < 1): + dataList[0] = os.path.basename(url) + newPageItem = QStandardItem() + newPageItem.setIcon(QIcon(QPixmap.fromImage(thumbnail))) + newPageItem.setDragEnabled(True) + newPageItem.setDropEnabled(False) + newPageItem.setEditable(False) + newPageItem.setText(dataList[0]) + newPageItem.setToolTip(relative) + page.close() + description = QStandardItem() + description.setText(dataList[1]) + description.setEditable(False) + listItem = [] + listItem.append(newPageItem) + listItem.append(description) + self.pagesModel.appendRow(listItem) + self.comicPageList.resizeRowsToContents() + self.comicPageList.resizeColumnToContents(0) + + """ + Remove the selected page from the list of pages. This does not remove it from disk(far too dangerous). + """ + + def slot_remove_selected_page(self): + index = self.comicPageList.currentIndex() + self.pagesModel.removeRow(index.row()) + + """ + This function adds a new page from the default template. If there's no default template, or the file does not exist, it will + show the create/import template dialog. It will remember the selected item as the default template. + """ + + def slot_add_new_page_single(self): + templateUrl = "templatepage" + templateExists = False + + if "singlePageTemplate" in self.setupDictionary.keys(): + templateUrl = self.setupDictionary["singlePageTemplate"] + if os.path.exists(os.path.join(self.projecturl, templateUrl)): + templateExists = True + + if templateExists is False: + if "templateLocation" not in self.setupDictionary.keys(): + self.setupDictionary["templateLocation"] = os.path.relpath(QFileDialog.getExistingDirectory(caption=i18n("Where are the templates located?"), options=QFileDialog.ShowDirsOnly), self.projecturl) + + templateDir = os.path.join(self.projecturl, self.setupDictionary["templateLocation"]) + template = comics_template_dialog.comics_template_dialog(templateDir) + + if template.exec_() == QDialog.Accepted: + templateUrl = os.path.relpath(template.url(), self.projecturl) + self.setupDictionary["singlePageTemplate"] = templateUrl + if os.path.exists(os.path.join(self.projecturl, templateUrl)): + self.add_new_page(templateUrl) + + """ + This function always asks for a template showing the new template window. This allows users to have multiple different + templates created for back covers, spreads, other and have them accesible, while still having the convenience of a singular + "add page" that adds a default. + """ + + def slot_add_new_page_from_template(self): + if "templateLocation" not in self.setupDictionary.keys(): + self.setupDictionary["templateLocation"] = os.path.relpath(QFileDialog.getExistingDirectory(caption=i18n("Where are the templates located?"), options=QFileDialog.ShowDirsOnly), self.projecturl) + + templateDir = os.path.join(self.projecturl, self.setupDictionary["templateLocation"]) + template = comics_template_dialog.comics_template_dialog(templateDir) + + if template.exec_() == QDialog.Accepted: + templateUrl = os.path.relpath(template.url(), self.projecturl) + self.add_new_page(templateUrl) + + """ + This is the actual function that adds the template using the template url. + It will attempt to name the new page projectName+number, and tries to get the first possible + number that is not in the pages list. If such a file already exists it will only append the file. + """ + + def add_new_page(self, templateUrl): + + # check for page list and or location. + pagesList = [] + if "pages" in self.setupDictionary.keys(): + pagesList = self.setupDictionary["pages"] + if (str(self.setupDictionary["pagesLocation"]).isspace()): + self.setupDictionary["pagesLocation"] = os.path.relpath(QFileDialog.getExistingDirectory(caption=i18n("Where should the pages go?"), options=QFileDialog.ShowDirsOnly), self.projecturl) + + # Search for the possible name. + pageName = str(self.setupDictionary["projectName"]) + str(format(len(pagesList), "03d")) + url = os.path.join(str(self.setupDictionary["pagesLocation"]), pageName + ".kra") + pageNumber = 0 + if (url in pagesList): + while (url in pagesList): + pageNumber += 1 + pageName = str(self.setupDictionary["projectName"]) + str(format(pageNumber, "03d")) + url = os.path.join(str(self.setupDictionary["pagesLocation"]), pageName + ".kra") + + # open the page by opening the template and resaving it, or just opening it. + absoluteUrl = os.path.join(self.projecturl, url) + if (os.path.exists(absoluteUrl)): + newPage = Application.openDocument(absoluteUrl) + else: + booltemplateExists = os.path.exists(os.path.join(self.projecturl, templateUrl)) + if booltemplateExists is False: + templateUrl = os.path.relpath(QFileDialog.getOpenFileName(caption=i18n("Which image should be the basis the new page?"), directory=self.projecturl, filter=str(i18n("Krita files") + "(*.kra)"))[0], self.projecturl) + newPage = Application.openDocument(os.path.join(self.projecturl, templateUrl)) + newPage.setFileName(absoluteUrl) + newPage.setName(pageName) + newPage.exportImage(absoluteUrl, InfoObject()) + + # Get out the extra data for the standard item. + newPageItem = QStandardItem() + newPageItem.setIcon(QIcon(QPixmap.fromImage(newPage.thumbnail(100, 100)))) + newPageItem.setDragEnabled(True) + newPageItem.setDropEnabled(False) + newPageItem.setEditable(False) + newPageItem.setText(pageName) + newPageItem.setToolTip(url) + + # close page document. + newPage.waitForDone() + if newPage.isIdle(): + newPage.close() + + # add item to page. + description = QStandardItem() + description.setText(str("")) + description.setEditable(False) + listItem = [] + listItem.append(newPageItem) + listItem.append(description) + self.pagesModel.appendRow(listItem) + self.comicPageList.resizeRowsToContents() + self.comicPageList.resizeColumnToContents(0) + + """ + Write to the json configuratin file. + This also checks the current state of the pages list. + """ + + def slot_write_config(self): + + # Don't load when the pages are still being loaded, otherwise we'll be overwriting our own pages list. + if (self.loadingPages is False): + print("CPMT: writing comic configuration...") + + # Generate a pages list from the pagesmodel. + # Because we made the drag-and-drop use the tableview header, we need to first request the logicalIndex + # for the visualIndex, and then request the items for the logical index in the pagesmodel. + # After that, we rename the verticalheader to have the appropriate numbers it will have when reloading. + pagesList = [] + listOfHeaderLabels = [] + for i in range(self.pagesModel.rowCount()): + listOfHeaderLabels.append(str(i)) + for i in range(self.pagesModel.rowCount()): + iLogical = self.comicPageList.verticalHeader().logicalIndex(i) + index = self.pagesModel.index(iLogical, 0) + if index.isValid() is False: + index = self.pagesModel.index(i, 0) + url = str(self.pagesModel.data(index, role=Qt.ToolTipRole)) + if url not in pagesList: + pagesList.append(url) + listOfHeaderLabels[iLogical] = str(i + 1) + self.pagesModel.setVerticalHeaderLabels(listOfHeaderLabels) + self.comicPageList.verticalHeader().update() + self.setupDictionary["pages"] = pagesList + + # Save to our json file. + configFile = open(self.path_to_config, "w", newline="", encoding="utf-16") + json.dump(self.setupDictionary, configFile, indent=4, sort_keys=True, ensure_ascii=False) + configFile.close() + print("CPMT: done") + + """ + Open a page in the pagesmodel in Krita. + """ + + def slot_open_page(self, index): + if index.column() is 0: + # Get the absolute url from the relative one in the pages model. + absoluteUrl = os.path.join(self.projecturl, str(self.pagesModel.data(index, role=Qt.ToolTipRole))) + + # Make sure the page exists. + if os.path.exists(absoluteUrl): + page = Application.openDocument(absoluteUrl) + + # Set the title to the filename if it was empty. It looks a bit neater. + if page.name().isspace or len(page.name()) < 1: + page.setName(str(self.pagesModel.data(index, role=Qt.DisplayRole))) + + # Add views for the document so the user can use it. + Application.activeWindow().addView(page) + Application.setActiveDocument(page) + else: + print("CPMT: The page cannot be opened because the file doesn't exist:", absoluteUrl) + + """ + Call up the metadata editor dialog. Only when the dialog is "Accepted" will the metadata be saved. + """ + + def slot_edit_meta_data(self): + dialog = comics_metadata_dialog.comic_meta_data_editor() + + dialog.setConfig(self.setupDictionary) + if (dialog.exec_() == QDialog.Accepted): + self.setupDictionary = dialog.getConfig(self.setupDictionary) + self.slot_write_config() + + """ + An attempt at making the description editable from the comic pages list. It is currently not working because ZipFile + has no overwrite mechanism, and I don't have the energy to write one yet. + """ + + def slot_write_description(self, index): + + for row in range(self.pagesModel.rowCount()): + index = self.pagesModel.index(row, 1) + indexUrl = self.pagesModel.index(row, 0) + absoluteUrl = os.path.join(self.projecturl, str(self.pagesModel.data(indexUrl, role=Qt.ToolTipRole))) + page = zipfile.ZipFile(absoluteUrl, "a") + xmlDoc = ET.ElementTree() + ET.register_namespace("", "http://www.calligra.org/DTD/document-info") + location = os.path.join(self.projecturl, "documentinfo.xml") + xmlDoc.parse(location) + xmlroot = ET.fromstring(page.read("documentinfo.xml")) + calligra = "{http://www.calligra.org/DTD/document-info}" + aboutelem = xmlroot.find(calligra + 'about') + if ET.iselement(aboutelem.find(calligra + 'subject')): + desc = aboutelem.find(calligra + 'subject') + desc.text = self.pagesModel.data(index, role=Qt.EditRole) + xmlstring = ET.tostring(xmlroot, encoding='unicode', method='xml', short_empty_elements=False) + page.writestr(zinfo_or_arcname="documentinfo.xml", data=xmlstring) + for document in Application.documents(): + if str(document.fileName()) == str(absoluteUrl): + document.setDocumentInfo(xmlstring) + page.close() + + """ + Calls up the export settings dialog. Only when accepted will the configuration be written. + """ + + def slot_edit_export_settings(self): + dialog = comics_export_dialog.comic_export_setting_dialog() + dialog.setConfig(self.setupDictionary) + + if (dialog.exec_() == QDialog.Accepted): + self.setupDictionary = dialog.getConfig(self.setupDictionary) + self.slot_write_config() + + """ + Export the comic. Won't work without export settings set. + """ + + def slot_export(self): + exporter = comics_exporter.comicsExporter() + exporter.set_config(self.setupDictionary, self.projecturl) + exportSuccess = exporter.export() + if exportSuccess: + print("CPMT: Export success! The files have been written to the export folder!") + + """ + Calls up the comics project setup wizard so users can create a new json file with the basic information. + """ + + def slot_new_project(self): + setup = comics_project_setup_wizard.ComicsProjectSetupWizard() + setup.showDialog() + + def slot_check_for_page_update(self, url): + if "pages" in self.setupDictionary.keys(): + relUrl = os.path.relpath(url, self.projecturl) + if relUrl in self.setupDictionary["pages"]: + index = self.pagesModel.index(self.setupDictionary["pages"].index(relUrl), 0) + index2 = self.pagesModel.index(index.row(), 1) + if index.isValid(): + pageItem = self.pagesModel.itemFromIndex(index) + page = zipfile.ZipFile(url, "r") + dataList = self.get_description_and_title(page.read("documentinfo.xml")) + if (dataList[0].isspace() or len(dataList[0]) < 1): + dataList[0] = os.path.basename(url) + thumbnail = QImage.fromData(page.read("preview.png")) + pageItem.setIcon(QIcon(QPixmap.fromImage(thumbnail))) + pageItem.setText(dataList[0]) + self.pagesModel.setItem(index.row(), index.column(), pageItem) + self.pagesModel.setData(index2, str(dataList[1]), Qt.DisplayRole) + + """ + Resize all the pages in the pages list. + It will show a dialog with the options for resizing. Then, it will try to pop up a progress dialog while resizing. + The progress dialog shows the remaining time and pages. + """ + + def slot_batch_resize(self): + dialog = QDialog() + dialog.setWindowTitle(i18n("Risize all pages.")) + buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) + buttons.accepted.connect(dialog.accept) + buttons.rejected.connect(dialog.reject) + sizesBox = comics_export_dialog.comic_export_resize_widget("Scale", batch=True, fileType=False) + exporterSizes = comics_exporter.sizesCalculator() + dialog.setLayout(QVBoxLayout()) + dialog.layout().addWidget(sizesBox) + dialog.layout().addWidget(buttons) + + if dialog.exec_() == QDialog.Accepted: + progress = QProgressDialog(i18n("Resizing pages..."), str(), 0, len(self.setupDictionary["pages"])) + progress.setWindowTitle(i18n("Resizing pages.")) + progress.setCancelButton(None) + timer = QElapsedTimer() + timer.start() + config = {} + config = sizesBox.get_config(config) + for p in range(len(self.setupDictionary["pages"])): + absoluteUrl = os.path.join(self.projecturl, self.setupDictionary["pages"][p]) + progress.setValue(p) + timePassed = timer.elapsed() + if (p > 0): + timeEstimated = (len(self.setupDictionary["pages"]) - p) * (timePassed / p) + passedString = str(int(timePassed / 60000)) + ":" + format(int(timePassed / 1000), "02d") + ":" + format(timePassed % 1000, "03d") + estimatedString = str(int(timeEstimated / 60000)) + ":" + format(int(timeEstimated / 1000), "02d") + ":" + format(int(timeEstimated % 1000), "03d") + progress.setLabelText(str(i18n("{pages} of {pagesTotal} done. \nTime passed: {passedString}:\n Estimated:{estimated}")).format(pages=p, pagesTotal=len(self.setupDictionary["pages"]), passedString=passedString, estimated=estimatedString)) + if os.path.exists(absoluteUrl): + doc = Application.openDocument(absoluteUrl) + listScales = exporterSizes.get_scale_from_resize_config(config["Scale"], [doc.width(), doc.height(), doc.resolution(), doc.resolution()]) + doc.scaleImage(listScales[0], listScales[1], listScales[2], listScales[3], "bicubic") + doc.waitForDone() + doc.save() + doc.waitForDone() + doc.close() + + def slot_show_page_viewer(self): + index = self.comicPageList.currentIndex() + if index.column() is not 0: + index = self.pagesModel.index(index.row(), 0) + # Get the absolute url from the relative one in the pages model. + absoluteUrl = os.path.join(self.projecturl, str(self.pagesModel.data(index, role=Qt.ToolTipRole))) + + # Make sure the page exists. + if os.path.exists(absoluteUrl): + page = zipfile.ZipFile(absoluteUrl, "r") + image = QImage.fromData(page.read("mergedimage.png")) + self.page_viewer_dialog.update_image(image) + self.page_viewer_dialog.show() + page.close() + """ + Function to copy the current project location into the clipboard. + This is useful for users because they'll be able to use that url to quickly + move to the project location in outside applications. + """ + + def slot_copy_project_url(self): + if self.projecturl is not None: + clipboard = qApp.clipboard() + clipboard.setText(str(self.projecturl)) + """ + This is required by the dockwidget class, otherwise unused. + """ + + def canvasChanged(self, canvas): + pass + + +""" +Add docker to program +""" +Application.addDockWidgetFactory(DockWidgetFactory("comics_project_manager_docker", DockWidgetFactoryBase.DockRight, comics_project_manager_docker)) diff --git a/plugins/extensions/pykrita/plugin/plugins/comics_project_management_tools/comics_project_page_viewer.py b/plugins/extensions/pykrita/plugin/plugins/comics_project_management_tools/comics_project_page_viewer.py new file mode 100644 index 0000000000..0fbd1161dd --- /dev/null +++ b/plugins/extensions/pykrita/plugin/plugins/comics_project_management_tools/comics_project_page_viewer.py @@ -0,0 +1,48 @@ +""" +Part of the comics project management tools (CPMT). + +This is a docker that shows your comic pages. +""" + +from PyQt5.QtGui import QImage, QPainter +from PyQt5.QtWidgets import QDialog, QWidget, QVBoxLayout, QSizePolicy +from PyQt5.QtCore import QSize, Qt +from krita import * + + +class page_viewer(QWidget): + + def __init__(self, parent=None, flags=None): + super(page_viewer, self).__init__(parent) + self.image = QImage() + self.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding) + + def set_image(self, image=QImage()): + self.image = image + self.update() + + def paintEvent(self, event): + painter = QPainter(self) + image = self.image.scaled(self.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation) + painter.drawImage(0, 0, image) + + def sizeHint(self): + return QSize(256, 256) + + +class comics_project_page_viewer(QDialog): + currentPageNumber = 0 + + def __init__(self): + super().__init__() + self.setModal(False) + self.setWindowTitle(i18n("Comics page viewer.")) + self.setMinimumSize(200, 200) + self.listOfImages = [QImage()] + self.setLayout(QVBoxLayout()) + + self.viewer = page_viewer() + self.layout().addWidget(self.viewer) + + def update_image(self, image=QImage()): + self.viewer.set_image(image) diff --git a/plugins/extensions/pykrita/plugin/plugins/comics_project_management_tools/comics_project_settings_dialog.py b/plugins/extensions/pykrita/plugin/plugins/comics_project_management_tools/comics_project_settings_dialog.py new file mode 100644 index 0000000000..0d518d4d01 --- /dev/null +++ b/plugins/extensions/pykrita/plugin/plugins/comics_project_management_tools/comics_project_settings_dialog.py @@ -0,0 +1,171 @@ +""" +Part of the comics project management tools (CPMT). + +A dialog for editing the general project settings. +""" +import os +from PyQt5.QtWidgets import QWidget, QDialog, QDialogButtonBox, QHBoxLayout, QFormLayout, QPushButton, QLabel, QLineEdit, QToolButton, QFrame, QAction, QFileDialog, QComboBox, QSizePolicy +from PyQt5.QtCore import QDir, Qt, pyqtSignal +from krita import * + +""" +A Widget that contains both a qlabel and a button for selecting a path. +""" + + +class path_select(QWidget): + projectUrl = "" + question = i18n("Which folder?") + + """ + emits when a new directory has been chosen. + """ + locationChanged = pyqtSignal() + """ + Initialise the widget. + @param question is the question asked when selecting a directory. + @param project url is the url to which the label is relative. + """ + + def __init__(self, parent=None, flags=None, question=str(), projectUrl=None): + super(path_select, self).__init__(parent) + self.setLayout(QHBoxLayout()) + self.location = QLabel() + self.button = QToolButton() # Until we have a proper icon + self.layout().addWidget(self.location) + self.layout().addWidget(self.button) + self.layout().setContentsMargins(0, 0, 0, 0) + self.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Minimum) + self.location.setFrameStyle(QFrame.StyledPanel | QFrame.Sunken) + self.button.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) + self.location.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Minimum) + self.location.setAlignment(Qt.AlignRight) + self.location.setLineWidth(1) + if projectUrl is None: + self.projectUrl = QDir.homePath() + else: + self.projectUrl = projectUrl + self.question = question + self.action_change_folder = QAction(i18n("Change Folder")) + self.action_change_folder.setIconText("...") + self.action_change_folder.triggered.connect(self.slot_change_location) + self.button.setDefaultAction(self.action_change_folder) + + """ + pops up a directory chooser widget, and when a directory is chosen a locationChanged signal is emited. + """ + + def slot_change_location(self): + location = QFileDialog.getExistingDirectory(caption=self.question, directory=self.projectUrl) + if location is not None and location.isspace() is False and len(location) > 0: + location = os.path.relpath(location, self.projectUrl) + self.location.setText(location) + self.locationChanged.emit() + """ + Set the location. + @param path - the location relative to the projectUrl. + """ + + def setLocation(self, path=str()): + self.location.setText(path) + """ + Get the location. + @returns a string with the location relative to the projectUrl. + """ + + def getLocation(self): + return str(self.location.text()) + + +""" +Dialog for editing basic proect details like the project name, default template, +template location, etc. +""" + + +class comics_project_details_editor(QDialog): + configGroup = "ComicsProjectManagementTools" + """ + Initialise the editor. + @param projectUrl - The directory to which all paths are relative. + """ + + def __init__(self, projectUrl=str()): + super().__init__() + self.projectUrl = projectUrl + layout = QFormLayout() + self.setLayout(layout) + self.setWindowTitle(i18n("Comic Project Settings")) + buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) + + buttons.accepted.connect(self.accept) + buttons.rejected.connect(self.reject) + self.lnProjectName = QLineEdit() + self.lnProjectConcept = QLineEdit() + self.cmb_defaultTemplate = QComboBox() + + self.pagesLocation = path_select(question=i18n("Where should the pages go?"), projectUrl=self.projectUrl) + self.exportLocation = path_select(question=i18n("Where should the export go?"), projectUrl=self.projectUrl) + self.templateLocation = path_select(question=i18n("Where are the templates?"), projectUrl=self.projectUrl) + self.keyLocation = path_select(question=i18n("Where are the extra auto-completion keys located?")) + self.keyLocation.setToolTip(i18n("The location for extra autocompletion keys in the meta-data editor. Point this at a folder containing key_characters/key_format/key_genre/key_rating/key_author_roles/key_other with inside txt files(csv for tating) containing the extra auto-completion keys, each on a new line. This path is stored in the krita configuration, and not the project configuration.")) + self.templateLocation.locationChanged.connect(self.refill_templates) + + layout.addRow(i18n("Project Name:"), self.lnProjectName) + layout.addRow(i18n("Project Concept:"), self.lnProjectConcept) + layout.addRow(i18n("Pages Folder:"), self.pagesLocation) + layout.addRow(i18n("Export Folder:"), self.exportLocation) + layout.addRow(i18n("Template Folder:"), self.templateLocation) + layout.addRow(i18n("Default Template:"), self.cmb_defaultTemplate) + layout.addRow(i18n("Extra Keys Folder:"), self.keyLocation) + + self.layout().addWidget(buttons) + + """ + Fill the templates doc with the kra files found in the templates directory. + Might want to extend this to other files as well, as they basically get resaved anyway... + """ + + def refill_templates(self): + self.cmb_defaultTemplate.clear() + templateLocation = os.path.join(self.projectUrl, self.templateLocation.getLocation()) + for entry in os.scandir(templateLocation): + if entry.name.endswith('.kra') and entry.is_file(): + name = os.path.relpath(entry.path, templateLocation) + self.cmb_defaultTemplate.addItem(name) + + """ + Load the UI values from the config dictionary given. + """ + + def setConfig(self, config, projectUrl): + + self.projectUrl = projectUrl + if "projectName"in config.keys(): + self.lnProjectName.setText(config["projectName"]) + if "concept"in config.keys(): + self.lnProjectConcept.setText(config["concept"]) + if "pagesLocation" in config.keys(): + self.pagesLocation.setLocation(config["pagesLocation"]) + if "exportLocation" in config.keys(): + self.exportLocation.setLocation(config["exportLocation"]) + if "templateLocation" in config.keys(): + self.templateLocation.setLocation(config["templateLocation"]) + self.refill_templates() + self.keyLocation.setLocation(Application.readSetting(self.configGroup, "extraKeysLocation", str())) + + """ + Store the GUI values into the config dictionary given. + + @return the config diactionary filled with new values. + """ + + def getConfig(self, config): + config["projectName"] = self.lnProjectName.text() + config["concept"] = self.lnProjectConcept.text() + config["pagesLocation"] = self.pagesLocation.getLocation() + config["exportLocation"] = self.exportLocation.getLocation() + config["templateLocation"] = self.templateLocation.getLocation() + config["singlePageTemplate"] = os.path.join(self.templateLocation.getLocation(), self.cmb_defaultTemplate.currentText()) + Application.writeSetting(self.configGroup, "extraKeysLocation", self.keyLocation.getLocation()) + return config diff --git a/plugins/extensions/pykrita/plugin/plugins/comics_project_management_tools/comics_project_setup_wizard.py b/plugins/extensions/pykrita/plugin/plugins/comics_project_management_tools/comics_project_setup_wizard.py new file mode 100644 index 0000000000..c86f7f6411 --- /dev/null +++ b/plugins/extensions/pykrita/plugin/plugins/comics_project_management_tools/comics_project_setup_wizard.py @@ -0,0 +1,176 @@ +""" +Part of the comics project management tools (CPMT). + +This is a wizard that helps you set up a comics project in Krita. +""" + +import json # For writing to json. +import os # For finding the script location. +from pathlib import Path # For reading all the files in a directory. +import random # For selecting two random words from a list. +from PyQt5.QtWidgets import QWidget, QWizard, QWizardPage, QHBoxLayout, QFormLayout, QFileDialog, QLineEdit, QPushButton, QCheckBox, QLabel, QDialog +from PyQt5.QtCore import QDate, QLocale +from krita import * +from . import comics_metadata_dialog + +""" +The actual wizard. +""" + + +class ComicsProjectSetupWizard(): + setupDictionary = {} + projectDirectory = "" + + def __init__(self): + # super().__init__(parent) + # Search the location of the script for the two lists that are used with the projectname generator. + mainP = Path(__file__).parent + self.generateListA = [] + self.generateListB = [] + if Path(mainP / "projectGenLists" / "listA.txt").exists(): + for l in open(str(mainP / "projectGenLists" / "listA.txt"), "r"): + if l.isspace() == False: + self.generateListA.append(l.strip("\n")) + if Path(mainP / "projectGenLists" / "listB.txt").exists(): + for l in open(str(mainP / "projectGenLists" / "listB.txt"), "r"): + if l.isspace() == False: + self.generateListB.append(l.strip("\n")) + + def showDialog(self): + # Initialise the setup directory empty toavoid exceptions. + self.setupDictionary = {} + + # ask for a project directory. + self.projectDirectory = QFileDialog.getExistingDirectory(caption=i18n("Where should the comic project go?"), options=QFileDialog.ShowDirsOnly) + if os.path.exists(self.projectDirectory) is False: + return + self.pagesDirectory = os.path.relpath(self.projectDirectory, self.projectDirectory) + self.exportDirectory = os.path.relpath(self.projectDirectory, self.projectDirectory) + + wizard = QWizard() + wizard.setWindowTitle(i18n("Comic Project Setup")) + wizard.setOption(QWizard.IndependentPages, True) + + # Set up the UI for the wizard + basicsPage = QWizardPage() + basicsPage.setTitle(i18n("Basic Comic Project Settings")) + formLayout = QFormLayout() + basicsPage.setLayout(formLayout) + projectLayout = QHBoxLayout() + self.lnProjectName = QLineEdit() + basicsPage.registerField("Project Name*", self.lnProjectName) + self.lnProjectName.setToolTip(i18n("A Project name. This can be different from the eventual title")) + btnRandom = QPushButton() + btnRandom.setText(i18n("Generate")) + btnRandom.setToolTip(i18n("If you cannot come up with a project name, our highly sophisticated project name generator will serve to give a classy yet down to earth name.")) + btnRandom.clicked.connect(self.slot_generate) + projectLayout.addWidget(self.lnProjectName) + projectLayout.addWidget(btnRandom) + lnConcept = QLineEdit() + lnConcept.setToolTip(i18n("What is your comic about? This is mostly for your own convenience so don't worry about what it says too much.")) + self.cmbLanguage = comics_metadata_dialog.language_combo_box() + self.cmbLanguage.setToolTip(i18n("The main language the comic is in")) + self.cmbLanguage.setEntryToCode(str(QLocale.system().name()).split("_")[0]) + self.lnProjectDirectory = QLabel(self.projectDirectory) + self.chkMakeProjectDirectory = QCheckBox(i18n("Make a new directory with the project name.")) + self.chkMakeProjectDirectory.setToolTip(i18n("This allows you to select a generic comics project directory, in which a new folder will be made for the project using the given project name.")) + self.chkMakeProjectDirectory.setChecked(True) + self.lnPagesDirectory = QLineEdit() + self.lnPagesDirectory.setText(i18n("pages")) + self.lnPagesDirectory.setToolTip(i18n("The name for the folder where the pages are contained. If it doesn't exist, it will be created.")) + self.lnExportDirectory = QLineEdit() + self.lnExportDirectory.setText(i18n("export")) + self.lnExportDirectory.setToolTip(i18n("The name for the folder where the export is put. If it doesn't exist, it will be created.")) + self.lnTemplateLocation = QLineEdit() + self.lnTemplateLocation.setText(i18n("templates")) + self.lnTemplateLocation.setToolTip(i18n("The name for the folder where the page templates are sought in.")) + formLayout.addRow(i18n("Comic Concept:"), lnConcept) + formLayout.addRow(i18n("Project Name:"), projectLayout) + formLayout.addRow(i18n("Main Language:"), self.cmbLanguage) + + buttonMetaData = QPushButton(i18n("Meta Data")) + buttonMetaData.clicked.connect(self.slot_edit_meta_data) + + wizard.addPage(basicsPage) + + foldersPage = QWizardPage() + foldersPage.setTitle(i18n("Folder names and other.")) + folderFormLayout = QFormLayout() + foldersPage.setLayout(folderFormLayout) + folderFormLayout.addRow(i18n("Project Directory:"), self.lnProjectDirectory) + folderFormLayout.addRow("", self.chkMakeProjectDirectory) + folderFormLayout.addRow(i18n("Pages Directory"), self.lnPagesDirectory) + folderFormLayout.addRow(i18n("Export Directory"), self.lnExportDirectory) + folderFormLayout.addRow(i18n("Template Directory"), self.lnTemplateLocation) + folderFormLayout.addRow("", buttonMetaData) + wizard.addPage(foldersPage) + + # Execute the wizard, and after wards... + if (wizard.exec_()): + + # First get the directories, check if the directories exist, and oterwise make them. + self.pagesDirectory = self.lnPagesDirectory.text() + self.exportDirectory = self.lnExportDirectory.text() + self.templateLocation = self.lnTemplateLocation.text() + projectPath = Path(self.projectDirectory) + # Only make a project directory if the checkbox for that has been checked. + if self.chkMakeProjectDirectory.isChecked(): + projectPath = projectPath / self.lnProjectName.text() + if projectPath.exists() is False: + projectPath.mkdir() + self.projectDirectory = str(projectPath) + if Path(projectPath / self.pagesDirectory).exists() is False: + Path(projectPath / self.pagesDirectory).mkdir() + if Path(projectPath / self.exportDirectory).exists() is False: + Path(projectPath / self.exportDirectory).mkdir() + if Path(projectPath / self.templateLocation).exists() is False: + Path(projectPath / self.templateLocation).mkdir() + + # Then store the information into the setup diactionary. + self.setupDictionary["projectName"] = self.lnProjectName.text() + self.setupDictionary["concept"] = lnConcept.text() + self.setupDictionary["language"] = str(self.cmbLanguage.codeForCurrentEntry()) + self.setupDictionary["pagesLocation"] = self.pagesDirectory + self.setupDictionary["exportLocation"] = self.exportDirectory + self.setupDictionary["templateLocation"] = self.templateLocation + + # Finally, write the dictionary into the json file. + self.writeConfig() + """ + This calls up the metadata dialog, for if people already have information they want to type in + at the setup stage. Not super likely, but the organisation and management aspect of the comic + manager means we should give the option to organise as smoothly as possible. + """ + + def slot_edit_meta_data(self): + dialog = comics_metadata_dialog.comic_meta_data_editor() + self.setupDictionary["language"] = str(self.cmbLanguage.codeForCurrentEntry()) + dialog.setConfig(self.setupDictionary) + dialog.setConfig(self.setupDictionary) + if (dialog.exec_() == QDialog.Accepted): + self.setupDictionary = dialog.getConfig(self.setupDictionary) + self.cmbLanguage.setEntryToCode(self.setupDictionary["language"]) + """ + Write the actual config to the chosen project directory. + """ + + def writeConfig(self): + print("CPMT: writing comic configuration...") + print(self.projectDirectory) + configFile = open(os.path.join(self.projectDirectory, "comicConfig.json"), "w", newline="", encoding="utf-16") + json.dump(self.setupDictionary, configFile, indent=4, sort_keys=True, ensure_ascii=False) + configFile.close() + print("CPMT: done") + """ + As you may be able to tell, the random projectname generator is hardly sophisticated. + It picks a word from a list of names of figures from Greek Mythology, and a name from a list + of vegetables and fruits and combines the two camelcased. + It makes for good codenames at the least. + """ + + def slot_generate(self): + if len(self.generateListA) > 0 and len(self.generateListB) > 0: + nameA = self.generateListA[random.randint(0, len(self.generateListA) - 1)] + nameB = self.generateListB[random.randint(0, len(self.generateListB) - 1)] + self.lnProjectName.setText(str(nameA.title() + nameB.title())) diff --git a/plugins/extensions/pykrita/plugin/plugins/comics_project_management_tools/comics_template_dialog.py b/plugins/extensions/pykrita/plugin/plugins/comics_project_management_tools/comics_template_dialog.py new file mode 100644 index 0000000000..ef45a9c8af --- /dev/null +++ b/plugins/extensions/pykrita/plugin/plugins/comics_project_management_tools/comics_template_dialog.py @@ -0,0 +1,283 @@ +""" +Part of the comics project management tools (CPMT). + +Template dialog +""" +import os +import shutil +#from PyQt5.QtGui import * +from PyQt5.QtWidgets import QDialog, QComboBox, QDialogButtonBox, QVBoxLayout, QFormLayout, QGridLayout, QWidget, QPushButton, QHBoxLayout, QLabel, QSpinBox, QDoubleSpinBox, QLineEdit, QTabWidget +from PyQt5.QtCore import QLocale, Qt, QByteArray +from krita import * +""" +Quick and dirty QComboBox subclassing that handles unitconversion for us. +""" + + +class simpleUnitBox(QComboBox): + pixels = i18n("Pixels") + inches = i18n("Inches") + centimeter = i18n("Centimeter") + millimeter = i18n("millimeter") + + def __init__(self): + super(simpleUnitBox, self).__init__() + self.addItem(self.pixels) + self.addItem(self.inches) + self.addItem(self.centimeter) + self.addItem(self.millimeter) + + if QLocale().system().measurementSystem() is QLocale.MetricSystem: + self.setCurrentIndex(2) # set to centimeter if metric system. + else: + self.setCurrentIndex(1) + + def pixelsForUnit(self, unit, DPI): + if (self.currentText() == self.pixels): + return unit + elif (self.currentText() == self.inches): + return self.inchesToPixels(unit, DPI) + elif (self.currentText() == self.centimeter): + return self.centimeterToPixels(unit, DPI) + elif (self.currentText() == self.millimeter): + return self.millimeterToPixels(unit, DPI) + + def inchesToPixels(self, inches, DPI): + return DPI * inches + + def centimeterToInches(self, cm): + return cm / 2.54 + + def centimeterToPixels(self, cm, DPI): + return self.inchesToPixels(self.centimeterToInches(cm), DPI) + + def millimeterToCentimeter(self, mm): + return mm / 10 + + def millimeterToPixels(self, mm, DPI): + return self.inchesToPixels(self.centimeterToInches(self.millimeterToCentimeter(mm)), DPI) + + +class comics_template_dialog(QDialog): + templateDirectory = str() + templates = QComboBox() + buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) + + def __init__(self, templateDirectory): + super().__init__() + self.templateDirectory = templateDirectory + self.setWindowTitle(i18n("Add new template")) + self.setLayout(QVBoxLayout()) + + self.templates.setEnabled(False) + self.fill_templates() + + self.buttons.accepted.connect(self.accept) + self.buttons.rejected.connect(self.reject) + self.buttons.button(QDialogButtonBox.Ok).setEnabled(False) + mainWidget = QWidget() + self.layout().addWidget(mainWidget) + self.layout().addWidget(self.buttons) + mainWidget.setLayout(QVBoxLayout()) + + btn_create = QPushButton(i18n("Create a template")) + btn_create.clicked.connect(self.slot_create_template) + btn_import = QPushButton(i18n("Import templates")) + btn_import.clicked.connect(self.slot_import_template) + mainWidget.layout().addWidget(self.templates) + mainWidget.layout().addWidget(btn_create) + mainWidget.layout().addWidget(btn_import) + + def fill_templates(self): + self.templates.clear() + for entry in os.scandir(self.templateDirectory): + if entry.name.endswith('.kra') and entry.is_file(): + name = os.path.relpath(entry.path, self.templateDirectory) + self.templates.addItem(name) + if self.templates.model().rowCount() > 0: + self.templates.setEnabled(True) + self.buttons.button(QDialogButtonBox.Ok).setEnabled(True) + + def slot_create_template(self): + create = comics_template_create(self.templateDirectory) + + if create.exec_() == QDialog.Accepted: + if (create.prepare_krita_file()): + self.fill_templates() + + def slot_import_template(self): + filenames = QFileDialog.getOpenFileNames(caption=i18n("Which files should be added to the template folder?"), directory=self.templateDirectory, filter=str(i18n("Krita files") + "(*.kra)"))[0] + for file in filenames: + shutil.copy2(file, self.templateDirectory) + self.fill_templates() + + def url(self): + return os.path.join(self.templateDirectory, self.templates.currentText()) + + +class comics_template_create(QDialog): + urlSavedTemplate = str() + templateDirectory = str() + + def __init__(self, templateDirectory): + super().__init__() + self.templateDirectory = templateDirectory + self.setWindowTitle(i18n("Create new template")) + self.setLayout(QVBoxLayout()) + buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) + buttons.accepted.connect(self.accept) + buttons.rejected.connect(self.reject) + mainWidget = QWidget() + self.layout().addWidget(mainWidget) + self.layout().addWidget(buttons) + mainWidget.setLayout(QHBoxLayout()) + explanation = QLabel(i18n("This allows you to make a template document with guides.\nThe width and height are the size of the live-area, the safe area is the live area minus the margins, and the full image is the live area plus the bleeds.")) + explanation.setWordWrap(True) + elements = QWidget() + elements.setLayout(QVBoxLayout()) + mainWidget.layout().addWidget(elements) + elements.layout().addWidget(explanation) + + self.templateName = QLineEdit() + elements.layout().addWidget(self.templateName) + + self.DPI = QSpinBox() + self.DPI.setMaximum(1200) + self.DPI.setValue(300) + self.spn_width = QDoubleSpinBox() + self.spn_width.setMaximum(10000) + self.spn_height = QDoubleSpinBox() + self.spn_height.setMaximum(10000) + self.widthUnit = simpleUnitBox() + self.heightUnit = simpleUnitBox() + + widgetSize = QWidget() + sizeForm = QFormLayout() + sizeForm.addRow(i18n("DPI:"), self.DPI) + widthLayout = QHBoxLayout() + widthLayout.addWidget(self.spn_width) + widthLayout.addWidget(self.widthUnit) + sizeForm.addRow(i18n("Width:"), widthLayout) + heightLayout = QHBoxLayout() + heightLayout.addWidget(self.spn_height) + heightLayout.addWidget(self.heightUnit) + sizeForm.addRow(i18n("Height:"), heightLayout) + widgetSize.setLayout(sizeForm) + elements.layout().addWidget(widgetSize) + + marginAndBleed = QTabWidget() + elements.layout().addWidget(marginAndBleed) + + margins = QWidget() + marginForm = QGridLayout() + margins.setLayout(marginForm) + self.marginLeft = QDoubleSpinBox() + self.marginLeft.setMaximum(1000) + self.marginLeftUnit = simpleUnitBox() + self.marginRight = QDoubleSpinBox() + self.marginRight.setMaximum(1000) + self.marginRightUnit = simpleUnitBox() + self.marginTop = QDoubleSpinBox() + self.marginTop.setMaximum(1000) + self.marginTopUnit = simpleUnitBox() + self.marginBottom = QDoubleSpinBox() + self.marginBottom.setMaximum(1000) + self.marginBottomUnit = simpleUnitBox() + marginForm.addWidget(QLabel(i18n("Left:")), 0, 0, Qt.AlignRight) + marginForm.addWidget(self.marginLeft, 0, 1) + marginForm.addWidget(self.marginLeftUnit, 0, 2) + marginForm.addWidget(QLabel(i18n("Top:")), 1, 0, Qt.AlignRight) + marginForm.addWidget(self.marginTop, 1, 1) + marginForm.addWidget(self.marginTopUnit, 1, 2) + marginForm.addWidget(QLabel(i18n("Right:")), 2, 0, Qt.AlignRight) + marginForm.addWidget(self.marginRight, 2, 1) + marginForm.addWidget(self.marginRightUnit, 2, 2) + marginForm.addWidget(QLabel(i18n("Bottom:")), 3, 0, Qt.AlignRight) + marginForm.addWidget(self.marginBottom, 3, 1) + marginForm.addWidget(self.marginBottomUnit, 3, 2) + marginAndBleed.addTab(margins, i18n("Margins")) + + bleeds = QWidget() + bleedsForm = QGridLayout() + bleeds.setLayout(bleedsForm) + self.bleedLeft = QDoubleSpinBox() + self.bleedLeft.setMaximum(1000) + self.bleedLeftUnit = simpleUnitBox() + self.bleedRight = QDoubleSpinBox() + self.bleedRight.setMaximum(1000) + self.bleedRightUnit = simpleUnitBox() + self.bleedTop = QDoubleSpinBox() + self.bleedTop.setMaximum(1000) + self.bleedTopUnit = simpleUnitBox() + self.bleedBottom = QDoubleSpinBox() + self.bleedBottom.setMaximum(1000) + self.bleedBottomUnit = simpleUnitBox() + bleedsForm.addWidget(QLabel(i18n("Left:")), 0, 0, Qt.AlignRight) + bleedsForm.addWidget(self.bleedLeft, 0, 1) + bleedsForm.addWidget(self.bleedLeftUnit, 0, 2) + bleedsForm.addWidget(QLabel(i18n("Top:")), 1, 0, Qt.AlignRight) + bleedsForm.addWidget(self.bleedTop, 1, 1) + bleedsForm.addWidget(self.bleedTopUnit, 1, 2) + bleedsForm.addWidget(QLabel(i18n("Right:")), 2, 0, Qt.AlignRight) + bleedsForm.addWidget(self.bleedRight, 2, 1) + bleedsForm.addWidget(self.bleedRightUnit, 2, 2) + bleedsForm.addWidget(QLabel(i18n("Bottom:")), 3, 0, Qt.AlignRight) + bleedsForm.addWidget(self.bleedBottom, 3, 1) + bleedsForm.addWidget(self.bleedBottomUnit, 3, 2) + marginAndBleed.addTab(bleeds, i18n("Bleeds")) + + def prepare_krita_file(self): + wBase = self.widthUnit.pixelsForUnit(self.spn_width.value(), self.DPI.value()) + bL = self.bleedLeftUnit.pixelsForUnit(self.bleedLeft.value(), self.DPI.value()) + bR = self.bleedRightUnit.pixelsForUnit(self.bleedRight.value(), self.DPI.value()) + mL = self.marginLeftUnit.pixelsForUnit(self.marginLeft.value(), self.DPI.value()) + mR = self.marginRightUnit.pixelsForUnit(self.marginRight.value(), self.DPI.value()) + + hBase = self.heightUnit.pixelsForUnit(self.spn_height.value(), self.DPI.value()) + bT = self.bleedTopUnit.pixelsForUnit(self.bleedTop.value(), self.DPI.value()) + bB = self.bleedBottomUnit.pixelsForUnit(self.bleedBottom.value(), self.DPI.value()) + mT = self.marginTopUnit.pixelsForUnit(self.marginTop.value(), self.DPI.value()) + mB = self.marginBottomUnit.pixelsForUnit(self.marginBottom.value(), self.DPI.value()) + + template = Application.createDocument((wBase + bL + bR), (hBase + bT + bB), self.templateName.text(), "RGBA", "U8", "sRGB built-in") + template.setResolution(self.DPI.value()) + + backgroundNode = template.activeNode() + backgroundNode.setName(i18n("Background")) + pixelByteArray = QByteArray() + pixelByteArray = backgroundNode.pixelData(0, 0, (wBase + bL + bR), (hBase + bT + bB)) + white = int(255) + pixelByteArray.fill(white.to_bytes(1, byteorder='little')) + backgroundNode.setPixelData(pixelByteArray, 0, 0, (wBase + bL + bR), (hBase + bT + bB)) + backgroundNode.setLocked(True) + + sketchNode = template.createNode(i18n("Sketch"), "paintlayer") + template.rootNode().setChildNodes([backgroundNode, sketchNode]) + + verticalGuides = [] + verticalGuides.append(bL) + verticalGuides.append(bL + mL) + verticalGuides.append((bL + wBase) - mR) + verticalGuides.append(bL + wBase) + + horizontalGuides = [] + horizontalGuides.append(bT) + horizontalGuides.append(bT + mT) + horizontalGuides.append((bT + hBase) - mB) + horizontalGuides.append(bT + hBase) + + template.setHorizontalGuides(horizontalGuides) + template.setVerticalGuides(verticalGuides) + template.setGuidesVisible(True) + template.setGuidesLocked(True) + + self.urlSavedTemplate = os.path.join(self.templateDirectory, self.templateName.text() + ".kra") + succes = template.exportImage(self.urlSavedTemplate, InfoObject()) + print("CPMT: Template", self.templateName.text(), "made and saved.") + template.waitForDone() + template.close() + + return succes + + def url(self): + return self.urlSavedTemplate diff --git a/plugins/extensions/pykrita/plugin/plugins/comics_project_management_tools/isoLanguagesList.csv b/plugins/extensions/pykrita/plugin/plugins/comics_project_management_tools/isoLanguagesList.csv new file mode 100644 index 0000000000..3e1716e1b8 --- /dev/null +++ b/plugins/extensions/pykrita/plugin/plugins/comics_project_management_tools/isoLanguagesList.csv @@ -0,0 +1,191 @@ +Abkhazian,ab, +Afar,aa, +Afrikaans,af, +Akan,ak, +Albanian,sq, +Amharic,am, +Arabic,ar, +Aragonese,an, +Armenian,hy, +Assamese,as, +Avaric,av, +Avestan,ae, +Aymara,ay, +Azerbaijani,az, +Bambara,bm, +Bashkir,ba, +Basque,eu, +Belarusian,be, +Bengali (Bangla),bn, +Bihari,bh, +Bislama,bi, +Bosnian,bs, +Breton,br, +Bulgarian,bg, +Burmese,my, +Catalan,ca, +Chamorro,ch, +Chechen,ce, +Chichewa,ny, +Chinese,zh, +Chinese (Simplified),zh-Hans, +Chinese (Traditional),zh-Hant, +Chuvash,cv, +Cornish,kw, +Corsican,co, +Cree,cr, +Croatian,hr, +Czech,cs, +Danish,da, +Divehi,dv, +Dutch,nl, +Dzongkha,dz, +English,en, +Esperanto,eo, +Estonian,et, +Ewe,ee, +Faroese,fo, +Fijian,fj, +Finnish,fi, +French,fr, +Fula,ff, +Galician,gl, +Gaelic (Scottish),gd, +Gaelic (Manx),gv, +Georgian,ka, +German,de, +Greek,el, +Greenlandic,kl, +Guarani,gn, +Gujarati,gu, +Haitian Creole,ht, +Hausa,ha, +Hebrew,he, +Herero,hz, +Hindi,hi, +Hiri Motu,ho, +Hungarian,hu, +Icelandic,is, +Ido,io, +Igbo,ig, +Indonesian,in, +Interlingua,ia, +Interlingue,ie, +Inuktitut,iu, +Inupiak,ik, +Irish,ga, +Italian,it, +Japanese,ja, +Javanese,jv, +Kalaallisut,kl, +Kannada,kn, +Kanuri,kr, +Kashmiri,ks, +Kazakh,kk, +Khmer,km, +Kikuyu,ki, +Kinyarwanda,rw, +Kirundi,rn, +Kyrgyz,ky, +Komi,kv, +Kongo,kg, +Korean,ko, +Kurdish,ku, +Kwanyama,kj, +Lao,lo, +Latin,la, +Latvian,lv, +Limburgish,li, +Lingala,ln, +Lithuanian,lt, +Luga-Katanga,lu, +Luganda,lg, +Luxembourgish,lb, +Manx,gv, +Macedonian,mk, +Malagasy,mg, +Malay,ms, +Malayalam,ml, +Maltese,mt, +Maori,mi, +Marathi,mr, +Marshallese,mh, +Moldavian,mo, +Mongolian,mn, +Nauru,na, +Navajo,nv, +Ndonga,ng, +Northern Ndebele,nd, +Nepali,ne, +Norwegian,no, +Norwegian bokmal,nb, +Norwegian nynorsk,nn, +Nuosu,ii, +Occitan,oc, +Ojibwe,oj, +Old Church Slavonic,cu, +Oriya,or, +Oromo,om, +Ossetian,os, +P?li,pi, +Pashto,ps, +Persian,fa, +Polish,pl, +Portuguese,pt, +Punjab,pa, +Quechua,qu, +Romansh,rm, +Romanian,ro, +Russian,ru, +Sami,se, +Samoan,sm, +Sango,sg, +Sanskrit,sa, +Serbian,sr, +Serbo-Croatian,sh, +Sesotho,st, +Setswana,tn, +Shona,sn, +Sichuan Yi,ii, +Sindhi,sd, +Sinhalese,si, +Siswati,ss, +Slovak,sk, +Slovenian,sl, +Somali,so, +Southern Ndebele,nr, +Spanish,es, +Sundanese,su, +Swahili,sw, +Swati,ss, +Swedish,sv, +Tagalog,tl, +Tahitian,ty, +Tajik,tg, +Tamil,ta, +Tatar,tt, +Telugu,te, +Thai,th, +Tibetan,bo, +Tigrinya,ti, +Tonga,to, +Tsonga,ts, +Turkish,tr, +Turkmen,tk, +Twi,tw, +Uyghur,ug, +Ukrainian,uk, +Urdu,ur, +Uzbek,uz, +Venda,ve, +Vietnamese,vi, +Volapk,vo, +Wallon,wa, +Welsh,cy, +Wolof,wo, +Western Frisian,fy, +Xhosa,xh, +Yiddish,yi, +Yoruba,yo, +Zhuang, Chuang,za +Zulu,zu, diff --git a/plugins/extensions/pykrita/plugin/plugins/comics_project_management_tools/key_author_roles/acbf_authorroles.txt b/plugins/extensions/pykrita/plugin/plugins/comics_project_management_tools/key_author_roles/acbf_authorroles.txt new file mode 100644 index 0000000000..2387eb0ffd --- /dev/null +++ b/plugins/extensions/pykrita/plugin/plugins/comics_project_management_tools/key_author_roles/acbf_authorroles.txt @@ -0,0 +1,13 @@ +Writer +Adapter +Artist +Penciller +Inker +Colorist +Letterer +Cover Artist +Photographer +Editor +Assistant Editor +Translator +Other diff --git a/plugins/extensions/pykrita/plugin/plugins/comics_project_management_tools/key_format/formats.txt b/plugins/extensions/pykrita/plugin/plugins/comics_project_management_tools/key_format/formats.txt new file mode 100644 index 0000000000..b348162d46 --- /dev/null +++ b/plugins/extensions/pykrita/plugin/plugins/comics_project_management_tools/key_format/formats.txt @@ -0,0 +1,8 @@ +Gag-a-day +Graphic Novel +Epistolary +Cartoon +Oneshot +4-koma +Wordless + diff --git a/plugins/extensions/pykrita/plugin/plugins/comics_project_management_tools/key_genre/acbf_genres.txt b/plugins/extensions/pykrita/plugin/plugins/comics_project_management_tools/key_genre/acbf_genres.txt new file mode 100644 index 0000000000..d342f26950 --- /dev/null +++ b/plugins/extensions/pykrita/plugin/plugins/comics_project_management_tools/key_genre/acbf_genres.txt @@ -0,0 +1,26 @@ +science_fiction +fantasy +adventure +horror +mystery +crime +military +real_life +superhero +humor +western +manga +politics +caricature +sports +history +biography +education +computer +religion +romance +children +non-fiction +adult +alternative +other diff --git a/plugins/extensions/pykrita/plugin/plugins/comics_project_management_tools/key_rating/DC.csv b/plugins/extensions/pykrita/plugin/plugins/comics_project_management_tools/key_rating/DC.csv new file mode 100644 index 0000000000..871c01f0fd --- /dev/null +++ b/plugins/extensions/pykrita/plugin/plugins/comics_project_management_tools/key_rating/DC.csv @@ -0,0 +1,5 @@ +Title,DC content rating system +E,Everyone - Appropriate for readers of all ages. May contain cartoon violence and/or some comic mischief. +T,"Teen - Appropriate for readers age 12 and older. May contain mild violence, language and/or suggestive themes." +T+,"Teen Plus - Appropriate for readers age 16 and older. May contain moderate violence, mild profanity, graphic imagery and/or suggestive themes." +M,"Mature - Appropriate for readers age 18 and older. May contain intense violence, extensive profanity, nudity, sexual themes and other content suitable only for older readers." diff --git a/plugins/extensions/pykrita/plugin/plugins/comics_project_management_tools/key_rating/FictionRatingsDotCom.csv b/plugins/extensions/pykrita/plugin/plugins/comics_project_management_tools/key_rating/FictionRatingsDotCom.csv new file mode 100644 index 0000000000..94a10d7685 --- /dev/null +++ b/plugins/extensions/pykrita/plugin/plugins/comics_project_management_tools/key_rating/FictionRatingsDotCom.csv @@ -0,0 +1,6 @@ +Title,FictionRatings.com +K,"Intended for general audience 5 years and older. Content should be free of any coarse language, violence, and adult themes. " +K+,"Suitable for more mature childen, 9 years and older, with minor action violence without serious injury. May contain mild coarse language. Should not contain any adult themes. " +T,"Suitable for teens, 13 years and older, with some violence, minor coarse language, and minor suggestive adult themes. " +M,"Not suitable for children or teens below the age of 16 with non-explicit suggestive adult themes, references to some violence, or coarse language. Fiction M can contain adult language, themes and suggestions. Detailed descriptions of physical interaction of sexual or violent nature is considered Fiction MA. " +MA,Content is only suitable for mature adults. May contain explicit language and adult themes. diff --git a/plugins/extensions/pykrita/plugin/plugins/comics_project_management_tools/key_rating/Marvel.csv b/plugins/extensions/pykrita/plugin/plugins/comics_project_management_tools/key_rating/Marvel.csv new file mode 100644 index 0000000000..cd5a372b6d --- /dev/null +++ b/plugins/extensions/pykrita/plugin/plugins/comics_project_management_tools/key_rating/Marvel.csv @@ -0,0 +1,6 @@ +Title,Marvel Content Rating system +ALL AGES,Appropriate for all ages. +T,"Appropriate for most readers, but parents are advised that they might want to read before or with younger children." +T+,T+ TEENS AND UP - Appropriate for teens 13 and above. +PARENTAL ADVISORY,"PARENTAL ADVISORY - Appropriate for older teens. Similar to T+, but featuring more mature themes and/or more graphic imagery. Recommended for teen and adult readers." +MAX: EXPLICIT CONTENT,"MAX: EXPLICIT CONTENT - 18+ years old Most Mature Readers books will fall under the MAX Comics banner, (created specifically for mature content titles) MAX and Mature-themed titles will continue to be designed to appear distinct from mainline Marvel titles, with the ""MAX: Explicit Content"" label very prominently displayed on the cover. MAX titles will not be sold on the newsstand, and they will not be sold to younger readers. It says anything from explicit to non-explicit." diff --git a/plugins/extensions/pykrita/plugin/plugins/comics_project_management_tools/kritapykrita_comics_project_management_tools.desktop b/plugins/extensions/pykrita/plugin/plugins/comics_project_management_tools/kritapykrita_comics_project_management_tools.desktop new file mode 100644 index 0000000000..5854e8b943 --- /dev/null +++ b/plugins/extensions/pykrita/plugin/plugins/comics_project_management_tools/kritapykrita_comics_project_management_tools.desktop @@ -0,0 +1,19 @@ +[Desktop Entry] +Type=Service +ServiceTypes=Krita/PythonPlugin +X-KDE-Library=comics_project_management_tools +X-Python-2-Compatible=false +Name=Comics Project Management Tools +Name[ca]=Eines per a la gestió dels projectes de còmics +Name[ca@valencia]=Eines per a la gestió dels projectes de còmics +Name[nl]=Hulpmiddelen voor projectbeheer van strips +Name[pt]=Ferramentas de Gestão de Projectos de Banda Desenhada +Name[uk]=Інструменти для керування проектами коміксів +Name[x-test]=xxComics Project Management Toolsxx +Comment=Tools for managing comics. +Comment[ca]=Eines per a gestionar els còmics. +Comment[ca@valencia]=Eines per a gestionar els còmics. +Comment[nl]=Hulpmiddelen voor beheer van strips. +Comment[pt]=Ferramentas para gerir bandas desenhadas. +Comment[uk]=Інструменти для керування коміксами +Comment[x-test]=xxTools for managing comics.xx diff --git a/plugins/extensions/pykrita/plugin/plugins/comics_project_management_tools/projectGenLists/listA.txt b/plugins/extensions/pykrita/plugin/plugins/comics_project_management_tools/projectGenLists/listA.txt new file mode 100644 index 0000000000..03be261e2a --- /dev/null +++ b/plugins/extensions/pykrita/plugin/plugins/comics_project_management_tools/projectGenLists/listA.txt @@ -0,0 +1,855 @@ + +Athena +Artemis +Ares +Aphrodite +Apollo +Demeter +Dionysus +Hades +Hera +Hestia +Hermes +Hephaestus +Poseidon +Zeus + +Achlys +Aion +Aether +Ananke +Chaos +Chronos +Erebus +Eros +Hemera +Hypnos +Gaia +Gaea +Nemesis +Nesoi +Nyx +Uranus +Ourea +Phanes +Pontus +Tartarus +Thalassa +Thanatos + +Coeus +Crius +Cronus +Hyperion +Iapetus +Menmosyne +Oceanus +Phoebe +Rhea +Tethys +Theia +Themis + +Asteria +Astraeus +Atlas +Aura +Clymene +Dione +Helios +Selene +Eos +Epometheus +Eurybia +Eurynome +Lelantos +Leto +Menoetius +Metis +Ophion +Pallas +Perses +Prometheus +Styx + +Agrius +Alcyoneus +Chthonius +Clytius +Enceladus +Ephialtes +Eurymedon +Eurytus +Gration +Hippolytus +Leon +Mimas +Polybotes +Porphyrion +Thoas +Thoon + +Aloadae +Anax +Antaeus +Antiphates +ArgusPanoptes +Asterius +Cacus +Arges +Brontes +Steropes +Cyclopes +Polyphemus +Gegeness +Geryon +Hekatonkheires +Briareus +Cottus +Gyges +Laestrygonians +Orion +Talos +Tityos +Typhon + +Adephagia +Adikia +Aergia +Agathodeamon +Agon +Aidos +Aisa +Alala +Alastor +Aletheia +Algea +Achos +Ania +Lupe +Alke +Amechania +Amphilogiai +Anaideia +Androktasiai +Angelia +Apate +Apheleia +Aporia +Area +Arete +Atee +Bia +Caerus +Corus +Deimos +Dikaiosyne +Dikee +Dolos +Dysnomia +Dyssebeia +Eirene +Ekecheiria +Eleos +Elpis +Epiphron +Eris +Erotes +Anteros +Hedylogos +Hermaphroditus +Himeros +Hymenaeus +Photos +Eucleia +Eulabeia +Eunomia +Eupheme +Eupraxia +Eusebeia +Euthenia +Gelos +Geras +Harmonia +Hebe +Hedone +Heimarmene +Homados +Homonoia +Horkos +Horme +Hybris +Hysminai +Ioke +Kakia +Kalogathia +Keres +Koalemos +Kratos +Kyodoimos +Lethe +Limos +Litea +Lyssa +Machai +Mania +Moirai +Clotho +Lachesis +Atropos +Momus +Morus +Nike +Nomos +Oizys +Oneiroi +Epiales +Morpheus +Phantasos +Phobetor +Palioxis +Peitharchia +Peitho +Penia +Penthus +Pepromene +Pheme +Philophrosyne +Philotes +Phonoi +Phrike +Phthonus +Pistis +Poine +Polemos +Ponos +Poros +Praxidike +Proioxis +Prophasis +Ptocheia +Roma +Soter +Soteria +Sophrosyne +Techne +Thrasos +Tuche +Zelos + +Amphiaraus +Angelos +Askalaphos +Cerberus +Charon +Empusa +Erebos +Erinyes +Alecto +Tisiphone +Megaera +Hecate +Aiakos +Minos +Rhadamathys +Keuthonymos +Lamia +Lampades +Goygyra +Orphne +Macaria +Melinoe +Menoetes +Mormo +Nyx +Persephone +Archeron +Kokytos +Phlegethon + +Aegaeon +Achelous +Amphritrite +Benthesikyme +Brizo +Ceto +Charybdis +Cymopoleia +Delphin +Eidothea +Glaucus +Gorgons +Stheno +Euryale +Medusa +Greaeae +Deino +Enyo +Pemphredo +Harpeia +Aello +Aellope +Ocypete +Ocypode +Ocythoe +Poarge +Podarke +Celaeno +Nicothoe +Hippocampi +Ichtyocentaurs +Bythos +Aphros +Karkinos +Ladon +Leucothea +Nereides +Thetis +Arethusa +Gelene +Psamathe +Nereus +Nerites +Nilus +Palaemon +Phorcys +Pontos +Proteus +Sangarius +Scylla +Sirens +Aglaope +Aglaphonos +Aglapheme +Himerope +Leucosia +Ligeia +Molpe +Parthenope +Peisinoe +Peisithoe +Raidne +Teles +Thechtereia +Theciope +Thelxiepeia +Telchines +Actaeus +Argyron +Atabyrius +Chalcon +Chryson +Damon +Demonax +Damnameneus +Dexithea +Lycos +Lysagora +Makelo +Megalesius +Mylas +Nikon +Ormenos +Simon +Skelmis +Thaumas +Thoosa +Triteia +Triton +Tritones + +Achelois +Aeolus +Alectrone +Aparctias +Apheliotes +Argestes +Caicias +Circios +Euronotus +Lipse +Skeiron +Arke +Astaios +Stilbon +Eosphorus +Hesperus +Pyroeis +Phaethon +Phaenon +Dios +Aurai +Aura +Chione +Sabazios +Menmosyne +Hesperides +Pleiades +Iris +Nephelai +Pandia +Ersa +Anemoi +Boreas +Eurus +Notus +Zephyrus +Alcyone +Sterope +Electra +Mara +Merope +Taygete + +Aetna +Amphictyonis +Anthousai +Aristaeus +Attis +Britomartis +Cabeiri +Aitnaois +Alkon +Eurymedon +Onnes +Tonnes +Centaur +Asbolus +Chariclo +Chiron +Eurytion +Nessus +Pholus +Cercopes +Akmon +Passalos +Chloris +Comus +Corymbus +Curetes +Cybele +Dindymene +Dactyls +Acmon +Delas +Epimedes +Heracles +Iasios +Kelmis +Skythes +Titias +Cellenus +Dryades +Epimeliodes +Hamadryades +Hecaterus +Horae +Thallo +Auxo +Karpo +Pherousa +Euporie +Othesie +Auge +Anatole +Anatolia +Mousika +Musica +Gymnastika +Gymnastica +Nymphe +Mesembria +Sponde +EleteAkte +Acte +Hesperis +Dysis +Arktos +Eiar +Theros +Pthinoporon +Cheimon +Korybantes +Damneus +Idaios +Kyrbas +Okythoos +Pymneus +Pyrrhichos +Ma +Maenades +Methe +Meliae +Naiades +Daphne +Metope +Minthe +Hekaerge +Loco +Oupis +Oreades +Adrasteia +Echo +Oceanides +Idyia +Ourae +Palici +Pan +Potamoi +Acis +Alpheus +Asopus +Cladeus +Eurotas +Peneus +Scamander +Priapus +Rhea +Satyress +Krotos +Silenus +Telete +Zagreus + +Adonis +Aphaea +Carme +Camanor +Chrysothemis +Cyamites +Despoina +Eunostus +Philomelus +Plutus +Triptolemus + +Asclepius +Aceso +Aegle +Epione +Hygieia +Iaso +Panacea +Telesphorus + +Acratopotes +Adrastrea +Agdistis +Alexiares +Anicetus +Aphroditus +Astraea +Auxesia +Damia +Charites +Aglaea +Euphrosyne +Thalia +Hegemone +Antheia +Pastithea +Cleta +Phaenna +Eudaimonia +Euthemia +Calleis +Paidia +Pandasia +Pannychis +Ceraon +Chrysus +Circe +DeamonesCeramici +Syntribos +Smaragos +Asbetos +Sabaktes +Omodamos +Deipneus +Euresione +Eileithyia +Enyalius +Enyo +Glycon +Harpocrates +Hymenaios +Ichnaea +Iynx +Matton +Muses +Aoide +Arche +Melete +Mneme +Thelxinoe +Calliope +Clio +Euterpe +Erato +Melpomone +Polyhymnia +Terpsichore +Thalia +Urania +Cephisso +Apollonis +Borysthenis +Hypate +Mese +Nete +Polymatheia +Rhapso +Taraxippus + +Achilles +Aiakos +Alabandus +Ariadne +Bolina +Dioscuri +Castor +Pollux +Endymion +Ganymede +Hemithea +Parthenos +Lampsace +Ino +Tenes +Leucippides +Hilaera +Orithyia +Phylonoe +Psyche +Semele + +Abderus +Aeneas +Ajax +Amphitryon +Antilochus +Bellerophon +Chrysippus +Deadalus +Diomedes +Eleusis +Hector +Icarus +Iolaus +Jason +Meleager +Odysseus +Orpheus +Pandion +Perseus +Theseus +Alcestis +Amymone +Andromache +Andromeda +Antigone +Arachne +Atalanta +Briseis +Ceaneus +Cassandra +Clytemnestra +Danaee +Deianeira +Europa +Hecuba +Helen +Hermione +Iphigenia +Ismene +Jocasta +Medea +Niobe +Pandora +Penelope +Polyxena +Thrace + +Abas +Acastus +Acrisius +Admetus +Adrastus +Aeacus +Aietes +Aegeus +Aegimius +Aegisthus +Aegyptus +Aeson +Aeethlius +Aetolus +Agamemnon +Agasthenes +Agenor +Alcinous +Alcmaeon +Aleus +Amphiaraus +Amphictyon +Apmhion +Zethus +Amycus +Bebrycus +Anaxagoras +Anchhises +Arcesius +Argeus +Assaracus +Asterion +Athamas +Atreus +Autesion +Bias +Busiris +Cadmus +Car +Catreus +Cecrops +Ceisus +Celeus +Cephalus +Cepheus +Charnabon +Cinyras +Codrus +Corinthus +Craneus +Creon +Cres +Cresphontes +Cretheus +Criasus +Cylarabes +Cynortas +Cyzinus +Danaus +Dardanus +Deiphontes +Demophon +Echemus +Echetus +Eetion +Electryon +Elephenor +Eleusis +Epaphus +Epopeus +Echteus +Erginus +Erichthonius +Eteocles +Eurystheus +Euxanthius +Gelanor +Heamus +Helenus +Hippothooen +Hyrieus +Ilus +Ixion +Laeertes +Laomedon +Lycaon +Lycurgus +Makedon +Megareus +Melampus +Manths +Meneleus +Menstheus +Midas +Myles +Nestor +Nycteus +Oebalus +Oedipus +Oeneus +Oenoprion +Oygus +Oicles +Oileus +Orestes +Oxyntes +Peleus +Pelias +Pelops +Pentheus +Periphas +Phineus +Phlegyas +Phoenix +Phoroneus +Phyleus +Pirithooes +Pittheus +Polybus +Polynices +Priam +Proetus +Pylades +Rhadamanthys +Rhesus +Sarpedon +Sisyphus +Sithon +Talaus +Tegyrios +Telamon +Telephus +Temenus +Teucer +Teutamides +Teuthras +Thersander +Thyestes +Tisamenus +Thyndareus + +Amphilochus +Anius +Bakis +Branchus +Calchas +Carnus +Carya +Ennomus +Halitherses +Iamus +Idmon +Manto +Mopsus +Polyeidos +Pythia +telemus +Theocrmenus +Tiresias + +Agea +Aella +Alchibie +Antandre +Antiope +Areto +Bremusa +Eurypyle +Hyppolyta +Hippotoe +Iphito +Lampedo +Marpersia +Melanippe +Molpadia +Myrina +Otrera +Pantariste +Penthesilea +Thalestris + +Danaides +Tantalus diff --git a/plugins/extensions/pykrita/plugin/plugins/comics_project_management_tools/projectGenLists/listB.txt b/plugins/extensions/pykrita/plugin/plugins/comics_project_management_tools/projectGenLists/listB.txt new file mode 100644 index 0000000000..8451f8f224 --- /dev/null +++ b/plugins/extensions/pykrita/plugin/plugins/comics_project_management_tools/projectGenLists/listB.txt @@ -0,0 +1,153 @@ + +Abiu +Acai +Acerola +Ackee +Apple +Ambarella +Apricot +Araza +Arhat +Atemoya +Avocado +Babaco +Bacupari +Bacura +Bael +Banana +Barbadine +Batuan +Blackberry +Blueberry +Bolwarra +Boquia +Bearberry +Ilimbi +Bilberry +BitterMellon +Caimoto +Cherry +Calamondin +Candlenut +Caniste +Chokeberry +Carambola +Cardon +Cashew +Claudberry +Clementone +Citron +Cocona +Cassabanana +Coconut +Coffee +Colanut +Cranberry +Crowberry +Date +DesertFig +DesertLime +Durian +Elderberry +Fig +Grape +Grapefruit +Gooseberry +Gojiberry +Guava +Grandadilla +Rumberry +Honeydew +Huckleberry +Almond +Juniperberry +Kiwi +Kumquat +Kapok +Lime +Lemon +Lychee +Mango +Mangosteen +Mulberry +Muskmelon +Olive +Orange +Papaya +Passionfruit +Pistachio +Plum +Peach +Nectarine +Pear +Pecan +Pomegranate +Pomelo +Pumkin +Pineapple +Rambutan +Rhubarb +Rosehip +Raspberry +Saguaro +Strawberry +Tamarind +Tangerine +Vanilla +Current +Mandarin + +Cucumber +Tomato +Lettuce +Carrot +Potato +Radish +Asparagus +Cocoa +Andive +Chigory +Zuchini +Aubergine +Onion +Paprika +Pepper +Bellpepper +Watermelon +Beetroot +Pea +Sojabean +Bean +Cabagge +Turnip +Corn +Yam +Rucola +Quinoa +Cotton +Amaranth +Millet +Grain +Edamame +Leek + +Basil +Thyme +Rosemary +Chives +Valerian +Parsley +Sage +Fennel +Kardemom +Cinnamon +Saffron +Coriander +Chamomille +Celery +Garlic +Watercress +Gardencress +Chili +Cayene + diff --git a/plugins/extensions/pykrita/plugin/plugins/quick_settings_docker/quick_settings_docker.py b/plugins/extensions/pykrita/plugin/plugins/quick_settings_docker/quick_settings_docker.py index 45ad3de096..6c739d6c80 100644 --- a/plugins/extensions/pykrita/plugin/plugins/quick_settings_docker/quick_settings_docker.py +++ b/plugins/extensions/pykrita/plugin/plugins/quick_settings_docker/quick_settings_docker.py @@ -1,162 +1,164 @@ ''' Description: A Python based docker for quickly choosing the brushsize like similar dockers in other drawing programs. By Wolthera @package quick_settings_docker ''' # Importing the relevant dependancies: import sys from PyQt5.QtGui import * from PyQt5.QtWidgets import * from krita import * class QuickSettingsDocker(DockWidget): # Init the docker def __init__(self): super().__init__() # make base-widget and layout widget = QWidget() layout = QVBoxLayout() widget.setLayout(layout) self.setWindowTitle("Quick Settings Docker") tabWidget = QTabWidget() - self.brushSizeTableView = QTableView() - self.brushSizeTableView.verticalHeader().hide() - self.brushSizeTableView.horizontalHeader().hide() - self.brushSizeTableView.setSelectionMode(QTableView.SingleSelection) - - self.brushOpacityTableView = QTableView() - self.brushOpacityTableView.verticalHeader().hide() - self.brushOpacityTableView.horizontalHeader().hide() - self.brushOpacityTableView.setSelectionMode(QTableView.SingleSelection) - - self.brushFlowTableView = QTableView() - self.brushFlowTableView.verticalHeader().hide() - self.brushFlowTableView.horizontalHeader().hide() - self.brushFlowTableView.setSelectionMode(QTableView.SingleSelection) + self.brushSizeTableView = QListView() + self.brushSizeTableView.setViewMode(QListView.IconMode) + self.brushSizeTableView.setMovement(QListView.Static) + self.brushSizeTableView.setResizeMode(QListView.Adjust) + self.brushSizeTableView.setUniformItemSizes(True) + self.brushSizeTableView.setSelectionMode(QListView.SingleSelection) + + self.brushOpacityTableView = QListView() + self.brushOpacityTableView.setViewMode(QListView.IconMode) + self.brushOpacityTableView.setMovement(QListView.Static) + self.brushOpacityTableView.setResizeMode(QListView.Adjust) + self.brushOpacityTableView.setUniformItemSizes(True) + self.brushOpacityTableView.setSelectionMode(QListView.SingleSelection) + + self.brushFlowTableView = QListView() + self.brushFlowTableView.setViewMode(QListView.IconMode) + self.brushFlowTableView.setMovement(QListView.Static) + self.brushFlowTableView.setResizeMode(QListView.Adjust) + self.brushFlowTableView.setUniformItemSizes(True) + self.brushFlowTableView.setSelectionMode(QListView.SingleSelection) tabWidget.addTab(self.brushSizeTableView, "Size") tabWidget.addTab(self.brushOpacityTableView, "Opacity") tabWidget.addTab(self.brushFlowTableView, "Flow") layout.addWidget(tabWidget) self.setWidget(widget) # Add the widget to the docker. # amount of columns in each row for all the tables. - self.columns = 4 # We want a grid with possible options to select. - # To do this, we'll make a TableView widget and use a standarditemmodel for the entries. + # To do this, we'll make a ListView widget and use a standarditemmodel for the entries. # The entries are filled out based on the sizes and opacity lists. # Sizes and opacity lists. The former is half-way copied from ptsai, the latter is based on personal experience of useful opacities. self.sizesList = [0.7, 1.0, 1.5, 2, 2.5, 3, 3.5, 4, 5, 6, 7, 8, 9, 10, 12, 14, 16, 20, 25, 30, 35, 40, 50, 60, 70, 80, 100, 120, 160, 200, 250, 300, 350, 400, 450, 500] self.opacityList = [0.1, 0.5, 1, 5, 10, 15, 20, 30, 40, 50, 60, 70, 80, 90, 100] - self.brushSizeModel = QStandardItemModel((len(self.sizesList) / self.columns) + 1, self.columns) - self.brushOpacityModel = QStandardItemModel((len(self.opacityList) / self.columns) + 1, self.columns) - self.brushFlowModel = QStandardItemModel((len(self.opacityList) / self.columns) + 1, self.columns) + self.brushSizeModel = QStandardItemModel() + self.brushOpacityModel = QStandardItemModel() + self.brushFlowModel = QStandardItemModel() self.fillSizesModel() self.fillOpacityModel() # Now we're done filling out our tables, we connect the views to the functions that'll change the settings. self.brushSizeTableView.clicked.connect(self.setBrushSize) self.brushOpacityTableView.clicked.connect(self.setBrushOpacity) self.brushFlowTableView.clicked.connect(self.setBrushFlow) def fillSizesModel(self): # First we empty the old model. We might wanna use this function in the future to fill it with the brushmask of the selected brush, but there's currently no API for recognising changes in the current brush nor is there a way to get its brushmask. self.brushSizeModel.clear() for s in range(len(self.sizesList)): # we're gonna itterate over our list, and make a new item for each entry. # We need to disable a bunch of stuff to make sure people won't do funny things to our entries. item = QStandardItem() item.setCheckable(False) item.setEditable(False) item.setDragEnabled(False) - item.setText(str(self.sizesList[s])) + item.setText(str(self.sizesList[s])+" px") # And from here on we'll make an icon. - brushImage = QPixmap(32, 32) - img = brushImage.toImage() + brushImage = QPixmap(64, 64) + img = QImage(64, 64, QImage.Format_RGBA8888) circlePainter = QPainter() - img.fill(self.brushSizeTableView.palette().color(QPalette.Base)) + img.fill(Qt.transparent) circlePainter.begin(img) brush = QBrush(Qt.SolidPattern) brush.setColor(self.brushSizeTableView.palette().color(QPalette.Text)) circlePainter.setBrush(brush) circlePainter.pen().setWidth(0) - brushSize = max(min((self.sizesList[s] / 500) * 100, 32), 1) + brushSize = max(min((self.sizesList[s] / 500) * 100, 64), 1) brushSize = brushSize * 0.5 - circlePainter.drawEllipse(QPointF(16, 16), brushSize, brushSize) + circlePainter.drawEllipse(QPointF(32, 32), brushSize, brushSize) circlePainter.end() brushImage = QPixmap.fromImage(img) # now we're done with drawing the icon, so we set it on the item. item.setIcon(QIcon(brushImage)) - self.brushSizeModel.setItem(s / 4, s % 4, item) + self.brushSizeModel.appendRow(item) self.brushSizeTableView.setModel(self.brushSizeModel) - self.brushSizeTableView.resizeColumnsToContents() def fillOpacityModel(self): self.brushOpacityModel.clear() self.brushFlowModel.clear() for s in range(len(self.opacityList)): # we're gonna itterate over our list, and make a new item for each entry. item = QStandardItem() item.setCheckable(False) item.setEditable(False) item.setDragEnabled(False) - item.setText(str(self.opacityList[s])) - brushImage = QPixmap(32, 32) - img = brushImage.toImage() + item.setText(str(self.opacityList[s])+" %") + brushImage = QPixmap(64, 64) + img = QImage(64, 64, QImage.Format_RGBA8888) circlePainter = QPainter() - img.fill(self.brushSizeTableView.palette().color(QPalette.Base)) + img.fill(Qt.transparent) circlePainter.begin(img) brush = QBrush(Qt.SolidPattern) brush.setColor(self.brushSizeTableView.palette().color(QPalette.Text)) circlePainter.setBrush(brush) circlePainter.pen().setWidth(0) circlePainter.setOpacity(self.opacityList[s] / 100) - circlePainter.drawEllipse(QPointF(16, 16), 16, 16) + circlePainter.drawEllipse(QPointF(32, 32), 32, 32) circlePainter.end() brushImage = QPixmap.fromImage(img) item.setIcon(QIcon(brushImage)) # the flow and opacity models will use virtually the same items, but Qt would like us to make sure we understand # these are not really the same items, so hence the clone. itemFlow = item.clone() - self.brushOpacityModel.setItem(s / 4, s % 4, item) - self.brushFlowModel.setItem(s / 4, s % 4, itemFlow) + self.brushOpacityModel.appendRow(item) + self.brushFlowModel.appendRow(itemFlow) self.brushOpacityTableView.setModel(self.brushOpacityModel) self.brushFlowTableView.setModel(self.brushFlowModel) - self.brushFlowTableView.resizeColumnsToContents() - self.brushOpacityTableView.resizeColumnsToContents() def canvasChanged(self, canvas): pass @pyqtSlot('QModelIndex') def setBrushSize(self, index): - i = index.column() + (index.row() * self.columns) + i = index.row() brushSize = self.sizesList[i] if Application.activeWindow() and len(Application.activeWindow().views()) > 0: Application.activeWindow().views()[0].setBrushSize(brushSize) @pyqtSlot('QModelIndex') def setBrushOpacity(self, index): - i = index.column() + (index.row() * self.columns) + i = index.row() brushOpacity = self.opacityList[i] / 100 if Application.activeWindow() and len(Application.activeWindow().views()) > 0: Application.activeWindow().views()[0].setPaintingOpacity(brushOpacity) @pyqtSlot('QModelIndex') def setBrushFlow(self, index): - i = index.column() + (index.row() * self.columns) + i = index.row() brushOpacity = self.opacityList[i] / 100 if Application.activeWindow() and len(Application.activeWindow().views()) > 0: Application.activeWindow().views()[0].setPaintingFlow(brushOpacity) # Add docker to the application :) Application.addDockWidgetFactory(DockWidgetFactory("quick_settings_docker", DockWidgetFactoryBase.DockRight, QuickSettingsDocker)) diff --git a/plugins/extensions/qmic/QMic.cpp b/plugins/extensions/qmic/QMic.cpp index e20f8d840d..c214a151b3 100644 --- a/plugins/extensions/qmic/QMic.cpp +++ b/plugins/extensions/qmic/QMic.cpp @@ -1,500 +1,508 @@ /* * Copyright (c) 2017 Boudewijn Rempt * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "QMic.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include +#include #include #include #include #include #include #include #include #include #include #include #include "kis_input_output_mapper.h" #include "kis_qmic_simple_convertor.h" #include "kis_import_qmic_processing_visitor.h" #include #include "kis_qmic_applicator.h" #include "kis_qmic_progress_manager.h" static const char ack[] = "ack"; K_PLUGIN_FACTORY_WITH_JSON(QMicFactory, "kritaqmic.json", registerPlugin();) QMic::QMic(QObject *parent, const QVariantList &) : KisViewPlugin(parent) , m_gmicApplicator(0) , m_progressManager(0) { #ifndef Q_OS_MAC KisPreferenceSetRegistry *preferenceSetRegistry = KisPreferenceSetRegistry::instance(); PluginSettingsFactory* settingsFactory = new PluginSettingsFactory(); preferenceSetRegistry->add("QMicPluginSettingsFactory", settingsFactory); m_qmicAction = createAction("QMic"); m_qmicAction->setActivationFlags(KisAction::ACTIVE_DEVICE); connect(m_qmicAction , SIGNAL(triggered()), this, SLOT(slotQMic())); m_againAction = createAction("QMicAgain"); m_againAction->setActivationFlags(KisAction::ACTIVE_DEVICE); m_againAction->setEnabled(false); connect(m_againAction, SIGNAL(triggered()), this, SLOT(slotQMicAgain())); m_gmicApplicator = new KisQmicApplicator(); connect(m_gmicApplicator, SIGNAL(gmicFinished(bool, int, QString)), this, SLOT(slotGmicFinished(bool, int, QString))); #endif } QMic::~QMic() { Q_FOREACH(QSharedMemory *memorySegment, m_sharedMemorySegments) { qDebug() << "detaching" << memorySegment->key(); memorySegment->detach(); } qDeleteAll(m_sharedMemorySegments); m_sharedMemorySegments.clear(); if (m_pluginProcess) { m_pluginProcess->close(); } delete m_gmicApplicator; delete m_progressManager; delete m_localServer; } void QMic::slotQMicAgain() { slotQMic(true); } void QMic::slotQMic(bool again) { m_qmicAction->setEnabled(false); m_againAction->setEnabled(false); if (m_pluginProcess) { qDebug() << "Plugin is already started" << m_pluginProcess->state(); return; } delete m_progressManager; m_progressManager = new KisQmicProgressManager(m_view); connect(m_progressManager, SIGNAL(sigProgress()), this, SLOT(slotUpdateProgress())); // find the krita-gmic-qt plugin QString pluginPath = PluginSettings::gmicQtPath(); if (pluginPath.isEmpty() || !QFileInfo(pluginPath).exists()) { { KoDialog dlg; dlg.setWindowTitle(i18nc("@title:Window", "Krita")); QWidget *w = new QWidget(&dlg); dlg.setMainWidget(w); QVBoxLayout *l = new QVBoxLayout(w); l->addWidget(new PluginSettings(w)); dlg.setButtons(KoDialog::Ok); dlg.exec(); } pluginPath = PluginSettings::gmicQtPath(); if (pluginPath.isEmpty() || !QFileInfo(pluginPath).exists()) { m_qmicAction->setEnabled(true); m_againAction->setEnabled(true); return; } } m_key = QUuid::createUuid().toString(); m_localServer = new QLocalServer(); m_localServer->listen(m_key); connect(m_localServer, SIGNAL(newConnection()), SLOT(connected())); m_pluginProcess = new QProcess(this); m_pluginProcess->setProcessChannelMode(QProcess::ForwardedChannels); connect(m_pluginProcess, SIGNAL(finished(int, QProcess::ExitStatus)), this, SLOT(pluginFinished(int,QProcess::ExitStatus))); connect(m_pluginProcess, SIGNAL(stateChanged(QProcess::ProcessState)), this, SLOT(pluginStateChanged(QProcess::ProcessState))); m_pluginProcess->start(pluginPath, QStringList() << m_key << (again ? QString(" reapply") : QString::null)); bool r = m_pluginProcess->waitForStarted(); while (m_pluginProcess->waitForFinished(10)) { qApp->processEvents(QEventLoop::ExcludeUserInputEvents); } qDebug() << "Plugin started" << r << m_pluginProcess->state(); } void QMic::connected() { qDebug() << "connected"; QLocalSocket *socket = m_localServer->nextPendingConnection(); if (!socket) { return; } while (socket->bytesAvailable() < static_cast(sizeof(quint32))) { if (!socket->isValid()) { // stale request return; } socket->waitForReadyRead(1000); } QDataStream ds(socket); QByteArray msg; quint32 remaining; ds >> remaining; msg.resize(remaining); int got = 0; char* uMsgBuf = msg.data(); // FIXME: Should use read transaction for Qt >= 5.7: // https://doc.qt.io/qt-5/qdatastream.html#using-read-transactions do { got = ds.readRawData(uMsgBuf, remaining); remaining -= got; uMsgBuf += got; } while (remaining && got >= 0 && socket->waitForReadyRead(2000)); if (got < 0) { qWarning() << "Message reception failed" << socket->errorString(); delete socket; m_localServer->close(); delete m_localServer; m_localServer = 0; return; } QString message = QString::fromUtf8(msg); qDebug() << "Received" << message; // Check the message: we can get three different ones QMultiMap messageMap; Q_FOREACH(QString line, message.split('\n', QString::SkipEmptyParts)) { QList kv = line.split('=', QString::SkipEmptyParts); if (kv.size() == 2) { messageMap.insert(kv[0], kv[1]); } else { qWarning() << "line" << line << "is invalid."; } } if (!messageMap.contains("command")) { qWarning() << "Message did not contain a command"; return; } int mode = 0; if (messageMap.contains("mode")) { mode = messageMap.values("mode").first().toInt(); } QByteArray ba; + QString messageBoxWarningText; if (messageMap.values("command").first() == "gmic_qt_get_image_size") { KisSelectionSP selection = m_view->image()->globalSelection(); if (selection) { QRect selectionRect = selection->selectedExactRect(); ba = QByteArray::number(selectionRect.width()) + "," + QByteArray::number(selectionRect.height()); } else { ba = QByteArray::number(m_view->image()->width()) + "," + QByteArray::number(m_view->image()->height()); } } else if (messageMap.values("command").first() == "gmic_qt_get_cropped_images") { // Parse the message, create the shared memory segments, and create a new message to send back and waid for ack QRectF cropRect(0.0, 0.0, 1.0, 1.0); if (!messageMap.contains("croprect") || messageMap.values("croprect").first().split(',', QString::SkipEmptyParts).size() != 4) { qWarning() << "gmic-qt didn't send a croprect or not a valid croprect"; } else { QStringList cr = messageMap.values("croprect").first().split(',', QString::SkipEmptyParts); cropRect.setX(cr[0].toFloat()); cropRect.setY(cr[1].toFloat()); cropRect.setWidth(cr[2].toFloat()); cropRect.setHeight(cr[3].toFloat()); } if (!prepareCroppedImages(&ba, cropRect, mode)) { qWarning() << "Failed to prepare images for gmic-qt"; } } else if (messageMap.values("command").first() == "gmic_qt_output_images") { // Parse the message. read the shared memory segments, fix up the current image and send an ack qDebug() << "gmic_qt_output_images"; QStringList layers = messageMap.values("layer"); m_outputMode = (OutputMode)mode; if (m_outputMode != IN_PLACE) { - QMessageBox::warning(0, i18nc("@title:window", "Krita"), i18n("Sorry, this output mode is not implemented yet.")); + messageBoxWarningText = i18n("Sorry, this output mode is not implemented yet."); m_outputMode = IN_PLACE; } slotStartApplicator(layers); } else if (messageMap.values("command").first() == "gmic_qt_detach") { Q_FOREACH(QSharedMemory *memorySegment, m_sharedMemorySegments) { qDebug() << "detaching" << memorySegment->key() << memorySegment->isAttached(); if (memorySegment->isAttached()) { if (!memorySegment->detach()) { qDebug() << "\t" << memorySegment->error() << memorySegment->errorString(); } } } qDeleteAll(m_sharedMemorySegments); m_sharedMemorySegments.clear(); } else { qWarning() << "Received unknown command" << messageMap.values("command"); } qDebug() << "Sending" << QString::fromUtf8(ba); // HACK: Make sure QDataStream does not refuse to write! // Proper fix: Change the above read to use read transaction ds.resetStatus(); ds.writeBytes(ba.constData(), ba.length()); // Flush the socket because we might not return to the event loop! if (!socket->waitForBytesWritten(2000)) { qWarning() << "Failed to write response:" << socket->error(); } // Wait for the ack bool r = true; r &= socket->waitForReadyRead(2000); // wait for ack r &= (socket->read(qstrlen(ack)) == ack); if (!socket->waitForDisconnected(2000)) { qWarning() << "Remote not disconnected:" << socket->error(); // Wait again socket->disconnectFromServer(); if (socket->waitForDisconnected(2000)) { qWarning() << "Disconnect timed out:" << socket->error(); } } + if (!messageBoxWarningText.isEmpty()) { + // Defer the message box to the event loop + QTimer::singleShot(0, [messageBoxWarningText]() { + QMessageBox::warning(KisPart::instance()->currentMainwindow(), i18nc("@title:window", "Krita"), messageBoxWarningText); + }); + } } void QMic::pluginStateChanged(QProcess::ProcessState state) { qDebug() << "stateChanged" << state; } void QMic::pluginFinished(int exitCode, QProcess::ExitStatus exitStatus) { qDebug() << "pluginFinished" << exitCode << exitStatus; delete m_pluginProcess; m_pluginProcess = 0; delete m_localServer; m_localServer = 0; delete m_progressManager; m_progressManager = 0; m_qmicAction->setEnabled(true); m_againAction->setEnabled(true); } void QMic::slotUpdateProgress() { if (!m_gmicApplicator) { qWarning() << "G'Mic applicator already deleted!"; return; } qDebug() << "slotUpdateProgress" << m_gmicApplicator->getProgress(); m_progressManager->updateProgress(m_gmicApplicator->getProgress()); } void QMic::slotStartProgressReporting() { qDebug() << "slotStartProgressReporting();"; if (m_progressManager->inProgress()) { m_progressManager->finishProgress(); } m_progressManager->initProgress(); } void QMic::slotGmicFinished(bool successfully, int milliseconds, const QString &msg) { qDebug() << "slotGmicFinished();" << successfully << milliseconds << msg; if (successfully) { m_gmicApplicator->finish(); } else { m_gmicApplicator->cancel(); QMessageBox::warning(0, i18nc("@title:window", "Krita"), i18n("G'Mic failed, reason:") + msg); } } void QMic::slotStartApplicator(QStringList gmicImages) { qDebug() << "slotStartApplicator();" << gmicImages; // Create a vector of gmic images QVector *> images; Q_FOREACH(const QString &image, gmicImages) { QStringList parts = image.split(',', QString::SkipEmptyParts); Q_ASSERT(parts.size() == 4); QString key = parts[0]; QString layerName = QByteArray::fromHex(parts[1].toLatin1()); int spectrum = parts[2].toInt(); int width = parts[3].toInt(); int height = parts[4].toInt(); qDebug() << key << layerName << width << height; QSharedMemory m(key); if (!m.attach(QSharedMemory::ReadOnly)) { qWarning() << "Could not attach to shared memory area." << m.error() << m.errorString(); } if (m.isAttached()) { if (!m.lock()) { qDebug() << "Could not lock memeory segment" << m.error() << m.errorString(); } qDebug() << "Memory segment" << key << m.size() << m.constData() << m.data(); gmic_image *gimg = new gmic_image(); gimg->assign(width, height, 1, spectrum); gimg->name = layerName; gimg->_data = new float[width * height * spectrum * sizeof(float)]; qDebug() << "width" << width << "height" << height << "size" << width * height * spectrum * sizeof(float) << "shared memory size" << m.size(); memcpy(gimg->_data, m.constData(), width * height * spectrum * sizeof(float)); qDebug() << "created gmic image" << gimg->name << gimg->_width << gimg->_height; if (!m.unlock()) { qDebug() << "Could not unlock memeory segment" << m.error() << m.errorString(); } if (!m.detach()) { qDebug() << "Could not detach from memeory segment" << m.error() << m.errorString(); } images.append(gimg); } } qDebug() << "Got" << images.size() << "gmic images"; // Start the applicator KUndo2MagicString actionName = kundo2_i18n("Gmic filter"); KisNodeSP rootNode = m_view->image()->root(); KisInputOutputMapper mapper(m_view->image(), m_view->activeNode()); KisNodeListSP layers = mapper.inputNodes(m_inputMode); m_gmicApplicator->setProperties(m_view->image(), rootNode, images, actionName, layers); m_gmicApplicator->preview(); m_gmicApplicator->finish(); } bool QMic::prepareCroppedImages(QByteArray *message, QRectF &rc, int inputMode) { m_view->image()->lock(); m_inputMode = (InputLayerMode)inputMode; qDebug() << "prepareCroppedImages()" << QString::fromUtf8(*message) << rc << inputMode; KisInputOutputMapper mapper(m_view->image(), m_view->activeNode()); KisNodeListSP nodes = mapper.inputNodes(m_inputMode); if (nodes->isEmpty()) { m_view->image()->unlock(); return false; } for (int i = 0; i < nodes->size(); ++i) { KisNodeSP node = nodes->at(i); - if (node->paintDevice()) { + if (node && node->paintDevice()) { QRect cropRect; KisSelectionSP selection = m_view->image()->globalSelection(); if (selection) { cropRect = selection->selectedExactRect(); } else { cropRect = m_view->image()->bounds(); } qDebug() << "Converting node" << node->name() << cropRect; const QRectF mappedRect = KisAlgebra2D::mapToRect(cropRect).mapRect(rc); const QRect resultRect = mappedRect.toAlignedRect(); QSharedMemory *m = new QSharedMemory(QString("key_%1").arg(QUuid::createUuid().toString())); m_sharedMemorySegments.append(m); if (!m->create(resultRect.width() * resultRect.height() * 4 * sizeof(float))) { //buf.size())) { qWarning() << "Could not create shared memory segment" << m->error() << m->errorString(); return false; } m->lock(); gmic_image img; img.assign(resultRect.width(), resultRect.height(), 1, 4); img._data = reinterpret_cast(m->data()); KisQmicSimpleConvertor::convertToGmicImageFast(node->paintDevice(), &img, resultRect); message->append(m->key().toUtf8()); m->unlock(); message->append(","); message->append(node->name().toUtf8().toHex()); message->append(","); message->append(QByteArray::number(resultRect.width())); message->append(","); message->append(QByteArray::number(resultRect.height())); message->append("\n"); } } qDebug() << QString::fromUtf8(*message); m_view->image()->unlock(); return true; } #include "QMic.moc"