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