1 /*
2     This file is part of the KDE libraries
3     SPDX-FileCopyrightText: 2004 Antonio Larrosa <larrosa@kde.org>
4 
5     SPDX-License-Identifier: LGPL-2.0-or-later
6 */
7 
8 #include "kpixmapregionselectorwidget.h"
9 #include <QAction>
10 #include <QApplication>
11 #include <QColor>
12 #include <QCursor>
13 #include <QHBoxLayout>
14 #include <QImage>
15 #include <QLabel>
16 #include <QMenu>
17 #include <QMouseEvent>
18 #include <QPainter>
19 #include <QRubberBand>
20 #include <QVBoxLayout>
21 
22 class KPixmapRegionSelectorWidgetPrivate
23 {
24 public:
KPixmapRegionSelectorWidgetPrivate(KPixmapRegionSelectorWidget * qq)25     KPixmapRegionSelectorWidgetPrivate(KPixmapRegionSelectorWidget *qq)
26         : q(qq)
27     {
28     }
29 
30     KPixmapRegionSelectorWidget *const q;
31 
32     /**
33      * Recalculates the pixmap that is shown based on the current selected area,
34      * the original image, etc.
35      */
36     void updatePixmap();
37 
38     QRect calcSelectionRectangle(const QPoint &startPoint, const QPoint &endPoint);
39 
40     enum CursorState { None = 0, Resizing, Moving };
41     CursorState m_state;
42 
43     QPixmap m_unzoomedPixmap;
44     QPixmap m_originalPixmap;
45     QPixmap m_linedPixmap;
46     QRect m_selectedRegion;
47     QLabel *m_label;
48 
49     QPoint m_tempFirstClick;
50     double m_forcedAspectRatio;
51 
52     int m_maxWidth, m_maxHeight;
53     double m_zoomFactor;
54 
55     QRubberBand *m_rubberBand;
56 };
57 
KPixmapRegionSelectorWidget(QWidget * parent)58 KPixmapRegionSelectorWidget::KPixmapRegionSelectorWidget(QWidget *parent)
59     : QWidget(parent)
60     , d(new KPixmapRegionSelectorWidgetPrivate(this))
61 {
62     QHBoxLayout *hboxLayout = new QHBoxLayout(this);
63 
64     hboxLayout->addStretch();
65     QVBoxLayout *vboxLayout = new QVBoxLayout();
66     hboxLayout->addItem(vboxLayout);
67 
68     vboxLayout->addStretch();
69     d->m_label = new QLabel(this);
70     d->m_label->setAttribute(Qt::WA_NoSystemBackground, true); // setBackgroundMode( Qt::NoBackground );
71     d->m_label->installEventFilter(this);
72 
73     vboxLayout->addWidget(d->m_label);
74     vboxLayout->addStretch();
75 
76     hboxLayout->addStretch();
77 
78     d->m_forcedAspectRatio = 0;
79 
80     d->m_zoomFactor = 1.0;
81     d->m_rubberBand = new QRubberBand(QRubberBand::Rectangle, d->m_label);
82     d->m_rubberBand->hide();
83 }
84 
85 KPixmapRegionSelectorWidget::~KPixmapRegionSelectorWidget() = default;
86 
pixmap() const87 QPixmap KPixmapRegionSelectorWidget::pixmap() const
88 {
89     return d->m_unzoomedPixmap;
90 }
91 
setPixmap(const QPixmap & pixmap)92 void KPixmapRegionSelectorWidget::setPixmap(const QPixmap &pixmap)
93 {
94     Q_ASSERT(!pixmap.isNull()); // This class isn't designed to deal with null pixmaps.
95     d->m_originalPixmap = pixmap;
96     d->m_unzoomedPixmap = pixmap;
97     d->m_label->setPixmap(pixmap);
98     resetSelection();
99 }
100 
resetSelection()101 void KPixmapRegionSelectorWidget::resetSelection()
102 {
103     d->m_selectedRegion = d->m_originalPixmap.rect();
104     d->m_rubberBand->hide();
105     d->updatePixmap();
106 }
107 
selectedRegion() const108 QRect KPixmapRegionSelectorWidget::selectedRegion() const
109 {
110     return d->m_selectedRegion;
111 }
112 
setSelectedRegion(const QRect & rect)113 void KPixmapRegionSelectorWidget::setSelectedRegion(const QRect &rect)
114 {
115     if (!rect.isValid()) {
116         resetSelection();
117     } else {
118         d->m_selectedRegion = rect;
119         d->updatePixmap();
120     }
121 }
122 
updatePixmap()123 void KPixmapRegionSelectorWidgetPrivate::updatePixmap()
124 {
125     Q_ASSERT(!m_originalPixmap.isNull());
126     if (m_originalPixmap.isNull()) {
127         m_label->setPixmap(m_originalPixmap);
128         return;
129     }
130     if (m_selectedRegion.width() > m_originalPixmap.width()) {
131         m_selectedRegion.setWidth(m_originalPixmap.width());
132     }
133     if (m_selectedRegion.height() > m_originalPixmap.height()) {
134         m_selectedRegion.setHeight(m_originalPixmap.height());
135     }
136 
137     QPainter painter;
138     if (m_linedPixmap.isNull()) {
139         m_linedPixmap = m_originalPixmap;
140         QPainter p(&m_linedPixmap);
141         p.setCompositionMode(QPainter::CompositionMode_SourceAtop);
142         p.fillRect(m_linedPixmap.rect(), QColor(0, 0, 0, 100));
143     }
144 
145     QPixmap pixmap = m_linedPixmap;
146     painter.begin(&pixmap);
147     painter.drawPixmap(m_selectedRegion.topLeft(), m_originalPixmap, m_selectedRegion);
148 
149     painter.end();
150 
151     m_label->setPixmap(pixmap);
152 
153     qApp->sendPostedEvents(nullptr, QEvent::LayoutRequest);
154 
155     if (m_selectedRegion == m_originalPixmap.rect()) { // d->m_label->rect()) //### CHECK!
156         m_rubberBand->hide();
157     } else {
158         m_rubberBand->setGeometry(QRect(m_selectedRegion.topLeft(), m_selectedRegion.size()));
159 
160         /*        m_rubberBand->setGeometry(QRect(m_label -> mapToGlobal(m_selectedRegion.topLeft()),
161                                                 m_selectedRegion.size()));
162         */
163         if (m_state != None) {
164             m_rubberBand->show();
165         }
166     }
167 }
168 
createPopupMenu()169 QMenu *KPixmapRegionSelectorWidget::createPopupMenu()
170 {
171     QMenu *popup = new QMenu(this);
172     popup->setObjectName(QStringLiteral("PixmapRegionSelectorPopup"));
173     popup->addSection(tr("Image Operations", "@title:menu"));
174 
175     popup->addAction(QIcon::fromTheme(QStringLiteral("object-rotate-right")),
176                      tr("&Rotate Clockwise", "@action:inmenu"),
177                      this,
178                      &KPixmapRegionSelectorWidget::rotateClockwise);
179     popup->addAction(QIcon::fromTheme(QStringLiteral("object-rotate-left")),
180                      tr("Rotate &Counterclockwise", "@action:inmenu"),
181                      this,
182                      &KPixmapRegionSelectorWidget::rotateCounterclockwise);
183 
184     /*
185        I wonder if it would be appropriate to have here an "Open with..." option to
186        edit the image (antlarr)
187     */
188     return popup;
189 }
190 
rotate(RotateDirection direction)191 void KPixmapRegionSelectorWidget::rotate(RotateDirection direction)
192 {
193     int w = d->m_originalPixmap.width();
194     int h = d->m_originalPixmap.height();
195     QImage img = d->m_unzoomedPixmap.toImage();
196     if (direction == Rotate90) {
197         img = img.transformed(QTransform().rotate(90.0));
198     } else if (direction == Rotate180) {
199         img = img.transformed(QTransform().rotate(180.0));
200     } else {
201         img = img.transformed(QTransform().rotate(270.0));
202     }
203 
204     d->m_unzoomedPixmap = QPixmap::fromImage(img);
205 
206     img = d->m_originalPixmap.toImage();
207     if (direction == Rotate90) {
208         img = img.transformed(QTransform().rotate(90.0));
209     } else if (direction == Rotate180) {
210         img = img.transformed(QTransform().rotate(180.0));
211     } else {
212         img = img.transformed(QTransform().rotate(270.0));
213     }
214 
215     d->m_originalPixmap = QPixmap::fromImage(img);
216 
217     d->m_linedPixmap = QPixmap();
218 
219     if (d->m_forcedAspectRatio > 0 && d->m_forcedAspectRatio != 1) {
220         resetSelection();
221     } else {
222         switch (direction) {
223         case (Rotate90): {
224             int x = h - d->m_selectedRegion.y() - d->m_selectedRegion.height();
225             int y = d->m_selectedRegion.x();
226             d->m_selectedRegion.setRect(x, y, d->m_selectedRegion.height(), d->m_selectedRegion.width());
227             d->updatePixmap();
228             //              qApp->sendPostedEvents(0,QEvent::LayoutRequest);
229             //              updatePixmap();
230 
231         } break;
232         case (Rotate270): {
233             int x = d->m_selectedRegion.y();
234             int y = w - d->m_selectedRegion.x() - d->m_selectedRegion.width();
235             d->m_selectedRegion.setRect(x, y, d->m_selectedRegion.height(), d->m_selectedRegion.width());
236             d->updatePixmap();
237             //              qApp->sendPostedEvents(0,QEvent::LayoutRequest);
238             //              updatePixmap();
239         } break;
240         default:
241             resetSelection();
242         }
243     }
244 
245     Q_EMIT pixmapRotated();
246 }
247 
rotateClockwise()248 void KPixmapRegionSelectorWidget::rotateClockwise()
249 {
250     rotate(Rotate90);
251 }
252 
rotateCounterclockwise()253 void KPixmapRegionSelectorWidget::rotateCounterclockwise()
254 {
255     rotate(Rotate270);
256 }
257 
eventFilter(QObject * obj,QEvent * ev)258 bool KPixmapRegionSelectorWidget::eventFilter(QObject *obj, QEvent *ev)
259 {
260     if (ev->type() == QEvent::MouseButtonPress) {
261         QMouseEvent *mev = (QMouseEvent *)(ev);
262         // qCDebug(KWidgetsAddonsLog) << QString("click at  %1,%2").arg( mev->x() ).arg( mev->y() );
263 
264         if (mev->button() == Qt::RightButton) {
265             QMenu *popup = createPopupMenu();
266             popup->exec(mev->globalPos());
267             delete popup;
268             return true;
269         }
270 
271         QCursor cursor;
272 
273         if (d->m_selectedRegion.contains(mev->pos()) && d->m_selectedRegion != d->m_originalPixmap.rect()) {
274             d->m_state = KPixmapRegionSelectorWidgetPrivate::Moving;
275             cursor.setShape(Qt::SizeAllCursor);
276             d->m_rubberBand->show();
277         } else {
278             d->m_state = KPixmapRegionSelectorWidgetPrivate::Resizing;
279             cursor.setShape(Qt::CrossCursor);
280         }
281         QApplication::setOverrideCursor(cursor);
282 
283         d->m_tempFirstClick = mev->pos();
284 
285         return true;
286     }
287 
288     if (ev->type() == QEvent::MouseMove) {
289         QMouseEvent *mev = (QMouseEvent *)(ev);
290 
291         // qCDebug(KWidgetsAddonsLog) << QString("move to  %1,%2").arg( mev->x() ).arg( mev->y() );
292 
293         if (d->m_state == KPixmapRegionSelectorWidgetPrivate::Resizing) {
294             setSelectedRegion(d->calcSelectionRectangle(d->m_tempFirstClick, mev->pos()));
295         } else if (d->m_state == KPixmapRegionSelectorWidgetPrivate::Moving) {
296             int mevx = mev->x();
297             int mevy = mev->y();
298             bool mouseOutside = false;
299             if (mevx < 0) {
300                 d->m_selectedRegion.translate(-d->m_selectedRegion.x(), 0);
301                 mouseOutside = true;
302             } else if (mevx > d->m_originalPixmap.width()) {
303                 d->m_selectedRegion.translate(d->m_originalPixmap.width() - d->m_selectedRegion.width() - d->m_selectedRegion.x(), 0);
304                 mouseOutside = true;
305             }
306             if (mevy < 0) {
307                 d->m_selectedRegion.translate(0, -d->m_selectedRegion.y());
308                 mouseOutside = true;
309             } else if (mevy > d->m_originalPixmap.height()) {
310                 d->m_selectedRegion.translate(0, d->m_originalPixmap.height() - d->m_selectedRegion.height() - d->m_selectedRegion.y());
311                 mouseOutside = true;
312             }
313             if (mouseOutside) {
314                 d->updatePixmap();
315                 return true;
316             };
317 
318             d->m_selectedRegion.translate(mev->x() - d->m_tempFirstClick.x(), //
319                                           mev->y() - d->m_tempFirstClick.y());
320 
321             // Check that the region has not fallen outside the image
322             if (d->m_selectedRegion.x() < 0) {
323                 d->m_selectedRegion.translate(-d->m_selectedRegion.x(), 0);
324             } else if (d->m_selectedRegion.right() > d->m_originalPixmap.width()) {
325                 d->m_selectedRegion.translate(-(d->m_selectedRegion.right() - d->m_originalPixmap.width()), 0);
326             }
327 
328             if (d->m_selectedRegion.y() < 0) {
329                 d->m_selectedRegion.translate(0, -d->m_selectedRegion.y());
330             } else if (d->m_selectedRegion.bottom() > d->m_originalPixmap.height()) {
331                 d->m_selectedRegion.translate(0, -(d->m_selectedRegion.bottom() - d->m_originalPixmap.height()));
332             }
333 
334             d->m_tempFirstClick = mev->pos();
335             d->updatePixmap();
336         }
337         return true;
338     }
339 
340     if (ev->type() == QEvent::MouseButtonRelease) {
341         QMouseEvent *mev = (QMouseEvent *)(ev);
342 
343         if (d->m_state == KPixmapRegionSelectorWidgetPrivate::Resizing && mev->pos() == d->m_tempFirstClick) {
344             resetSelection();
345         }
346 
347         d->m_state = KPixmapRegionSelectorWidgetPrivate::None;
348         QApplication::restoreOverrideCursor();
349         d->m_rubberBand->hide();
350         return true;
351     }
352 
353     QWidget::eventFilter(obj, ev);
354     return false;
355 }
356 
calcSelectionRectangle(const QPoint & startPoint,const QPoint & _endPoint)357 QRect KPixmapRegionSelectorWidgetPrivate::calcSelectionRectangle(const QPoint &startPoint, const QPoint &_endPoint)
358 {
359     QPoint endPoint = _endPoint;
360     if (endPoint.x() < 0) {
361         endPoint.setX(0);
362     } else if (endPoint.x() > m_originalPixmap.width()) {
363         endPoint.setX(m_originalPixmap.width());
364     }
365     if (endPoint.y() < 0) {
366         endPoint.setY(0);
367     } else if (endPoint.y() > m_originalPixmap.height()) {
368         endPoint.setY(m_originalPixmap.height());
369     }
370     int w = abs(startPoint.x() - endPoint.x());
371     int h = abs(startPoint.y() - endPoint.y());
372 
373     if (m_forcedAspectRatio > 0) {
374         double aspectRatio = w / double(h);
375 
376         if (aspectRatio > m_forcedAspectRatio) {
377             h = (int)(w / m_forcedAspectRatio);
378         } else {
379             w = (int)(h * m_forcedAspectRatio);
380         }
381     }
382 
383     int x;
384     int y;
385     if (startPoint.x() < endPoint.x()) {
386         x = startPoint.x();
387     } else {
388         x = startPoint.x() - w;
389     }
390 
391     if (startPoint.y() < endPoint.y()) {
392         y = startPoint.y();
393     } else {
394         y = startPoint.y() - h;
395     }
396 
397     if (x < 0) {
398         w += x;
399         x = 0;
400         h = (int)(w / m_forcedAspectRatio);
401 
402         if (startPoint.y() > endPoint.y()) {
403             y = startPoint.y() - h;
404         }
405     } else if (x + w > m_originalPixmap.width()) {
406         w = m_originalPixmap.width() - x;
407         h = (int)(w / m_forcedAspectRatio);
408 
409         if (startPoint.y() > endPoint.y()) {
410             y = startPoint.y() - h;
411         }
412     }
413 
414     if (y < 0) {
415         h += y;
416         y = 0;
417         w = (int)(h * m_forcedAspectRatio);
418 
419         if (startPoint.x() > endPoint.x()) {
420             x = startPoint.x() - w;
421         }
422     } else if (y + h > m_originalPixmap.height()) {
423         h = m_originalPixmap.height() - y;
424         w = (int)(h * m_forcedAspectRatio);
425 
426         if (startPoint.x() > endPoint.x()) {
427             x = startPoint.x() - w;
428         }
429     }
430 
431     return QRect(x, y, w, h);
432 }
433 
unzoomedSelectedRegion() const434 QRect KPixmapRegionSelectorWidget::unzoomedSelectedRegion() const
435 {
436     return QRect((int)(d->m_selectedRegion.x() / d->m_zoomFactor),
437                  (int)(d->m_selectedRegion.y() / d->m_zoomFactor),
438                  (int)(d->m_selectedRegion.width() / d->m_zoomFactor),
439                  (int)(d->m_selectedRegion.height() / d->m_zoomFactor));
440 }
441 
selectedImage() const442 QImage KPixmapRegionSelectorWidget::selectedImage() const
443 {
444     QImage origImage = d->m_unzoomedPixmap.toImage();
445     return origImage.copy(unzoomedSelectedRegion());
446 }
447 
setSelectionAspectRatio(int width,int height)448 void KPixmapRegionSelectorWidget::setSelectionAspectRatio(int width, int height)
449 {
450     d->m_forcedAspectRatio = width / double(height);
451 }
452 
setFreeSelectionAspectRatio()453 void KPixmapRegionSelectorWidget::setFreeSelectionAspectRatio()
454 {
455     d->m_forcedAspectRatio = 0;
456 }
457 
setMaximumWidgetSize(int width,int height)458 void KPixmapRegionSelectorWidget::setMaximumWidgetSize(int width, int height)
459 {
460     d->m_maxWidth = width;
461     d->m_maxHeight = height;
462 
463     if (d->m_selectedRegion == d->m_originalPixmap.rect()) {
464         d->m_selectedRegion = QRect();
465     }
466     d->m_originalPixmap = d->m_unzoomedPixmap;
467 
468     //   qCDebug(KWidgetsAddonsLog) << QString(" original Pixmap :") << d->m_originalPixmap.rect();
469     //   qCDebug(KWidgetsAddonsLog) << QString(" unzoomed Pixmap : %1 x %2 ").arg(d->m_unzoomedPixmap.width()).arg(d->m_unzoomedPixmap.height());
470 
471     if (!d->m_originalPixmap.isNull() && (d->m_originalPixmap.width() > d->m_maxWidth || d->m_originalPixmap.height() > d->m_maxHeight)) {
472         /* We have to resize the pixmap to get it complete on the screen */
473         QImage image = d->m_originalPixmap.toImage();
474         d->m_originalPixmap = QPixmap::fromImage(image.scaled(width, height, Qt::KeepAspectRatio, Qt::SmoothTransformation));
475         double oldZoomFactor = d->m_zoomFactor;
476         d->m_zoomFactor = d->m_originalPixmap.width() / (double)d->m_unzoomedPixmap.width();
477 
478         if (d->m_selectedRegion.isValid()) {
479             d->m_selectedRegion = QRect((int)(d->m_selectedRegion.x() * d->m_zoomFactor / oldZoomFactor),
480                                         (int)(d->m_selectedRegion.y() * d->m_zoomFactor / oldZoomFactor),
481                                         (int)(d->m_selectedRegion.width() * d->m_zoomFactor / oldZoomFactor),
482                                         (int)(d->m_selectedRegion.height() * d->m_zoomFactor / oldZoomFactor));
483         }
484     }
485 
486     if (!d->m_selectedRegion.isValid()) {
487         d->m_selectedRegion = d->m_originalPixmap.rect();
488     }
489 
490     d->m_linedPixmap = QPixmap();
491     d->updatePixmap();
492     resize(d->m_label->width(), d->m_label->height());
493 }
494