File indexing completed on 2024-06-02 04:17:33

0001 /* ============================================================
0002  *
0003  * This file is a part of digiKam project
0004  * https://www.digikam.org
0005  *
0006  * Date        : 2008-05-19
0007  * Description : a widget to draw sketch.
0008  *
0009  * SPDX-FileCopyrightText: 2008-2024 by Gilles Caulier <caulier dot gilles at gmail dot com>
0010  * SPDX-FileCopyrightText: 2008-2010 by Marcel Wiesweg <marcel dot wiesweg at gmx dot de>
0011  *
0012  * SPDX-License-Identifier: GPL-2.0-or-later
0013  *
0014  * ============================================================ */
0015 
0016 #include "sketchwidget.h"
0017 
0018 // Qt includes
0019 
0020 #include <QCursor>
0021 #include <QMap>
0022 #include <QPainter>
0023 #include <QPainterPath>
0024 #include <QColor>
0025 #include <QPixmap>
0026 #include <QPoint>
0027 #include <QMouseEvent>
0028 #include <QTime>
0029 
0030 // KDE includes
0031 
0032 #include <klocalizedstring.h>
0033 
0034 namespace Digikam
0035 {
0036 
0037 class Q_DECL_HIDDEN DrawEvent
0038 {
0039 public:
0040 
0041     DrawEvent()
0042       : penWidth(10),
0043         penColor(Qt::black)
0044     {
0045     }
0046 
0047     DrawEvent(int width, const QColor& color)
0048       : penWidth(width),
0049         penColor(color)
0050     {
0051     }
0052 
0053     void lineTo(const QPoint& pos)
0054     {
0055         path.lineTo(pos);
0056     }
0057 
0058 public:
0059 
0060     int          penWidth;
0061     QColor       penColor;
0062     QPainterPath path;
0063 };
0064 
0065 // ------------------------------------------------------------------------------
0066 
0067 class Q_DECL_HIDDEN SketchWidget::Private
0068 {
0069 public:
0070 
0071     explicit Private()
0072       : isClear     (true),
0073         drawing     (false),
0074         penWidth    (10),
0075         eventIndex  (-1),
0076         penColor    (Qt::black),
0077         pixmap      (QPixmap(256, 256))
0078     {
0079     }
0080 
0081     void startDrawEvent(const QPoint& pos)
0082     {
0083         // Remove all draw events from history map which are upper than current index.
0084         // If user redoes actions and makes new draw events, these will be queued at
0085         // end of history and will replace removed items.
0086 
0087         for (int i = (drawEventList.count() - 1) ; i > eventIndex ; --i)
0088         {
0089             drawEventList.removeAt(i);
0090         }
0091 
0092         drawEventCreationTime = QTime::currentTime();
0093         DrawEvent event(penWidth, penColor);
0094         event.path.moveTo(pos);
0095         drawEventList << event;
0096 
0097         eventIndex = drawEventList.count() - 1;
0098     }
0099 
0100     DrawEvent& currentDrawEvent()
0101     {
0102         QTime currentTime = QTime::currentTime();
0103 
0104         if (drawEventCreationTime.isNull() || (drawEventCreationTime.msecsTo(currentTime) > 1000))
0105         {
0106             drawEventCreationTime = currentTime;
0107             DrawEvent event(penWidth, penColor);
0108             event.path.moveTo(drawEventList.last().path.currentPosition());
0109             drawEventList << event;
0110             ++eventIndex;
0111         }
0112 
0113         return drawEventList.last();
0114     }
0115 
0116     void ensureNewDrawEvent()
0117     {
0118         drawEventCreationTime = QTime();
0119     }
0120 
0121 public:
0122 
0123     bool             isClear;
0124     bool             drawing;
0125 
0126     int              penWidth;
0127     int              eventIndex;
0128 
0129     QColor           penColor;
0130 
0131     QPixmap          pixmap;
0132 
0133     QPoint           lastPoint;
0134     QTime            drawEventCreationTime;
0135 
0136     QCursor          drawCursor;
0137 
0138     QList<DrawEvent> drawEventList;
0139 };
0140 
0141 SketchWidget::SketchWidget(QWidget* const parent)
0142     : QWidget(parent),
0143       d(new Private)
0144 {
0145     setWhatsThis(i18n("You simply draw here a rough sketch of what you want to find "
0146                       "and digiKam will displays the best matches in thumbnail view."));
0147 
0148     setAttribute(Qt::WA_StaticContents);
0149     setMouseTracking(true);
0150     setFixedSize(256, 256);
0151     setFocusPolicy(Qt::StrongFocus);
0152     slotClear();
0153 }
0154 
0155 SketchWidget::~SketchWidget()
0156 {
0157     delete d;
0158 }
0159 
0160 void SketchWidget::slotClear()
0161 {
0162     d->isClear    = true;
0163     d->eventIndex = -1;
0164     d->pixmap.fill(qRgb(255, 255, 255));
0165     d->drawEventList.clear();
0166     update();
0167 
0168     Q_EMIT signalUndoRedoStateChanged(false, false);
0169 }
0170 
0171 bool SketchWidget::isClear() const
0172 {
0173     return d->isClear;
0174 }
0175 
0176 void SketchWidget::setPenColor(const QColor& newColor)
0177 {
0178     d->penColor = newColor;
0179     d->ensureNewDrawEvent();
0180 }
0181 
0182 QColor SketchWidget::penColor() const
0183 {
0184     return d->penColor;
0185 }
0186 
0187 void SketchWidget::setPenWidth(int newWidth)
0188 {
0189     d->penWidth = newWidth;
0190     updateDrawCursor();
0191     d->ensureNewDrawEvent();
0192 }
0193 
0194 int SketchWidget::penWidth() const
0195 {
0196     return d->penWidth;
0197 }
0198 
0199 void SketchWidget::slotUndo()
0200 {
0201     if (d->eventIndex == -1)
0202     {
0203         return;
0204     }
0205 
0206     d->eventIndex--;
0207 
0208     // cppcheck-suppress knownConditionTrueFalse
0209     if (d->eventIndex == -1)
0210     {
0211         d->isClear = true;
0212         d->pixmap.fill(qRgb(255, 255, 255));
0213         update();
0214         Q_EMIT signalUndoRedoStateChanged(false, true);
0215     }
0216     else
0217     {
0218         replayEvents(d->eventIndex);
0219 
0220         Q_EMIT signalSketchChanged(sketchImage());
0221 
0222         Q_EMIT signalUndoRedoStateChanged(
0223                                         // cppcheck-suppress knownConditionTrueFalse
0224                                         (d->eventIndex != -1),
0225                                         (d->eventIndex != (d->drawEventList.count() - 1))
0226                                        );
0227     }
0228 }
0229 
0230 void SketchWidget::slotRedo()
0231 {
0232     if (d->eventIndex == (d->drawEventList.count() - 1))
0233     {
0234         return;
0235     }
0236 
0237     d->eventIndex++;
0238     d->isClear = false;
0239     replayEvents(d->eventIndex);
0240 
0241     Q_EMIT signalSketchChanged(sketchImage());
0242 
0243     Q_EMIT signalUndoRedoStateChanged(
0244                                     (d->eventIndex != -1),
0245                                     // cppcheck-suppress knownConditionTrueFalse
0246                                     (d->eventIndex != (d->drawEventList.count() - 1))
0247                                    );
0248 }
0249 
0250 void SketchWidget::replayEvents(int index)
0251 {
0252     d->pixmap.fill(qRgb(255, 255, 255));
0253 
0254     for (int i = 0 ; i <= index ; ++i)
0255     {
0256         const DrawEvent& drawEvent = d->drawEventList.at(i);
0257         drawPath(drawEvent.penWidth, drawEvent.penColor, drawEvent.path);
0258     }
0259 
0260     update();
0261 }
0262 
0263 void SketchWidget::sketchImageToXML(QXmlStreamWriter& writer)
0264 {
0265     writer.writeStartElement(QLatin1String("SketchImage"));
0266 
0267     for (int i = 0 ; i <= d->eventIndex ; ++i)
0268     {
0269         const DrawEvent& event = d->drawEventList.at(i);
0270 
0271         // Write the pen size and color
0272 
0273         writer.writeStartElement(QLatin1String("Path"));
0274         writer.writeAttribute(QLatin1String("Size"), QString::number(event.penWidth));
0275         writer.writeAttribute(QLatin1String("Color"), event.penColor.name());
0276 
0277         // Write the lines contained in the QPainterPath
0278 
0279         // Initial position is 0,0
0280 
0281         QPointF pos(0, 0);
0282 
0283         for (int j = 0 ; j < event.path.elementCount() ; ++j)
0284         {
0285             const QPainterPath::Element& element = event.path.elementAt(j);
0286 
0287             // Store begin and end point of a line, so no need to write moveTo elements to XML
0288 
0289             if (element.isLineTo())
0290             {
0291                 QPoint begin = pos.toPoint();
0292                 QPoint end   = ((QPointF)element).toPoint();
0293                 writer.writeStartElement(QLatin1String("Line"));
0294                 writer.writeAttribute(QLatin1String("x1"), QString::number(begin.x()));
0295                 writer.writeAttribute(QLatin1String("y1"), QString::number(begin.y()));
0296                 writer.writeAttribute(QLatin1String("x2"), QString::number(end.x()));
0297                 writer.writeAttribute(QLatin1String("y2"), QString::number(end.y()));
0298                 writer.writeEndElement();
0299             }
0300 
0301             // Keep track of current position after this element
0302             // The starting point of the next element is the end point of this element
0303             // This handles both lineTo and moveTo elements
0304 
0305             pos = element;
0306         }
0307 
0308         writer.writeEndElement();
0309     }
0310 
0311     writer.writeEndElement();
0312 }
0313 
0314 QString SketchWidget::sketchImageToXML()
0315 {
0316     QString xml;
0317     QXmlStreamWriter writer(&xml);
0318     writer.writeStartDocument();
0319     sketchImageToXML(writer);
0320     writer.writeEndDocument();
0321 
0322     return xml;
0323 }
0324 
0325 bool SketchWidget::setSketchImageFromXML(const QString& xml)
0326 {
0327     QXmlStreamReader reader(xml);
0328     QXmlStreamReader::TokenType element;
0329 
0330     while (!reader.atEnd())
0331     {
0332         element = reader.readNext();
0333 
0334         if ((element == QXmlStreamReader::StartElement) &&
0335             (reader.name() == QLatin1String("SketchImage")))
0336         {
0337             return setSketchImageFromXML(reader);
0338         }
0339     }
0340 
0341     return false;
0342 }
0343 
0344 bool SketchWidget::setSketchImageFromXML(QXmlStreamReader& reader)
0345 {
0346     QXmlStreamReader::TokenType element;
0347 
0348     // We assume that the reader is positioned at the start element for our XML
0349 
0350     if (!reader.isStartElement() ||
0351         (reader.name() != QLatin1String("SketchImage")))
0352     {
0353         return false;
0354     }
0355 
0356     d->isClear = false;
0357 
0358     // rebuild list of drawing chunks
0359 
0360     d->drawEventList.clear();
0361 
0362     while (!reader.atEnd())
0363     {
0364         element = reader.readNext();
0365 
0366         if      (element == QXmlStreamReader::StartElement)
0367         {
0368             // every chunk (DrawEvent) is stored as a vector path
0369 
0370             if (reader.name() == QLatin1String("Path"))
0371             {
0372                 addPath(reader);    // recurse
0373             }
0374         }
0375         else if (element == QXmlStreamReader::EndElement)
0376         {
0377             // we have finished
0378 
0379             if (reader.name() == QLatin1String("SketchImage"))
0380             {
0381                 break;
0382             }
0383         }
0384     }
0385 
0386     // set current event to the last event
0387 
0388     d->eventIndex = d->drawEventList.count() - 1;
0389 
0390     // apply events to our pixmap
0391 
0392     replayEvents(d->eventIndex);
0393     Q_EMIT signalUndoRedoStateChanged(d->eventIndex != -1, false);
0394 
0395     return true;
0396 }
0397 
0398 void SketchWidget::addPath(QXmlStreamReader& reader)
0399 {
0400     QXmlStreamReader::TokenType element;
0401 
0402     DrawEvent event;
0403 
0404     // Retrieve pen color and size
0405 
0406     QStringView size  = reader.attributes().value(QLatin1String("Size"));
0407     QStringView color = reader.attributes().value(QLatin1String("Color"));
0408 
0409     if (!size.isEmpty())
0410     {
0411         event.penWidth = size.toString().toInt();
0412     }
0413 
0414     if (!color.isEmpty())
0415     {
0416         event.penColor.setNamedColor(color.toString());
0417     }
0418 
0419     QPointF begin(0, 0), end(0, 0);
0420 
0421     while (!reader.atEnd())
0422     {
0423         element = reader.readNext();
0424 
0425         if      (element == QXmlStreamReader::StartElement)
0426         {
0427             // The line element has four attributes, x1,y1,x2,y2
0428 
0429             if (reader.name() == QLatin1String("Line"))
0430             {
0431                 QStringView x1 = reader.attributes().value(QLatin1String("x1"));
0432                 QStringView y1 = reader.attributes().value(QLatin1String("y1"));
0433                 QStringView x2 = reader.attributes().value(QLatin1String("x2"));
0434                 QStringView y2 = reader.attributes().value(QLatin1String("y2"));
0435 
0436                 if (!x1.isEmpty() && !y1.isEmpty())
0437                 {
0438                     begin.setX(x1.toString().toInt());
0439                     begin.setY(y1.toString().toInt());
0440                 }
0441                 else
0442                 {
0443                     begin = end;
0444                 }
0445 
0446                 if (!x2.isEmpty() && !y2.isEmpty())
0447                 {
0448                     end.setX(x2.toString().toInt());
0449                     end.setY(y2.toString().toInt());
0450                 }
0451 
0452                 // move to starting point
0453 
0454                 event.path.moveTo(begin);
0455 
0456                 // draw line
0457 
0458                 event.path.lineTo(end);
0459             }
0460         }
0461         else if (element == QXmlStreamReader::EndElement)
0462         {
0463             // we have finished
0464 
0465             if (reader.name() == QLatin1String("Path"))
0466             {
0467                 break;
0468             }
0469         }
0470     }
0471 
0472     d->drawEventList << event;
0473 }
0474 
0475 QImage SketchWidget::sketchImage() const
0476 {
0477     return d->pixmap.toImage();
0478 }
0479 
0480 void SketchWidget::setSketchImage(const QImage& image)
0481 {
0482     d->isClear    = false;
0483     d->pixmap     = QPixmap::fromImage(image);
0484     d->eventIndex = -1;
0485     d->drawEventList.clear();
0486 
0487     Q_EMIT signalUndoRedoStateChanged(false, false);
0488 
0489     update();
0490 }
0491 
0492 void SketchWidget::mousePressEvent(QMouseEvent* e)
0493 {
0494     if (e->button() == Qt::LeftButton)
0495     {
0496         if (d->isClear)
0497         {
0498             d->pixmap.fill(qRgb(255, 255, 255));
0499             d->isClear = false;
0500             update();
0501         }
0502 
0503         // sample color
0504 
0505         if (e->modifiers() & Qt::CTRL)
0506         {
0507             QImage img = d->pixmap.toImage();
0508 
0509             Q_EMIT signalPenColorChanged((img.pixel(e->pos())));
0510 
0511             return;
0512         }
0513 
0514         d->lastPoint = e->pos();
0515         d->drawing   = true;
0516         setCursor(d->drawCursor);
0517 
0518         d->startDrawEvent(e->pos());
0519     }
0520 }
0521 
0522 void SketchWidget::mouseMoveEvent(QMouseEvent* e)
0523 {
0524 
0525 #if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
0526 
0527     if (rect().contains(e->position().toPoint().x(), e->position().toPoint().y()))
0528 
0529 #else
0530 
0531     if (rect().contains(e->x(), e->y()))
0532 
0533 #endif
0534 
0535     {
0536         setFocus();
0537 
0538         if (d->drawing || !(e->modifiers() & Qt::CTRL))
0539         {
0540             setCursor(d->drawCursor);
0541         }
0542         else
0543         {
0544             setCursor(Qt::CrossCursor);
0545         }
0546 
0547         if ((e->buttons() & Qt::LeftButton))
0548         {
0549             QPoint currentPos = e->pos();
0550             d->currentDrawEvent().lineTo(currentPos);
0551             drawLineTo(currentPos);
0552         }
0553     }
0554     else
0555     {
0556         unsetCursor();
0557         clearFocus();
0558     }
0559 }
0560 
0561 void SketchWidget::wheelEvent(QWheelEvent* e)
0562 {
0563     int x = e->position().toPoint().x();
0564     int y = e->position().toPoint().y();
0565 
0566     if (rect().contains(x, y))
0567     {
0568         int size = d->penWidth;
0569         int decr = (e->modifiers() & Qt::SHIFT) ? 1 : 10;
0570 
0571         if      (e->angleDelta().y() > 0)
0572         {
0573             size += decr;
0574         }
0575         else if (e->angleDelta().y() < 0)
0576         {
0577             size -= decr;
0578         }
0579 
0580         Q_EMIT signalPenSizeChanged(size);
0581         setCursor(d->drawCursor);
0582     }
0583 }
0584 
0585 void SketchWidget::mouseReleaseEvent(QMouseEvent* e)
0586 {
0587     if ((e->button() == Qt::LeftButton) && d->drawing)
0588     {
0589         QPoint currentPos = e->pos();
0590         d->currentDrawEvent().lineTo(currentPos);
0591         d->drawing        = false;
0592         Q_EMIT signalSketchChanged(sketchImage());
0593         Q_EMIT signalUndoRedoStateChanged(true, false);
0594     }
0595 }
0596 
0597 void SketchWidget::keyPressEvent(QKeyEvent* e)
0598 {
0599     QWidget::keyPressEvent(e);
0600 
0601     if (e->modifiers() == Qt::CTRL)
0602     {
0603         setCursor(Qt::CrossCursor);
0604     }
0605 }
0606 
0607 void SketchWidget::keyReleaseEvent(QKeyEvent* e)
0608 {
0609     QWidget::keyReleaseEvent(e);
0610 
0611     if (e->key() == Qt::Key_Control)
0612     {
0613         setCursor(d->drawCursor);
0614     }
0615 }
0616 
0617 void SketchWidget::paintEvent(QPaintEvent*)
0618 {
0619     QPainter p(this);
0620 
0621     if (d->isClear)
0622     {
0623         p.drawText(0, 0, width(), height(), Qt::AlignCenter,
0624                    i18n("Draw a sketch here\nto perform a\nFuzzy search"));
0625     }
0626     else
0627     {
0628         p.drawPixmap(QPoint(0, 0), d->pixmap);
0629     }
0630 }
0631 
0632 void SketchWidget::drawLineTo(const QPoint& endPoint)
0633 {
0634     drawLineTo(d->penWidth, d->penColor, d->lastPoint, endPoint);
0635 }
0636 
0637 void SketchWidget::drawLineTo(int width, const QColor& color, const QPoint& start, const QPoint& end)
0638 {
0639     QPainter painter(&d->pixmap);
0640     painter.setPen(QPen(color, width, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin));
0641     painter.drawLine(start, end);
0642 
0643     int rad      = (width / 2) + 2;
0644 
0645     update(QRect(start, end).normalized().adjusted(-rad, -rad, +rad, +rad));
0646     d->lastPoint = end;
0647 }
0648 
0649 void SketchWidget::drawPath(int width, const QColor& color, const QPainterPath& path)
0650 {
0651     QPainter painter(&d->pixmap);
0652     painter.setPen(QPen(color, width, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin));
0653     painter.drawPath(path);
0654 
0655     update(path.boundingRect().toRect());
0656     d->lastPoint = path.currentPosition().toPoint();
0657 }
0658 
0659 void SketchWidget::updateDrawCursor()
0660 {
0661     int size = d->penWidth;
0662 
0663     if (size > 64)
0664     {
0665         size = 64;
0666     }
0667 
0668     if (size < 3)
0669     {
0670         size = 3;
0671     }
0672 
0673     QPixmap pix(size, size);
0674     pix.fill(Qt::transparent);
0675 
0676     QPainter p(&pix);
0677     p.setRenderHint(QPainter::Antialiasing, true);
0678     p.drawEllipse(1, 1, size - 2, size - 2);
0679 
0680     d->drawCursor = QCursor(pix);
0681 }
0682 
0683 } // namespace Digikam
0684 
0685 #include "moc_sketchwidget.cpp"