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/templates/animation/.directory b/krita/data/templates/animation/.directory index 41c05f1fe8..2a56c5142c 100644 --- a/krita/data/templates/animation/.directory +++ b/krita/data/templates/animation/.directory @@ -1,19 +1,20 @@ [Desktop Entry] Name=Animation Templates Name[ca]=Plantilles d'animació Name[ca@valencia]=Plantilles d'animació Name[cs]=Šablony animací: Name[de]=Animations-Vorlagen Name[en_GB]=Animation Templates Name[es]=Plantillas de animación Name[gl]=Modelos de animación Name[it]=Modelli di animazioni Name[nl]=Animatiesjablonen Name[pl]=Szablony animacji Name[pt]=Modelos de Animações Name[pt_BR]=Modelos de animação Name[sv]=Animeringsmallar Name[tr]=Canlandırma Şablonları Name[uk]=Шаблони анімацій Name[x-test]=xxAnimation Templatesxx +Name[zh_CN]=动画模板 X-KDE-DefaultTab=true 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/actions/kis_selection_action_factories.cpp b/libs/ui/actions/kis_selection_action_factories.cpp index e11d6124cb..fed059d12a 100644 --- a/libs/ui/actions/kis_selection_action_factories.cpp +++ b/libs/ui/actions/kis_selection_action_factories.cpp @@ -1,579 +1,580 @@ /* * Copyright (c) 2012 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_selection_action_factories.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "KisViewManager.h" #include "kis_canvas_resource_provider.h" #include "kis_clipboard.h" #include "kis_pixel_selection.h" #include "kis_paint_layer.h" #include "kis_image.h" #include "kis_image_barrier_locker.h" #include "kis_fill_painter.h" #include "kis_transaction.h" #include "kis_iterator_ng.h" #include "kis_processing_applicator.h" #include "kis_group_layer.h" #include "commands/kis_selection_commands.h" #include "commands/kis_image_layer_add_command.h" #include "kis_tool_proxy.h" #include "kis_canvas2.h" #include "kis_canvas_controller.h" #include "kis_selection_manager.h" #include "kis_transaction_based_command.h" #include "kis_selection_filters.h" #include "kis_shape_selection.h" #include "KisPart.h" #include "kis_shape_layer.h" #include #include #include #include "kis_canvas_resource_provider.h" #include "kis_figure_painting_tool_helper.h" namespace ActionHelper { void copyFromDevice(KisViewManager *view, KisPaintDeviceSP device, bool makeSharpClip = false) { KisImageWSP image = view->image(); if (!image) return; KisSelectionSP selection = view->selection(); QRect rc = (selection) ? selection->selectedExactRect() : image->bounds(); KisPaintDeviceSP clip = new KisPaintDevice(device->colorSpace()); Q_CHECK_PTR(clip); const KoColorSpace *cs = clip->colorSpace(); // TODO if the source is linked... copy from all linked layers?!? // Copy image data KisPainter::copyAreaOptimized(QPoint(), device, clip, rc); if (selection) { // Apply selection mask. KisPaintDeviceSP selectionProjection = selection->projection(); KisHLineIteratorSP layerIt = clip->createHLineIteratorNG(0, 0, rc.width()); KisHLineConstIteratorSP selectionIt = selectionProjection->createHLineIteratorNG(rc.x(), rc.y(), rc.width()); const KoColorSpace *selCs = selection->projection()->colorSpace(); for (qint32 y = 0; y < rc.height(); y++) { for (qint32 x = 0; x < rc.width(); x++) { /** * Sharp method is an exact reverse of COMPOSITE_OVER * so if you cover the cut/copied piece over its source * you get an exactly the same image without any seams */ if (makeSharpClip) { qreal dstAlpha = cs->opacityF(layerIt->rawData()); qreal sel = selCs->opacityF(selectionIt->oldRawData()); qreal newAlpha = sel * dstAlpha / (1.0 - dstAlpha + sel * dstAlpha); float mask = newAlpha / dstAlpha; cs->applyAlphaNormedFloatMask(layerIt->rawData(), &mask, 1); } else { cs->applyAlphaU8Mask(layerIt->rawData(), selectionIt->oldRawData(), 1); } layerIt->nextPixel(); selectionIt->nextPixel(); } layerIt->nextRow(); selectionIt->nextRow(); } } KisClipboard::instance()->setClip(clip, rc.topLeft()); } } void KisSelectAllActionFactory::run(KisViewManager *view) { KisImageWSP image = view->image(); if (!image) return; KisProcessingApplicator *ap = beginAction(view, kundo2_i18n("Select All")); if (!image->globalSelection()) { ap->applyCommand(new KisSetEmptyGlobalSelectionCommand(image), KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::EXCLUSIVE); } struct SelectAll : public KisTransactionBasedCommand { SelectAll(KisImageSP image) : m_image(image) {} KisImageSP m_image; KUndo2Command* paint() override { KisSelectionSP selection = m_image->globalSelection(); KisSelectionTransaction transaction(selection->pixelSelection()); + selection->pixelSelection()->clear(); selection->pixelSelection()->select(m_image->bounds()); return transaction.endAndTake(); } }; ap->applyCommand(new SelectAll(image), KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::EXCLUSIVE); endAction(ap, KisOperationConfiguration(id()).toXML()); } void KisDeselectActionFactory::run(KisViewManager *view) { KisImageWSP image = view->image(); if (!image) return; KUndo2Command *cmd = new KisDeselectGlobalSelectionCommand(image); KisProcessingApplicator *ap = beginAction(view, cmd->text()); ap->applyCommand(cmd, KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::EXCLUSIVE); endAction(ap, KisOperationConfiguration(id()).toXML()); } void KisReselectActionFactory::run(KisViewManager *view) { KisImageWSP image = view->image(); if (!image) return; KUndo2Command *cmd = new KisReselectGlobalSelectionCommand(image); KisProcessingApplicator *ap = beginAction(view, cmd->text()); ap->applyCommand(cmd, KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::EXCLUSIVE); endAction(ap, KisOperationConfiguration(id()).toXML()); } void KisFillActionFactory::run(const QString &fillSource, KisViewManager *view) { KisNodeSP node = view->activeNode(); if (!node || !node->hasEditablePaintDevice()) return; KisSelectionSP selection = view->selection(); QRect selectedRect = selection ? selection->selectedRect() : view->image()->bounds(); Q_UNUSED(selectedRect); KisPaintDeviceSP filled = node->paintDevice()->createCompositionSourceDevice(); Q_UNUSED(filled); bool usePattern = false; bool useBgColor = false; if (fillSource.contains("pattern")) { usePattern = true; } else if (fillSource.contains("bg")) { useBgColor = true; } KisProcessingApplicator applicator(view->image(), node, KisProcessingApplicator::NONE, KisImageSignalVector() << ModifiedSignal, kundo2_i18n("Flood Fill Layer")); KisResourcesSnapshotSP resources = new KisResourcesSnapshot(view->image(), node, view->resourceProvider()->resourceManager()); if (!fillSource.contains("opacity")) { resources->setOpacity(1.0); } KisProcessingVisitorSP visitor = new FillProcessingVisitor(QPoint(0, 0), // start position selection, resources, false, // fast mode usePattern, true, // fill only selection, 0, // feathering radius 0, // sizemod 80, // threshold, false, // unmerged useBgColor); applicator.applyVisitor(visitor, KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::EXCLUSIVE); applicator.end(); } void KisClearActionFactory::run(KisViewManager *view) { // XXX: "Add saving of XML data for Clear action" view->canvasBase()->toolProxy()->deleteSelection(); } void KisImageResizeToSelectionActionFactory::run(KisViewManager *view) { // XXX: "Add saving of XML data for Image Resize To Selection action" KisSelectionSP selection = view->selection(); if (!selection) return; view->image()->cropImage(selection->selectedExactRect()); } void KisCutCopyActionFactory::run(bool willCut, bool makeSharpClip, KisViewManager *view) { KisImageSP image = view->image(); if (!image) return; bool haveShapesSelected = view->selectionManager()->haveShapesSelected(); if (haveShapesSelected) { // XXX: "Add saving of XML data for Cut/Copy of shapes" KisImageBarrierLocker locker(image); if (willCut) { view->canvasBase()->toolProxy()->cut(); } else { view->canvasBase()->toolProxy()->copy(); } } else { KisNodeSP node = view->activeNode(); if (!node) return; KisSelectionSP selection = view->selection(); if (selection.isNull()) return; { KisImageBarrierLocker locker(image); KisPaintDeviceSP dev = node->paintDevice(); if (!dev) { dev = node->projection(); } if (!dev) { view->showFloatingMessage( i18nc("floating message when cannot copy from a node", "Cannot copy pixels from this type of layer "), QIcon(), 3000, KisFloatingMessage::Medium); return; } if (dev->exactBounds().isEmpty()) { view->showFloatingMessage( i18nc("floating message when copying empty selection", "Selection is empty: no pixels were copied "), QIcon(), 3000, KisFloatingMessage::Medium); return; } ActionHelper::copyFromDevice(view, dev, makeSharpClip); } if (willCut) { KUndo2Command *command = 0; if (node->hasEditablePaintDevice()) { struct ClearSelection : public KisTransactionBasedCommand { ClearSelection(KisNodeSP node, KisSelectionSP sel) : m_node(node), m_sel(sel) {} KisNodeSP m_node; KisSelectionSP m_sel; KUndo2Command* paint() override { KisSelectionSP cutSelection = m_sel; // Shrinking the cutting area was previously used // for getting seamless cut-paste. Now we use makeSharpClip // instead. // QRect originalRect = cutSelection->selectedExactRect(); // static const int preciseSelectionThreshold = 16; // // if (originalRect.width() > preciseSelectionThreshold || // originalRect.height() > preciseSelectionThreshold) { // cutSelection = new KisSelection(*m_sel); // delete cutSelection->flatten(); // // KisSelectionFilter* filter = new KisShrinkSelectionFilter(1, 1, false); // // QRect processingRect = filter->changeRect(originalRect); // filter->process(cutSelection->pixelSelection(), processingRect); // } KisTransaction transaction(m_node->paintDevice()); m_node->paintDevice()->clearSelection(cutSelection); m_node->setDirty(cutSelection->selectedRect()); return transaction.endAndTake(); } }; command = new ClearSelection(node, selection); } KUndo2MagicString actionName = willCut ? kundo2_i18n("Cut") : kundo2_i18n("Copy"); KisProcessingApplicator *ap = beginAction(view, actionName); if (command) { ap->applyCommand(command, KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::NORMAL); } KisOperationConfiguration config(id()); config.setProperty("will-cut", willCut); endAction(ap, config.toXML()); } } } void KisCopyMergedActionFactory::run(KisViewManager *view) { KisImageWSP image = view->image(); if (!image) return; if (!view->blockUntilOperationsFinished(image)) return; image->barrierLock(); KisPaintDeviceSP dev = image->root()->projection(); ActionHelper::copyFromDevice(view, dev); image->unlock(); KisProcessingApplicator *ap = beginAction(view, kundo2_i18n("Copy Merged")); endAction(ap, KisOperationConfiguration(id()).toXML()); } void KisPasteNewActionFactory::run(KisViewManager *viewManager) { Q_UNUSED(viewManager); KisPaintDeviceSP clip = KisClipboard::instance()->clip(QRect(), true); if (!clip) return; QRect rect = clip->exactBounds(); if (rect.isEmpty()) return; KisDocument *doc = KisPart::instance()->createDocument(); KisImageSP image = new KisImage(doc->createUndoStore(), rect.width(), rect.height(), clip->colorSpace(), i18n("Pasted")); KisPaintLayerSP layer = new KisPaintLayer(image.data(), image->nextLayerName() + i18n("(pasted)"), OPACITY_OPAQUE_U8, clip->colorSpace()); KisPainter::copyAreaOptimized(QPoint(), clip, layer->paintDevice(), rect); image->addNode(layer.data(), image->rootLayer()); doc->setCurrentImage(image); KisPart::instance()->addDocument(doc); KisMainWindow *win = viewManager->mainWindow(); win->addViewAndNotifyLoadingCompleted(doc); } void KisInvertSelectionOperation::runFromXML(KisViewManager* view, const KisOperationConfiguration& config) { KisSelectionFilter* filter = new KisInvertSelectionFilter(); runFilter(filter, view, config); } void KisSelectionToVectorActionFactory::run(KisViewManager *view) { KisSelectionSP selection = view->selection(); if (selection->hasShapeSelection() || !selection->outlineCacheValid()) { return; } QPainterPath selectionOutline = selection->outlineCache(); QTransform transform = view->canvasBase()->coordinatesConverter()->imageToDocumentTransform(); KoShape *shape = KoPathShape::createShapeFromPainterPath(transform.map(selectionOutline)); shape->setShapeId(KoPathShapeId); /** * Mark a shape that it belongs to a shape selection */ if(!shape->userData()) { shape->setUserData(new KisShapeSelectionMarker); } KisProcessingApplicator *ap = beginAction(view, kundo2_i18n("Convert to Vector Selection")); ap->applyCommand(view->canvasBase()->shapeController()->addShape(shape), KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::EXCLUSIVE); endAction(ap, KisOperationConfiguration(id()).toXML()); } void KisShapesToVectorSelectionActionFactory::run(KisViewManager* view) { const QList originalShapes = view->canvasBase()->shapeManager()->selection()->selectedShapes(); QList clonedShapes; Q_FOREACH (KoShape *shape, originalShapes) { clonedShapes << shape->cloneShape(); } KisSelectionToolHelper helper(view->canvasBase(), kundo2_i18n("Convert shapes to vector selection")); helper.addSelectionShapes(clonedShapes); } void KisSelectionToShapeActionFactory::run(KisViewManager *view) { KisSelectionSP selection = view->selection(); if (!selection->outlineCacheValid()) { return; } QPainterPath selectionOutline = selection->outlineCache(); QTransform transform = view->canvasBase()->coordinatesConverter()->imageToDocumentTransform(); KoShape *shape = KoPathShape::createShapeFromPainterPath(transform.map(selectionOutline)); shape->setShapeId(KoPathShapeId); KoColor fgColor = view->canvasBase()->resourceManager()->resource(KoCanvasResourceManager::ForegroundColor).value(); KoShapeStrokeSP border(new KoShapeStroke(1.0, fgColor.toQColor())); shape->setStroke(border); view->document()->shapeController()->addShape(shape); } void KisStrokeSelectionActionFactory::run(KisViewManager *view, StrokeSelectionOptions params) { KisImageWSP image = view->image(); if (!image) { return; } KisSelectionSP selection = view->selection(); if (!selection) { return; } int size = params.lineSize; KisPixelSelectionSP pixelSelection = selection->projection(); if (!pixelSelection->outlineCacheValid()) { pixelSelection->recalculateOutlineCache(); } QPainterPath outline = pixelSelection->outlineCache(); QColor color = params.color.toQColor(); KisNodeSP currentNode = view->resourceProvider()->resourceManager()->resource(KisCanvasResourceProvider::CurrentKritaNode).value(); if (!currentNode->inherits("KisShapeLayer") && currentNode->childCount() == 0) { KoCanvasResourceManager * rManager = view->resourceProvider()->resourceManager(); KisPainter::StrokeStyle strokeStyle = KisPainter::StrokeStyleBrush; KisPainter::FillStyle fillStyle = params.fillStyle(); KisFigurePaintingToolHelper helper(kundo2_i18n("Draw Polyline"), image, currentNode, rManager , strokeStyle, fillStyle); helper.setFGColorOverride(params.color); helper.setSelectionOverride(0); QPen pen(Qt::red, size); pen.setJoinStyle(Qt::RoundJoin); if (fillStyle != KisPainter::FillStyleNone) { helper.paintPainterPathQPenFill(outline, pen, params.fillColor); } else { helper.paintPainterPathQPen(outline, pen, params.fillColor); } } else { QTransform transform = view->canvasBase()->coordinatesConverter()->imageToDocumentTransform(); KoShape *shape = KoPathShape::createShapeFromPainterPath(transform.map(outline)); shape->setShapeId(KoPathShapeId); KoShapeStrokeSP border(new KoShapeStroke(size, color)); shape->setStroke(border); view->document()->shapeController()->addShape(shape); } image->setModified(); } void KisStrokeBrushSelectionActionFactory::run(KisViewManager *view, StrokeSelectionOptions params) { KisImageWSP image = view->image(); if (!image) { return; } KisSelectionSP selection = view->selection(); if (!selection) { return; } KisPixelSelectionSP pixelSelection = selection->projection(); if (!pixelSelection->outlineCacheValid()) { pixelSelection->recalculateOutlineCache(); } KisNodeSP currentNode = view->resourceProvider()->resourceManager()->resource(KisCanvasResourceProvider::CurrentKritaNode).value(); if (!currentNode->inherits("KisShapeLayer") && currentNode->childCount() == 0) { KoCanvasResourceManager * rManager = view->resourceProvider()->resourceManager(); QPainterPath outline = pixelSelection->outlineCache(); KisPainter::StrokeStyle strokeStyle = KisPainter::StrokeStyleBrush; KisPainter::FillStyle fillStyle = KisPainter::FillStyleNone; KoColor color = params.color; KisFigurePaintingToolHelper helper(kundo2_i18n("Draw Polyline"), image, currentNode, rManager , strokeStyle, fillStyle); helper.setFGColorOverride(color); helper.setSelectionOverride(0); helper.paintPainterPath(outline); image->setModified(); } } 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/tool/kis_selection_tool_helper.cpp b/libs/ui/tool/kis_selection_tool_helper.cpp index db14dc4223..3a48b830ac 100644 --- a/libs/ui/tool/kis_selection_tool_helper.cpp +++ b/libs/ui/tool/kis_selection_tool_helper.cpp @@ -1,263 +1,227 @@ /* * Copyright (c) 2007 Sven Langkamp * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_selection_tool_helper.h" #include #include #include #include "kis_pixel_selection.h" #include "kis_shape_selection.h" #include "kis_image.h" #include "canvas/kis_canvas2.h" #include "KisViewManager.h" #include "kis_selection_manager.h" #include "kis_transaction.h" #include "commands/kis_selection_commands.h" #include "kis_shape_controller.h" #include #include "kis_processing_applicator.h" #include "kis_transaction_based_command.h" #include "kis_gui_context_command.h" #include "kis_command_utils.h" #include "commands/kis_deselect_global_selection_command.h" #include "kis_algebra_2d.h" #include "kis_config.h" KisSelectionToolHelper::KisSelectionToolHelper(KisCanvas2* canvas, const KUndo2MagicString& name) : m_canvas(canvas) , m_name(name) { m_image = m_canvas->viewManager()->image(); } KisSelectionToolHelper::~KisSelectionToolHelper() { } struct LazyInitGlobalSelection : public KisTransactionBasedCommand { LazyInitGlobalSelection(KisViewManager *view) : m_view(view) {} KisViewManager *m_view; KUndo2Command* paint() override { return !m_view->selection() ? new KisSetEmptyGlobalSelectionCommand(m_view->image()) : 0; } }; void KisSelectionToolHelper::selectPixelSelection(KisPixelSelectionSP selection, SelectionAction action) { KisViewManager* view = m_canvas->viewManager(); if (selection->selectedExactRect().isEmpty()) { m_canvas->viewManager()->selectionManager()->deselect(); return; } KisProcessingApplicator applicator(view->image(), 0 /* we need no automatic updates */, KisProcessingApplicator::SUPPORTS_WRAPAROUND_MODE, KisImageSignalVector() << ModifiedSignal, m_name); applicator.applyCommand(new LazyInitGlobalSelection(view)); struct ApplyToPixelSelection : public KisTransactionBasedCommand { ApplyToPixelSelection(KisViewManager *view, KisPixelSelectionSP selection, SelectionAction action) : m_view(view), m_selection(selection), m_action(action) {} KisViewManager *m_view; KisPixelSelectionSP m_selection; SelectionAction m_action; KUndo2Command* paint() override { KisPixelSelectionSP pixelSelection = m_view->selection()->pixelSelection(); KIS_ASSERT_RECOVER(pixelSelection) { return 0; } bool hasSelection = !pixelSelection->isEmpty(); KisSelectionTransaction transaction(pixelSelection); if (!hasSelection && m_action == SELECTION_SUBTRACT) { pixelSelection->invert(); } pixelSelection->applySelection(m_selection, m_action); - const QRect imageBounds = m_view->image()->bounds(); - QRect selectionExactRect = m_view->selection()->selectedExactRect(); - - if (!imageBounds.contains(selectionExactRect)) { - pixelSelection->crop(imageBounds); - if (pixelSelection->outlineCacheValid()) { - QPainterPath cache = pixelSelection->outlineCache(); - QPainterPath imagePath; - imagePath.addRect(imageBounds); - cache &= imagePath; - pixelSelection->setOutlineCache(cache); - } - selectionExactRect &= imageBounds; - } - - QRect dirtyRect = imageBounds; + QRect dirtyRect = m_view->image()->bounds(); if (hasSelection && m_action != SELECTION_REPLACE && m_action != SELECTION_INTERSECT) { dirtyRect = m_selection->selectedRect(); } m_view->selection()->updateProjection(dirtyRect); KUndo2Command *savedCommand = transaction.endAndTake(); pixelSelection->setDirty(dirtyRect); - if (selectionExactRect.isEmpty()) { + if (m_view->selection()->selectedExactRect().isEmpty()) { KisCommandUtils::CompositeCommand *cmd = new KisCommandUtils::CompositeCommand(); cmd->addCommand(savedCommand); cmd->addCommand(new KisDeselectGlobalSelectionCommand(m_view->image())); savedCommand = cmd; } return savedCommand; } }; applicator.applyCommand(new ApplyToPixelSelection(view, selection, action)); applicator.end(); } void KisSelectionToolHelper::addSelectionShape(KoShape* shape) { QList shapes; shapes.append(shape); addSelectionShapes(shapes); } void KisSelectionToolHelper::addSelectionShapes(QList< KoShape* > shapes) { KisViewManager* view = m_canvas->viewManager(); if (view->image()->wrapAroundModePermitted()) { view->showFloatingMessage( i18n("Shape selection does not fully " "support wraparound mode. Please " "use pixel selection instead"), KisIconUtils::loadIcon("selection-info")); } KisProcessingApplicator applicator(view->image(), 0 /* we need no automatic updates */, KisProcessingApplicator::NONE, KisImageSignalVector() << ModifiedSignal, m_name); applicator.applyCommand(new LazyInitGlobalSelection(view)); struct ClearPixelSelection : public KisTransactionBasedCommand { ClearPixelSelection(KisViewManager *view) : m_view(view) {} KisViewManager *m_view; KUndo2Command* paint() override { KisPixelSelectionSP pixelSelection = m_view->selection()->pixelSelection(); KIS_ASSERT_RECOVER(pixelSelection) { return 0; } KisSelectionTransaction transaction(pixelSelection); pixelSelection->clear(); return transaction.endAndTake(); } }; applicator.applyCommand(new ClearPixelSelection(view)); struct AddSelectionShape : public KisTransactionBasedCommand { AddSelectionShape(KisViewManager *view, KoShape* shape) : m_view(view), m_shape(shape) {} KisViewManager *m_view; KoShape* m_shape; KUndo2Command* paint() override { /** * Mark a shape that it belongs to a shape selection */ if(!m_shape->userData()) { m_shape->setUserData(new KisShapeSelectionMarker); } return m_view->canvasBase()->shapeController()->addShape(m_shape); } }; Q_FOREACH (KoShape* shape, shapes) { applicator.applyCommand( new KisGuiContextCommand(new AddSelectionShape(view, shape), view)); } applicator.end(); } - -void KisSelectionToolHelper::cropRectIfNeeded(QRect *rect, SelectionAction action) -{ - KisImageWSP image = m_canvas->viewManager()->image(); - - if (!image->wrapAroundModePermitted() && action != SELECTION_SUBTRACT) { - *rect &= image->bounds(); - } -} - bool KisSelectionToolHelper::canShortcutToDeselect(const QRect &rect, SelectionAction action) { return rect.isEmpty() && (action == SELECTION_INTERSECT || action == SELECTION_REPLACE); } bool KisSelectionToolHelper::canShortcutToNoop(const QRect &rect, SelectionAction action) { return rect.isEmpty() && action == SELECTION_ADD; } -void KisSelectionToolHelper::cropPathIfNeeded(QPainterPath *path) -{ - KisImageWSP image = m_canvas->viewManager()->image(); - - if (!image->wrapAroundModePermitted()) { - QPainterPath cropPath; - cropPath.addRect(image->bounds()); - *path &= cropPath; - } -} - bool KisSelectionToolHelper::tryDeselectCurrentSelection(const QRectF selectionViewRect, SelectionAction action) { bool result = false; if (KisAlgebra2D::maxDimension(selectionViewRect) < KisConfig().selectionViewSizeMinimum() && (action == SELECTION_INTERSECT || action == SELECTION_REPLACE)) { // Queueing this action to ensure we avoid a race condition when unlocking the node system QTimer::singleShot(0, m_canvas->viewManager()->selectionManager(), SLOT(deselect())); result = true; } return result; } diff --git a/libs/ui/tool/kis_selection_tool_helper.h b/libs/ui/tool/kis_selection_tool_helper.h index 33fd928020..dc4c838cea 100644 --- a/libs/ui/tool/kis_selection_tool_helper.h +++ b/libs/ui/tool/kis_selection_tool_helper.h @@ -1,61 +1,59 @@ /* * Copyright (c) 2007 Sven Langkamp * * 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_SELECTION_TOOL_HELPER_H #define KIS_SELECTION_TOOL_HELPER_H #include #include #include "kundo2magicstring.h" #include "kis_layer.h" #include "kis_selection.h" #include "kis_canvas2.h" class KoShape; /** * XXX: Doc! */ class KRITAUI_EXPORT KisSelectionToolHelper { public: KisSelectionToolHelper(KisCanvas2* canvas, const KUndo2MagicString& name); virtual ~KisSelectionToolHelper(); void selectPixelSelection(KisPixelSelectionSP selection, SelectionAction action); void addSelectionShape(KoShape* shape); void addSelectionShapes(QList shapes); - void cropRectIfNeeded(QRect *rect, SelectionAction action); bool canShortcutToDeselect(const QRect &rect, SelectionAction action); bool canShortcutToNoop(const QRect &rect, SelectionAction action); - void cropPathIfNeeded(QPainterPath *path); bool tryDeselectCurrentSelection(const QRectF selectionViewRect, SelectionAction action); private: QPointer m_canvas; KisImageSP m_image; KisLayerSP m_layer; KUndo2MagicString m_name; }; #endif diff --git a/plugins/extensions/pykrita/plugin/plugins/assignprofiledialog/kritapykrita_assignprofiledialog.desktop b/plugins/extensions/pykrita/plugin/plugins/assignprofiledialog/kritapykrita_assignprofiledialog.desktop index f1b0de32e4..dfc4377128 100644 --- a/plugins/extensions/pykrita/plugin/plugins/assignprofiledialog/kritapykrita_assignprofiledialog.desktop +++ b/plugins/extensions/pykrita/plugin/plugins/assignprofiledialog/kritapykrita_assignprofiledialog.desktop @@ -1,36 +1,38 @@ [Desktop Entry] Type=Service ServiceTypes=Krita/PythonPlugin X-KDE-Library=assignprofiledialog X-Python-2-Compatible=false Name=Assign Profile to Image Name[ca]=Assigna un perfil a una imatge Name[ca@valencia]=Assigna un perfil a una imatge Name[cs]=Přiřadit obrázku profil Name[en_GB]=Assign Profile to Image Name[es]=Asignar perfil a imagen Name[gl]=Asignar un perfil á imaxe Name[it]=Assegna profilo a immagine Name[nl]=Profiel aan afbeelding toekennen Name[pl]=Przypisz profil do obrazu Name[pt]=Atribuir um Perfil à Imagem Name[pt_BR]=Atribuir perfil a imagem Name[sv]=Tilldela profil till bild Name[tr]=Görüntüye Profil Ata Name[uk]=Призначити профіль до зображення Name[x-test]=xxAssign Profile to Imagexx +Name[zh_CN]=应用配置到图像 Comment=Assign a profile to an image without converting it. Comment[ca]=Assigna un perfil a una imatge sense convertir-la. Comment[ca@valencia]=Assigna un perfil a una imatge sense convertir-la. Comment[en_GB]=Assign a profile to an image without converting it. Comment[es]=Asignar un perfil a una imagen sin convertirla. Comment[gl]=Asignar un perfil a unha imaxe sen convertela. Comment[it]=Assegna un profilo a un'immagine senza convertirla. Comment[nl]=Een profiel aan een afbeelding toekennen zonder het te converteren. Comment[pl]=Przypisz profil do obrazu bez jego przekształcania. Comment[pt]=Atribui um perfil à imagem sem a converter. Comment[pt_BR]=Atribui um perfil para uma imagem sem convertê-la. Comment[sv]=Tilldela en profil till en bild utan att konvertera den. Comment[tr]=Bir görüntüye, görüntüyü değiştirmeden bir profil ata. Comment[uk]=Призначити профіль до зображення без його перетворення. Comment[x-test]=xxAssign a profile to an image without converting it.xx +Comment[zh_CN]=为图像设定配置而无需转换它。 diff --git a/plugins/extensions/pykrita/plugin/plugins/colorspace/kritapykrita_colorspace.desktop b/plugins/extensions/pykrita/plugin/plugins/colorspace/kritapykrita_colorspace.desktop index f90ad28ac2..38b14f44ce 100644 --- a/plugins/extensions/pykrita/plugin/plugins/colorspace/kritapykrita_colorspace.desktop +++ b/plugins/extensions/pykrita/plugin/plugins/colorspace/kritapykrita_colorspace.desktop @@ -1,32 +1,35 @@ [Desktop Entry] Type=Service ServiceTypes=Krita/PythonPlugin X-KDE-Library=colorspace X-Python-2-Compatible=false Name=Color Space Name[ca]=Espai de color Name[ca@valencia]=Espai de color Name[cs]=Barevný prostor Name[es]=Espacio de color Name[gl]=Espazo de cores Name[it]=Spazio dei colori Name[nl]=Kleurruimte +Name[pl]=Przestrzeń barw Name[pt]=Espaço de Cores Name[pt_BR]=Espaço de cores Name[sk]=Farebný priestor Name[sv]=Färgrymd Name[uk]=Простір кольорів Name[x-test]=xxColor Spacexx Name[zh_CN]=色彩空间 Comment=Plugin to change color space to selected documents Comment[ca]=Un connector per canviar l'espai de color dels documents seleccionats Comment[ca@valencia]=Un connector per canviar l'espai de color dels documents seleccionats Comment[es]=Complemento para cambiar el espacio de color de los documentos seleccionados Comment[gl]=Complemento para cambiar o espazo de cores dos documentos seleccionados. Comment[it]=Estensione per cambiare lo spazio dei colori ai documenti selezionati Comment[nl]=Plug-in om kleurruimte in geselecteerde documenten te wijzigen +Comment[pl]=Wtyczka do zmiany przestrzeni barw wybranych dokumentów Comment[pt]='Plugin' para mudar o espaço de cores do documento seleccionado Comment[pt_BR]=Plug-in para alterar o espaço de cores em documentos selecionados Comment[sv]=Insticksprogram för att ändra färgrymd för valda dokument Comment[uk]=Додаток для зміни простору кольорів у позначених документах Comment[x-test]=xxPlugin to change color space to selected documentsxx +Comment[zh_CN]=将颜色空间改为所选文档的插件 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 index 07655d694a..32e2746a2a 100644 --- a/plugins/extensions/pykrita/plugin/plugins/comics_project_management_tools/__init__.py +++ b/plugins/extensions/pykrita/plugin/plugins/comics_project_management_tools/__init__.py @@ -1,2 +1,2 @@ - # let's make a module +# 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 index d68e39eead..d1052919a5 100644 --- 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 @@ -1,361 +1,397 @@ """ Part of the comics project management tools (CPMT). A dialog for editing the exporter settings. """ -from PyQt5.QtGui import * -from PyQt5.QtWidgets import * -from PyQt5.QtCore import * +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): + + 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)) - + 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 = 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. + + # 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. + # 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. + + # 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) + # CBZexportSettings.layout().addWidget(CBZgroupMeta) CBZgroupMeta.setLayout(QFormLayout()) - + mainWidget.addTab(CBZexportSettings, "CBZ") - - #ACBF, crop, resize, creator name, version history, panel layer, text layers. + + # 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. + + # 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. + + # 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. + + # 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"+self.spnACBFVersion.value()+"-"+i18n("in this version...")) + 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())) - + 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 index c5a383baab..dab2b683ab 100644 --- 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 @@ -1,1080 +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.QtGui import * #For the progress dialog. +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] + 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) + # percentage + percentage = config["Percentage"] / 100 + listScaleTo[0] = round(oldWidth * percentage) + listScaleTo[1] = round(oldHeight * percentage) if method is 1: - #dpi + # dpi DPI = config["DPI"] - listScaleTo[0] = round((oldWidth/oldXDPI)*DPI) - listScaleTo[1] = round((oldHeight/oldYDPI)*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 + # maximum width width = config["Width"] listScaleTo[0] = width - listScaleTo[1] = round((oldHeight/oldWidth)*width) + listScaleTo[1] = round((oldHeight / oldWidth) * width) if method is 3: - #maximum height + # maximum height height = config["Height"] listScaleTo[1] = height - listScaleTo[0] = round((oldWidth/oldHeight)*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) - exportPath = path / self.configDictionary["exportLocation"] - if Path(exportPath / "metadata").exists() is False: - Path(exportPath/"metadata").mkdir() - - - 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_success = self.save_out_pngs(sizesList) - if export_success: - export_success = self.export_to_acbf() - if export_success: - if "CBZ" in sizesList.keys(): - export_success = self.export_to_cbz() - print("Exported to CBZ", export_success) - if "EPUB" in sizesList.keys(): - export_success = self.export_to_epub() - print("Exported to EPUB", export_success) - + 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 = 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 + 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 + + # for each image, make an xml file htmlFiles = [] for i in range(len(pagesList)): - pageName = "Page"+str(i)+".xhtml" + 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: + + if pagesList[i] != coverpageurl: pagenumber = ET.Element("p") - pagenumber.text = "Page "+str(i) + pagenumber.text = "Page " + str(i) body.append(pagenumber) html.append(body) - - filename = str(Path(textPath/pageName)) + + filename = str(Path(textPath / pageName)) doc.write(filename, encoding="utf-8", xml_declaration=True) - if pagesList[i]==coverpageurl: + if pagesList[i] == coverpageurl: coverpagehtml = os.path.relpath(filename, str(oebps)) htmlFiles.append(filename) - - #opf file + + # opf file opfFile = ET.ElementTree() opfRoot = ET.Element("package") opfRoot.set("version", "3.0") - opfRoot.set("unique-identifier","BookId") + opfRoot.set("unique-identifier", "BookId") opfRoot.set("xmlns", "http://www.idpf.org/2007/opf") opfFile._setroot(opfRoot) - - #metadata + + # 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"]+")") + authorName.append("(" + authorDict["nickname"] + ")") author.text = ", ".join(authorName) opfMeta.append(author) if "role" in authorDict.keys(): - author.set("id", "cre"+str(authorE)) + author.set("id", "cre" + str(authorE)) role = ET.Element("meta") - role.set("refines", "cre"+str(authorE)) + role.set("refines", "cre" + str(authorE)) role.set("scheme", "marc:relators") role.set("property", "role") - role.text= str(authorDict["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"] + 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: + 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 + + 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) - + + 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(): - if len(sizesList.keys())<1: + + # 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)) + 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.refreshProjection() page.flatten() while page.isIdle() is False: page.waitForDone() + + # Start making the format specific copy. if page.isIdle(): - #page crop for key in sizesList.keys(): w = sizesList[key] - #copy over data + # 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 + 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)>0: + 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)>0: + 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 + croph = page.height() - self.configDictionary["cropBottom"] - cropy projection.crop(cropx, cropy, cropw, croph) projection.waitForDone() - - #resize appropriately + + # 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: - if projection.colorDepth() is not "U8" or projection.colorDepth() is not "U16": + # 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 - folderName = str(key+"-"+w["FileType"]) + + # 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) - fn = os.path.join(str(Path(exportPath/folderName)), "page_"+format(p,"03d")+"_"+str(listScales[0])+"x"+str(listScales[1])+"."+w["FileType"]) + 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 + + # 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. + # 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: + 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")) + 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 + 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) - + # 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: + 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")) + 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"]+")") + 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")) + 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"]+")") + 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"])): + 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"]+")") + 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"] - url = os.path.join(self.projectURL, self.configDictionary["exportLocation"], title+".cbz") + + # 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() - pass - + + """ + 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) - shutil.move(src=str(url+".zip"), dst=str(url+".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 index 5ae39c36bd..153004abdf 100644 --- 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 @@ -1,567 +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 os # For finding the script location. import csv -from pathlib import Path #For reading all the files in a directory. -from PyQt5.QtGui import * -from PyQt5.QtWidgets import * -from PyQt5.QtCore import * +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: + 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): + 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 + completerStrings = [] + completerColumn = 0 + def __init__(self, parent=None): super(QStyledItemDelegate, self).__init__(parent) - def setCompleterData(self,completerStrings, completerColumn): + 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) + 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. + 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 = 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 self.authorTable.verticalHeader().count(): + 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.append(str(logicalI+1)) + 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()): + + 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 + 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"] + 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. + 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: + 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: + 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: + 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: + 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: + 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: + 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: + 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: + if len(self.lnSeriesName.text()) > 0: config["seriesName"] = self.lnSeriesName.text() config["seriesNumber"] = self.spnSeriesNumber.value() - if self.spnSeriesVol.value() >0: + 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: + 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 index 761b34964d..ffc7788b2f 100644 --- 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 @@ -1,640 +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 readin gof documents +import zipfile # quick reading of documents import shutil import xml.etree.ElementTree as ET -from PyQt5.QtGui import * -from PyQt5.QtWidgets import * +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: + + # 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.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)) + + 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] + 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.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): + 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(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 + 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) + 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') + 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) + 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"])) - + 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] - + 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): + 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. + + # check for page list and or location. pagesList = [] if "pages" in self.setupDictionary.keys(): pagesList = self.setupDictionary["pages"] - if (str(self.setupDictionary["pagesLocation"]).isspace()) : + 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") + + # 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) + 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 : + 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) + 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. + + # Get out the extra data for the standard item. newPageItem = QStandardItem() - newPageItem.setIcon(QIcon(QPixmap.fromImage(newPage.thumbnail(100,100)))) + 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. + + # close page document. newPage.waitForDone() if newPage.isIdle(): newPage.close() - - #add item to page. + + # 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. + + # 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.append(str(index.row()+1)) + 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: + 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') + 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): + 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 = 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 (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 index 323c78a93f..0fbd1161dd 100644 --- 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 @@ -1,45 +1,48 @@ """ Part of the comics project management tools (CPMT). This is a docker that shows your comic pages. """ -from PyQt5.QtGui import * -from PyQt5.QtWidgets import * -from PyQt5.QtCore import * +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()): + 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()): + 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 index e52ad7204e..0d518d4d01 100644 --- 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 @@ -1,159 +1,171 @@ """ Part of the comics project management tools (CPMT). A dialog for editing the general project settings. """ import os -from PyQt5.QtGui import * -from PyQt5.QtWidgets import * -from PyQt5.QtCore import * +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): + + 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.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.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 = 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()): + + 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()): + + 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 = 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.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"] ) + self.pagesLocation.setLocation(config["pagesLocation"]) if "exportLocation" in config.keys(): - self.exportLocation.setLocation( config["exportLocation"] ) + 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 index ed1e92a6f2..c86f7f6411 100644 --- 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 @@ -1,172 +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 sys -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.QtGui import * -from PyQt5.QtWidgets import * -from PyQt5.QtCore import * + +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) + # 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: + 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: + 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) - + 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() - + 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") + 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())) + 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 index ff934ebb5c..ef45a9c8af 100644 --- 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 @@ -1,279 +1,283 @@ """ Part of the comics project management tools (CPMT). Template dialog """ import os import shutil -from PyQt5.QtGui import * -from PyQt5.QtWidgets import * -from PyQt5.QtCore import * +#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. + self.setCurrentIndex(2) # set to centimeter if metric system. else: self.setCurrentIndex(1) - + def pixelsForUnit(self, unit, DPI): - if (self.currentText()==self.pixels): + if (self.currentText() == self.pixels): return unit - elif (self.currentText()==self.inches): + elif (self.currentText() == self.inches): return self.inchesToPixels(unit, DPI) - elif (self.currentText()==self.centimeter): + elif (self.currentText() == self.centimeter): return self.centimeterToPixels(unit, DPI) - elif (self.currentText()==self.millimeter): + elif (self.currentText() == self.millimeter): return self.millimeterToPixels(unit, DPI) - + def inchesToPixels(self, inches, DPI): - return DPI*inches - + return DPI * inches + def centimeterToInches(self, cm): - return cm/2.54 + return cm / 2.54 def centimeterToPixels(self, cm, DPI): return self.inchesToPixels(self.centimeterToInches(cm), DPI) - + def millimeterToCentimeter(self, mm): - return mm/10 + 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) - + 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: + 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] + 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() - + 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 = 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 = 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)) + 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.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) - + 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) - + 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") + + 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/kritapykrita_comics_project_management_tools.desktop b/plugins/extensions/pykrita/plugin/plugins/comics_project_management_tools/kritapykrita_comics_project_management_tools.desktop index e98929d76e..c671a2d0f8 100644 --- 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 @@ -1,7 +1,27 @@ [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[es]=Herramientas de gestión de proyectos de cómics +Name[it]=Strumenti per la gestione dei progetti di fumetti +Name[nl]=Hulpmiddelen voor projectbeheer van strips +Name[pl]=Narzędzia do zarządzania projektami komiksów +Name[pt]=Ferramentas de Gestão de Projectos de Banda Desenhada +Name[sv]=Projekthanteringsverktyg för tecknade serier +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[es]=Herramientas para gestionar cómics. +Comment[it]=Strumenti per la gestione dei fumetti. +Comment[nl]=Hulpmiddelen voor beheer van strips. +Comment[pl]=Narzędzie do zarządzania komiksami. +Comment[pt]=Ferramentas para gerir bandas desenhadas. +Comment[sv]=Verktyg för att hantera tecknade serier. +Comment[uk]=Інструменти для керування коміксами +Comment[x-test]=xxTools for managing comics.xx diff --git a/plugins/extensions/pykrita/plugin/plugins/documenttools/kritapykrita_documenttools.desktop b/plugins/extensions/pykrita/plugin/plugins/documenttools/kritapykrita_documenttools.desktop index dfa6fa1453..d7d0b3d046 100644 --- a/plugins/extensions/pykrita/plugin/plugins/documenttools/kritapykrita_documenttools.desktop +++ b/plugins/extensions/pykrita/plugin/plugins/documenttools/kritapykrita_documenttools.desktop @@ -1,29 +1,34 @@ [Desktop Entry] Type=Service ServiceTypes=Krita/PythonPlugin X-KDE-Library=documenttools X-Python-2-Compatible=false Name=Document Tools Name[ca]=Eines de document Name[ca@valencia]=Eines de document +Name[cs]=Dokumentové nástroje Name[es]=Herramientas de documentos Name[gl]=Ferramentas de documentos Name[it]=Strumenti per i documenti Name[nl]=Documenthulpmiddelen +Name[pl]=Narzędzia dokumentu Name[pt]=Ferramentas de Documentos Name[pt_BR]=Ferramentas de documento Name[sv]=Dokumentverktyg Name[uk]=Засоби документа Name[x-test]=xxDocument Toolsxx +Name[zh_CN]=文档工具 Comment=Plugin to manipulate properties of selected documents Comment[ca]=Un connector per manipular propietats dels documents seleccionats Comment[ca@valencia]=Un connector per manipular propietats dels documents seleccionats Comment[es]=Complemento para manipular las propiedades de los documentos seleccionados Comment[gl]=Complemento para manipular as propiedades dos documentos seleccionados. Comment[it]=Estensione per manipolare le proprietà dei documenti selezionati Comment[nl]=Plug-in om eigenschappen van geselecteerde documenten te manipuleren +Comment[pl]=Wtyczka do zmiany właściwości wybranych dokumentów Comment[pt]='Plugin' para manipular as propriedades dos documentos seleccionados Comment[pt_BR]=Plug-in para manipular as propriedades de documentos selecionados Comment[sv]=Insticksprogram för att ändra egenskaper för valda dokument Comment[uk]=Додаток для керування властивостями позначених документів Comment[x-test]=xxPlugin to manipulate properties of selected documentsxx +Comment[zh_CN]=操纵选中文档的属性的插件 diff --git a/plugins/extensions/pykrita/plugin/plugins/exportlayers/kritapykrita_exportlayers.desktop b/plugins/extensions/pykrita/plugin/plugins/exportlayers/kritapykrita_exportlayers.desktop index 382c2ae6c7..61719b8064 100644 --- a/plugins/extensions/pykrita/plugin/plugins/exportlayers/kritapykrita_exportlayers.desktop +++ b/plugins/extensions/pykrita/plugin/plugins/exportlayers/kritapykrita_exportlayers.desktop @@ -1,29 +1,35 @@ [Desktop Entry] Type=Service ServiceTypes=Krita/PythonPlugin X-KDE-Library=exportlayers X-Python-2-Compatible=false Name=Export Layers Name[ca]=Exportació de capes Name[ca@valencia]=Exportació de capes +Name[cs]=Exportovat vrstvy Name[es]=Exportar capas Name[gl]=Exportar as capas Name[it]=Esporta livelli Name[nl]=Lagen exporteren +Name[pl]=Eksportuj warstwy Name[pt]=Exportar as Camadas Name[pt_BR]=Exportar camadas Name[sv]=Exportera lager Name[uk]=Експортувати шари Name[x-test]=xxExport Layersxx +Name[zh_CN]=导出图层 Comment=Plugin to export layers from a document Comment[ca]=Un connector per exportar capes d'un document Comment[ca@valencia]=Un connector per exportar capes d'un document +Comment[cs]=Modul pro export vrstev z dokumentu Comment[es]=Complemento para exportar las capas de un documento Comment[gl]=Complemento para exportar as capas dun documento. Comment[it]=Estensione per esportare i livelli da un documento Comment[nl]=Plug-in om lagen uit een document te exporteren +Comment[pl]=Wtyczka do eksportowania warstw z dokumentu Comment[pt]='Plugin' para exportar as camadas de um documento Comment[pt_BR]=Plug-in para exportar as camadas de um documento Comment[sv]=Insticksprogram för att exportera lager från ett dokument Comment[uk]=Додаток для експортування шарів з документа Comment[x-test]=xxPlugin to export layers from a documentxx +Comment[zh_CN]=从文档导出图层的插件 diff --git a/plugins/extensions/pykrita/plugin/plugins/filtermanager/kritapykrita_filtermanager.desktop b/plugins/extensions/pykrita/plugin/plugins/filtermanager/kritapykrita_filtermanager.desktop index dcfd45398b..8a0313e9ff 100644 --- a/plugins/extensions/pykrita/plugin/plugins/filtermanager/kritapykrita_filtermanager.desktop +++ b/plugins/extensions/pykrita/plugin/plugins/filtermanager/kritapykrita_filtermanager.desktop @@ -1,29 +1,34 @@ [Desktop Entry] Type=Service ServiceTypes=Krita/PythonPlugin X-KDE-Library=filtermanager X-Python-2-Compatible=false Name=Filter Manager Name[ca]=Gestor de filtres Name[ca@valencia]=Gestor de filtres +Name[cs]=Správce filtrů Name[es]=Gestor de filtros Name[gl]=Xestor de filtros Name[it]=Gestore dei filtri Name[nl]=Beheerder van filters +Name[pl]=Zarządzanie filtrami Name[pt]=Gestor de Filtros Name[pt_BR]=Gerenciador de filtros Name[sv]=Filterhantering Name[uk]=Керування фільтрами Name[x-test]=xxFilter Managerxx +Name[zh_CN]=滤镜管理器 Comment=Plugin to filters management Comment[ca]=Un connector per gestionar filtres Comment[ca@valencia]=Un connector per gestionar filtres Comment[es]=Complemento para la gestión de filtros Comment[gl]=Complemento para a xestión de filtros. Comment[it]=Estensione per la gestione dei filtri Comment[nl]=Plug-in voor beheer van filters +Comment[pl]=Wtyczka do zarządzania filtrami Comment[pt]='Plugin' para a gestão de filtros Comment[pt_BR]=Plug-in para gerenciamento de filtros Comment[sv]=Insticksprogram för filterhantering Comment[uk]=Додаток для керування фільтрами Comment[x-test]=xxPlugin to filters managementxx +Comment[zh_CN]=滤镜管理插件 diff --git a/plugins/extensions/pykrita/plugin/plugins/hello/kritapykrita_hello.desktop b/plugins/extensions/pykrita/plugin/plugins/hello/kritapykrita_hello.desktop index 0364dd8721..59d7afab9d 100644 --- a/plugins/extensions/pykrita/plugin/plugins/hello/kritapykrita_hello.desktop +++ b/plugins/extensions/pykrita/plugin/plugins/hello/kritapykrita_hello.desktop @@ -1,39 +1,41 @@ [Desktop Entry] Type=Service ServiceTypes=Krita/PythonPlugin X-KDE-Library=hello X-Python-2-Compatible=false Name=Hello World Name[ca]=Hola món Name[ca@valencia]=Hola món Name[cs]=Hello World Name[en_GB]=Hello World Name[es]=Hola mundo Name[fr]=Bonjour tout le monde Name[gl]=Ola mundo Name[it]=Ciao mondo Name[nl]=Hallo wereld Name[pl]=Witaj świecie Name[pt]=Olá Mundo Name[pt_BR]=Olá mundo Name[sk]=Ahoj svet Name[sv]=Hello World Name[tr]=Merhaba Dünya Name[uk]=Привіт, світе Name[x-test]=xxHello Worldxx +Name[zh_CN]=Hello World Comment=Basic plugin to test PyKrita Comment[ca]=Connector bàsic per provar el PyKrita Comment[ca@valencia]=Connector bàsic per provar el PyKrita Comment[cs]=Základní modul pro testování PyKrita Comment[en_GB]=Basic plugin to test PyKrita Comment[es]=Complemento básico para probar PyKrita Comment[gl]=Complemento básico para probar PyKrita. Comment[it]=Estensione di base per provare PyKrita Comment[nl]=Basisplug-in om PyKrita te testen Comment[pl]=Podstawowa wtyczka do wypróbowania PyKrity Comment[pt]='Plugin' básico para testar o PyKrita Comment[pt_BR]=Plug-in básico para testar o PyKrita Comment[sv]=Enkelt insticksprogram för att utprova PyKrita Comment[tr]=PyKrita'yı test etmek için temel eklenti Comment[uk]=Базовий додаток для тестування PyKrita Comment[x-test]=xxBasic plugin to test PyKritaxx +Comment[zh_CN]=测试 PyKrita 的基本插件 diff --git a/plugins/extensions/pykrita/plugin/plugins/highpass/kritapykrita_highpass.desktop b/plugins/extensions/pykrita/plugin/plugins/highpass/kritapykrita_highpass.desktop index b98ae07537..d22f4eb667 100644 --- a/plugins/extensions/pykrita/plugin/plugins/highpass/kritapykrita_highpass.desktop +++ b/plugins/extensions/pykrita/plugin/plugins/highpass/kritapykrita_highpass.desktop @@ -1,38 +1,40 @@ [Desktop Entry] Type=Service ServiceTypes=Krita/PythonPlugin X-KDE-Library=highpass X-Python-2-Compatible=false Name=Highpass Filter Name[ca]=Filtre passa-alt Name[ca@valencia]=Filtre passa-alt Name[cs]=Filtr s horní propustí Name[de]=Hochpassfilter Name[en_GB]=Highpass Filter Name[es]=Filtro paso alto Name[gl]=Filtro de paso alto Name[it]=Filtro di accentuazione passaggio Name[nl]=Hoogdoorlaatfilter Name[pl]=Filtr górnoprzepustowy Name[pt]=Filtro Passa-Alto Name[pt_BR]=Filtro passa alta Name[sv]=Högpassfilter Name[tr]=Yüksek Geçirgen Süzgeç Name[uk]=Високочастотний фільтр Name[x-test]=xxHighpass Filterxx +Name[zh_CN]=高通滤镜 Comment=Highpass Filter, based on http://registry.gimp.org/node/7385 Comment[ca]=Filtre passa-alt, basat en el http://registry.gimp.org/node/7385 Comment[ca@valencia]=Filtre passa-alt, basat en el http://registry.gimp.org/node/7385 Comment[de]=Hochpassfilter, Grundlage ist http://registry.gimp.org/node/7385 Comment[en_GB]=Highpass Filter, based on http://registry.gimp.org/node/7385 Comment[es]=Filtro paso alto, basado en http://registry.gimp.org/node/7385 Comment[gl]=Filtro de paso alto, baseado en http://registry.gimp.org/node/7385. Comment[it]=Filtro di accentuazione passaggio, basato su http://registry.gimp.org/node/7385 Comment[nl]=Hoogdoorlaatfilter, gebaseerd op http://registry.gimp.org/node/7385 Comment[pl]=Filtr górnoprzepustowy, oparty na http://registry.gimp.org/node/7385 Comment[pt]=Filtro passa-alto, baseado em http://registry.gimp.org/node/7385 Comment[pt_BR]=Filtro passa alta, baseado em http://registry.gimp.org/node/7385 Comment[sv]=Högpassfilter, baserat på http://registry.gimp.org/node/7385 Comment[tr]=http://registry.gimp.org/node/7385 tabanlı Yüksek Geçirgen Süzgeç Comment[uk]=Високочастотний фільтр, засновано на on http://registry.gimp.org/node/7385 Comment[x-test]=xxHighpass Filter, based on http://registry.gimp.org/node/7385xx +Comment[zh_CN]=高通滤镜,基于 http://registry.gimp.org/node/7385 diff --git a/plugins/extensions/pykrita/plugin/plugins/lastdocumentsdocker/kritapykrita_lastdocumentsdocker.desktop b/plugins/extensions/pykrita/plugin/plugins/lastdocumentsdocker/kritapykrita_lastdocumentsdocker.desktop index f21de2391d..8e052723f0 100644 --- a/plugins/extensions/pykrita/plugin/plugins/lastdocumentsdocker/kritapykrita_lastdocumentsdocker.desktop +++ b/plugins/extensions/pykrita/plugin/plugins/lastdocumentsdocker/kritapykrita_lastdocumentsdocker.desktop @@ -1,27 +1,31 @@ [Desktop Entry] Type=Service ServiceTypes=Krita/PythonPlugin X-KDE-Library=lastdocumentsdocker X-Python-2-Compatible=false Name=Last Documents Docker Name[ca]=Acoblador dels darrers documents Name[ca@valencia]=Acoblador dels darrers documents Name[es]=Panel de últimos documentos Name[gl]=Doca dos últimos documentos Name[it]=Area di aggancio Ultimi documenti Name[nl]=Vastzetter van laatste documenten +Name[pl]=Dok ostatnich dokumentów Name[pt]=Área dos Últimos Documentos Name[sv]=Dockningsfönster för senaste dokument Name[uk]=Бічна панель останніх документів Name[x-test]=xxLast Documents Dockerxx +Name[zh_CN]=最近文档坞窗 Comment=A Python-based docker for show thumbnails to last ten documents Comment[ca]=Un acoblador basant en el Python per mostrar miniatures dels darrers deu documents Comment[ca@valencia]=Un acoblador basant en el Python per mostrar miniatures dels darrers deu documents Comment[es]=Un panel basado en Python para mostrar miniaturas de los últimos diez documentos Comment[gl]=Unha doca escrita en Python para mostrar as miniaturas dos últimos dez documentos. Comment[it]=Un'area di aggancio basata su Python per mostrare miniature degli ultimi dieci documenti. Comment[nl]=Een op Python gebaseerde vastzetter om miniaturen te tonen naar de laatste tien documenten. +Comment[pl]=Dok oparty na pythonie do wyświetlania miniatur ostatnich dziesięciu dokumentów Comment[pt]=Uma área acoplável, feita em Python, para mostrar as miniaturas dos últimos dez documentos Comment[sv]=Ett Python-baserat dockningsfönster för att visa miniatyrbilder för de tio senaste dokumenten Comment[uk]=Бічна панель на основі Python для показу мініатюр останніх десяти документів Comment[x-test]=xxA Python-based docker for show thumbnails to last ten documentsxx +Comment[zh_CN]=基于 Python 的坞窗,展示最近十个文档的缩略图 diff --git a/plugins/extensions/pykrita/plugin/plugins/palette_docker/kritapykrita_palette_docker.desktop b/plugins/extensions/pykrita/plugin/plugins/palette_docker/kritapykrita_palette_docker.desktop index 39badbb00e..4a98e9c2bf 100644 --- a/plugins/extensions/pykrita/plugin/plugins/palette_docker/kritapykrita_palette_docker.desktop +++ b/plugins/extensions/pykrita/plugin/plugins/palette_docker/kritapykrita_palette_docker.desktop @@ -1,27 +1,32 @@ [Desktop Entry] Type=Service ServiceTypes=Krita/PythonPlugin X-KDE-Library=palette_docker X-Python-2-Compatible=false Name=Palette docker Name[ca]=Acoblador de paletes Name[ca@valencia]=Acoblador de paletes +Name[cs]=Dok palet Name[es]=Panel de paleta Name[gl]=Doca de paleta Name[it]=Area di aggancio della tavolozza Name[nl]=Vastzetter van palet +Name[pl]=Dok palety Name[pt]=Área acoplável da paleta Name[sv]=Dockningsfönster för palett Name[uk]=Панель палітри Name[x-test]=xxPalette dockerxx +Name[zh_CN]=调色板坞窗 Comment=A Python-based docker to edit color palettes. Comment[ca]=Un acoblador basant en el Python per editar paletes de colors. Comment[ca@valencia]=Un acoblador basant en el Python per editar paletes de colors. Comment[es]=Un panel basado en Python para editar paletas de colores. Comment[gl]=unha doca escrita en Python para editar paletas de cores. Comment[it]=Un'area di aggancio per modificare le tavolozze di colori basata su Python. Comment[nl]=Een op Python gebaseerde vastzetter om kleurpaletten te bewerken. +Comment[pl]=Dok oparty na pythonie do edytowania palet barw. Comment[pt]=Uma área acoplável, feita em Python, para editar paletas de cores. Comment[sv]=Ett Python-baserat dockningsfönster för att redigera färgpaletter. Comment[uk]=Бічна панель для редагування палітр кольорів на основі Python. Comment[x-test]=xxA Python-based docker to edit color palettes.xx +Comment[zh_CN]=基于 Python 的调色板编辑坞窗 diff --git a/plugins/extensions/pykrita/plugin/plugins/quick_settings_docker/kritapykrita_quick_settings_docker.desktop b/plugins/extensions/pykrita/plugin/plugins/quick_settings_docker/kritapykrita_quick_settings_docker.desktop index f8ba508426..36b2e90161 100644 --- a/plugins/extensions/pykrita/plugin/plugins/quick_settings_docker/kritapykrita_quick_settings_docker.desktop +++ b/plugins/extensions/pykrita/plugin/plugins/quick_settings_docker/kritapykrita_quick_settings_docker.desktop @@ -1,27 +1,32 @@ [Desktop Entry] Type=Service ServiceTypes=Krita/PythonPlugin X-KDE-Library=quick_settings_docker X-Python-2-Compatible=false Name=Quick Settings Docker Name[ca]=Acoblador d'arranjament ràpid Name[ca@valencia]=Acoblador d'arranjament ràpid +Name[cs]=Dok pro rychlé nastavení Name[es]=Panel de ajustes rápidos Name[gl]=Doca de configuración rápida Name[it]=Area di aggancio delle impostazioni rapide Name[nl]=Docker voor snelle instellingen +Name[pl]=Dok szybkich ustawień Name[pt]=Área de Configuração Rápida Name[sv]=Dockningspanel med snabbinställningar Name[uk]=Панель швидких параметрів Name[x-test]=xxQuick Settings Dockerxx +Name[zh_CN]=快速设置坞窗 Comment=A Python-based docker for quickly changing brush size and opacity. Comment[ca]=Un acoblador basant en el Python per canviar ràpidament la mida del pinzell i l'opacitat. Comment[ca@valencia]=Un acoblador basant en el Python per canviar ràpidament la mida del pinzell i l'opacitat. Comment[es]=Un panel basado en Python para cambiar rápidamente el tamaño y la opacidad del pincel. Comment[gl]=Unha doca escrita en Python para cambiar rapidamente a opacidade e o tamaño dos pinceis. Comment[it]=Un'area di aggancio basata su Python per cambiare rapidamente la dimensione del pennello e l'opacità. Comment[nl]=Een op Python gebaseerde docker voor snel wijzigen van penseelgrootte en dekking. +Comment[pl]=Dok oparty na pythonie do szybkiej zmiany rozmiaru i nieprzezroczystości pędzla. Comment[pt]=Uma área acoplável em Python para mudar rapidamente o tamanho e opacidade do pincel. Comment[sv]=En Python-baserad dockningspanel för att snabbt ändra penselstorlek och ogenomskinlighet. Comment[uk]=Панель на основі мови програмування Python для швидкої зміни розміру та непрозорості пензля. Comment[x-test]=xxA Python-based docker for quickly changing brush size and opacity.xx +Comment[zh_CN]=基于 Python 的坞窗,用于快速更改笔刷尺寸和透明度。 diff --git a/plugins/extensions/pykrita/plugin/plugins/scriptdocker/kritapykrita_scriptdocker.desktop b/plugins/extensions/pykrita/plugin/plugins/scriptdocker/kritapykrita_scriptdocker.desktop index 8123cec731..f7dd9fe891 100644 --- a/plugins/extensions/pykrita/plugin/plugins/scriptdocker/kritapykrita_scriptdocker.desktop +++ b/plugins/extensions/pykrita/plugin/plugins/scriptdocker/kritapykrita_scriptdocker.desktop @@ -1,27 +1,32 @@ [Desktop Entry] Type=Service ServiceTypes=Krita/PythonPlugin X-KDE-Library=scriptdocker X-Python-2-Compatible=false Name=Script Docker Name[ca]=Acoblador de scripts Name[ca@valencia]=Acoblador de scripts +Name[cs]=Dok skriptu Name[es]=Panel de guiones Name[gl]=Doca de scripts Name[it]=Area di aggancio degli script Name[nl]=Vastzetten van scripts +Name[pl]=Dok skryptów Name[pt]=Área de Programas Name[sv]=Dockningsfönster för skript Name[uk]=Бічна панель скриптів Name[x-test]=xxScript Dockerxx +Name[zh_CN]=脚本坞窗 Comment=A Python-based docker for create actions and point to Python scripts Comment[ca]=Un acoblador basant en el Python per crear accions i apuntar a scripts del Python Comment[ca@valencia]=Un acoblador basant en el Python per crear accions i apuntar a scripts del Python Comment[es]=Un panel basado en Python para crear y apuntar a guiones de Python Comment[gl]=Unha doca escrita en Python para crear accións e apuntar a scripts escritos en Python. Comment[it]=Un'area di aggancio basata su Python per creare azioni e scegliere sctipt Python Comment[nl]=Een op Python gebaseerde vastzetter voor aanmaakacties en wijzen naar Python-scripts +Comment[pl]=Dok oparty na pythonie do tworzenia działań i punktów dla skryptów pythona Comment[pt]=Uma área acoplável, feita em Python, para criar acções e apontar para programas em Python Comment[sv]=Ett Python-baserat dockningsfönster för att skapa åtgärder och peka ut Python-skript Comment[uk]=Бічна панель для створення дій і керування скриптами на основі Python. Comment[x-test]=xxA Python-based docker for create actions and point to Python scriptsxx +Comment[zh_CN]=基于 Python 的坞窗,用于创建动作并指向 Python 脚本 diff --git a/plugins/extensions/pykrita/plugin/plugins/scripter/kritapykrita_scripter.desktop b/plugins/extensions/pykrita/plugin/plugins/scripter/kritapykrita_scripter.desktop index cd6a43dc76..309edaa0ba 100644 --- a/plugins/extensions/pykrita/plugin/plugins/scripter/kritapykrita_scripter.desktop +++ b/plugins/extensions/pykrita/plugin/plugins/scripter/kritapykrita_scripter.desktop @@ -1,34 +1,36 @@ [Desktop Entry] Type=Service ServiceTypes=Krita/PythonPlugin X-KDE-Library=scripter X-Python-2-Compatible=false Name=Scripter Name[ca]=Scripter Name[ca@valencia]=Scripter Name[de]=Scripter Name[en_GB]=Scripter Name[es]=Guionador Name[gl]=Executor de scripts Name[it]=Scripter Name[nl]=Scriptschrijver Name[pl]=Skrypter Name[pt]=Programador Name[sv]=Skriptgenerator Name[tr]=Betik yazarı Name[uk]=Скриптер Name[x-test]=xxScripterxx +Name[zh_CN]=脚本编写者 Comment=Plugin to execute ad-hoc Python code Comment[ca]=Connector per executar codi Python ad-hoc Comment[ca@valencia]=Connector per executar codi Python ad-hoc Comment[en_GB]=Plugin to execute ad-hoc Python code Comment[es]=Complemento para ejecutar código Python a medida Comment[gl]=Complemento para executar código de Python escrito no momento. Comment[it]=Estensione per eseguire ad-hoc codice Python Comment[nl]=Plug-in om ad-hoc Python code uit te voeren Comment[pl]=Wtyczka do wykonywania kodu Pythona ad-hoc Comment[pt]='Plugin' para executar código em Python arbitrário Comment[sv]=Insticksprogram för att köra godtycklig Python-kod Comment[tr]=Geçici Python kodu çalıştırmak için eklenti Comment[uk]=Додаток для виконання апріорного коду Python Comment[x-test]=xxPlugin to execute ad-hoc Python codexx +Comment[zh_CN]=执行特定 Python 代码的插件 diff --git a/plugins/extensions/pykrita/plugin/plugins/selectionsbagdocker/kritapykrita_selectionsbagdocker.desktop b/plugins/extensions/pykrita/plugin/plugins/selectionsbagdocker/kritapykrita_selectionsbagdocker.desktop index 4869071a07..2d84762700 100644 --- a/plugins/extensions/pykrita/plugin/plugins/selectionsbagdocker/kritapykrita_selectionsbagdocker.desktop +++ b/plugins/extensions/pykrita/plugin/plugins/selectionsbagdocker/kritapykrita_selectionsbagdocker.desktop @@ -1,34 +1,36 @@ [Desktop Entry] Type=Service ServiceTypes=Krita/PythonPlugin X-KDE-Library=selectionsbagdocker X-Python-2-Compatible=false Name=Selections Bag Docker Name[ca]=Acoblador de bossa de seleccions Name[ca@valencia]=Acoblador de bossa de seleccions Name[en_GB]=Selections Bag Docker Name[es]=Panel de selecciones Name[gl]=Doca de bolsa das seleccións Name[it]=Area di raccolta selezioni Name[nl]=Docker van zak met selecties Name[pl]=Dok worka zaznaczeń Name[pt]=Área de Selecções Name[sv]=Dockningspanel med markeringspåse Name[tr]=Seçim Çantası İşçisi Name[uk]=Бічна панель позначеного Name[x-test]=xxSelections Bag Dockerxx +Name[zh_CN]=选区包坞窗 Comment=A docker that allow to store a list of selections Comment[ca]=Un acoblador que permet emmagatzemar una llista de seleccions Comment[ca@valencia]=Un acoblador que permet emmagatzemar una llista de seleccions Comment[cs]=Dok umožňující uložit seznam výběrů Comment[en_GB]=A docker that allow to store a list of selections Comment[es]=Un panel que permite guardar una lista de selecciones Comment[gl]=Unha doca que permite almacenar unha lista de seleccións. Comment[it]=Un'area di aggancio che consente di memorizzare un elenco di selezioni Comment[nl]=Een docker die een lijst met selecties kan opslaan Comment[pl]=Dok, który umożliwia przechowywanie listy zaznaczeń Comment[pt]=Uma área acoplável que permite guardar uma lista de selecções Comment[sv]=En dockningspanel som gör det möjligt att lagra en lista över markeringar Comment[tr]=Seçimlerin bir listesini saklamayı sağlayan işçi Comment[uk]=Бічна панель, на якій можна зберігати список позначеного Comment[x-test]=xxA docker that allow to store a list of selectionsxx +Comment[zh_CN]=存储选区列表的坞窗 diff --git a/plugins/extensions/pykrita/plugin/plugins/tenbrushes/kritapykrita_tenbrushes.desktop b/plugins/extensions/pykrita/plugin/plugins/tenbrushes/kritapykrita_tenbrushes.desktop index 044771174f..9928551910 100644 --- a/plugins/extensions/pykrita/plugin/plugins/tenbrushes/kritapykrita_tenbrushes.desktop +++ b/plugins/extensions/pykrita/plugin/plugins/tenbrushes/kritapykrita_tenbrushes.desktop @@ -1,34 +1,36 @@ [Desktop Entry] Type=Service ServiceTypes=Krita/PythonPlugin X-KDE-Library=tenbrushes X-Python-2-Compatible=false Name=Ten Brushes Name[ca]=Deu pinzells Name[ca@valencia]=Deu pinzells Name[cs]=Deset štětců Name[en_GB]=Ten Brushes Name[es]=Diez pinceles Name[gl]=Dez pinceis Name[it]=Dieci pennelli Name[nl]=Tien penselen Name[pl]=Dziesięć pędzli Name[pt]=Dez Pincéis Name[sv]=Tio penslar Name[tr]=On Fırça Name[uk]=Десять пензлів Name[x-test]=xxTen Brushesxx +Name[zh_CN]=十笔刷 Comment=Assign a preset to ctrl-1 to ctrl-0 Comment[ca]=Assigna una predefinició des de Ctrl-1 a Ctrl-0 Comment[ca@valencia]=Assigna una predefinició des de Ctrl-1 a Ctrl-0 Comment[en_GB]=Assign a preset to ctrl-1 to ctrl-0 Comment[es]=Asignar una preselección a Ctrl-1 hasta Ctrl-0 Comment[gl]=Asigne unha predefinición do Ctrl+1 ao Ctrl+0. Comment[it]=Assegna una preimpostazione per ctrl-1 a ctrl-0 Comment[nl]=Een voorinstelling toekennen aan Ctrl-1 tot Ctrl-0 Comment[pl]=Przypisz nastawę do ctrl-1 lub ctrl-0 Comment[pt]=Atribuir uma predefinição de Ctrl-1 a Ctrl-0 Comment[sv]=Tilldela en förinställning för Ctrl+1 till Ctrl+0 Comment[tr]= ctrl-1 ve ctrl-0 için ayar atama Comment[uk]=Прив’язування наборів налаштувань до скорочень від ctrl-1 до ctrl-0 Comment[x-test]=xxAssign a preset to ctrl-1 to ctrl-0xx +Comment[zh_CN]=将预设分配给 Ctrl+1 到 Ctrl+0 diff --git a/plugins/extensions/qmic/PluginSettings.cpp b/plugins/extensions/qmic/PluginSettings.cpp index a3c0c581cc..86d8390e94 100644 --- a/plugins/extensions/qmic/PluginSettings.cpp +++ b/plugins/extensions/qmic/PluginSettings.cpp @@ -1,103 +1,108 @@ /* * Copyright (c) 2017 Boudewijn Rempt * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; version 2 of the License. * * 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 Lesser General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "PluginSettings.h" #include #include #include #include #include #include #include "kis_config.h" PluginSettings::PluginSettings(QWidget *parent) : KisPreferenceSet(parent) { setupUi(this); fileRequester->setFileName(gmicQtPath()); fileRequester->setStartDir(QStandardPaths::writableLocation(QStandardPaths::HomeLocation)); } PluginSettings::~PluginSettings() { KisConfig().writeEntry("gmic_qt_plugin_path", fileRequester->fileName()); } QString PluginSettings::id() { return QString("qmicsettings"); } QString PluginSettings::name() { return header(); } QString PluginSettings::header() { return QString(i18n("G'Mic-Qt Integration")); } QIcon PluginSettings::icon() { return koIcon("gmic"); } QString PluginSettings::gmicQtPath() { - QString gmicqt("gmic_krita_qt"); + QString gmicqt = "gmic_krita_qt"; #ifdef Q_OS_WIN gmicqt += ".exe"; #endif - gmicqt = KisConfig().readEntry("gmic_qt_plugin_path", qApp->applicationDirPath() + "/" + gmicqt); - QFileInfo fi(gmicqt); - if (!fi.exists()) { + + QString gmicqt_path = KisConfig().readEntry("gmic_qt_plugin_path", qApp->applicationDirPath() + "/" + gmicqt); + + QFileInfo fi(gmicqt_path); + if (!fi.exists() || !fi.isFile()) { + QFileInfo fi2(qApp->applicationDirPath() + "/" + gmicqt); - if (fi2.exists()) { - gmicqt = qApp->applicationDirPath() + "/" + gmicqt; + + if (fi2.exists() && fi2.isFile()) { + gmicqt_path = fi2.canonicalFilePath(); } else { - gmicqt.clear(); + gmicqt_path.clear(); } } + return gmicqt; } void PluginSettings::savePreferences() const { KisConfig().writeEntry("gmic_qt_plugin_path", fileRequester->fileName()); Q_EMIT(settingsChanged()); } void PluginSettings::loadPreferences() { QString gmicqt("gmic_host_krita"); #ifdef Q_OS_WIN gmicqt += ".exe"; #endif fileRequester->setFileName(gmicQtPath()); } void PluginSettings::loadDefaultPreferences() { fileRequester->setFileName(gmicQtPath()); } diff --git a/plugins/extensions/qmic/QMic.cpp b/plugins/extensions/qmic/QMic.cpp index c214a151b3..fbedad9ca0 100644 --- a/plugins/extensions/qmic/QMic.cpp +++ b/plugins/extensions/qmic/QMic.cpp @@ -1,508 +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()) { + if (pluginPath.isEmpty() || (!QFileInfo(pluginPath).exists() && !QFileInfo(pluginPath).isFile())) { { 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) { 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 && 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" diff --git a/plugins/tools/selectiontools/kis_tool_select_rectangular.cc b/plugins/tools/selectiontools/kis_tool_select_rectangular.cc index 0688aa0683..a63559ab1b 100644 --- a/plugins/tools/selectiontools/kis_tool_select_rectangular.cc +++ b/plugins/tools/selectiontools/kis_tool_select_rectangular.cc @@ -1,92 +1,91 @@ /* * kis_tool_select_rectangular.cc -- part of Krita * * Copyright (c) 1999 Michael Koch * Copyright (c) 2001 John Califf * Copyright (c) 2002 Patrick Julien * Copyright (c) 2007 Sven Langkamp * 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. */ #include "kis_tool_select_rectangular.h" #include "kis_painter.h" #include #include "kis_selection_options.h" #include "kis_canvas2.h" #include "kis_pixel_selection.h" #include "kis_selection_tool_helper.h" #include "kis_shape_tool_helper.h" #include "KisViewManager.h" #include "kis_selection_manager.h" __KisToolSelectRectangularLocal::__KisToolSelectRectangularLocal(KoCanvasBase * canvas) : KisToolRectangleBase(canvas, KisToolRectangleBase::SELECT, KisCursor::load("tool_rectangular_selection_cursor.png", 6, 6)) { setObjectName("tool_select_rectangular"); } void __KisToolSelectRectangularLocal::finishRect(const QRectF& rect) { KisCanvas2 * kisCanvas = dynamic_cast(canvas()); if (!kisCanvas) return; KisSelectionToolHelper helper(kisCanvas, kundo2_i18n("Select Rectangle")); QRect rc(rect.normalized().toRect()); - helper.cropRectIfNeeded(&rc, selectionAction()); if (helper.tryDeselectCurrentSelection(pixelToView(rc), selectionAction())) { return; } if (helper.canShortcutToNoop(rc, selectionAction())) { return; } if (selectionMode() == PIXEL_SELECTION) { if (rc.isValid()) { KisPixelSelectionSP tmpSel = KisPixelSelectionSP(new KisPixelSelection()); tmpSel->select(rc); QPainterPath cache; cache.addRect(rc); tmpSel->setOutlineCache(cache); helper.selectPixelSelection(tmpSel, selectionAction()); } } else { QRectF documentRect = convertToPt(rc); helper.addSelectionShape(KisShapeToolHelper::createRectangleShape(documentRect)); } } KisToolSelectRectangular::KisToolSelectRectangular(KoCanvasBase *canvas): KisToolSelectBase<__KisToolSelectRectangularLocal>(canvas, i18n("Rectangular Selection")) { connect(&m_widgetHelper, &KisSelectionToolConfigWidgetHelper::selectionActionChanged, this, &KisToolSelectRectangular::setSelectionAction); } void KisToolSelectRectangular::setSelectionAction(int action) { changeSelectionAction(action); }