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