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"