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 { 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 | |
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, on: true); // setBackgroundMode( Qt::NoBackground ); |
71 | d->m_label->installEventFilter(filterObj: 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 | |
87 | QPixmap KPixmapRegionSelectorWidget::pixmap() const |
88 | { |
89 | return d->m_unzoomedPixmap; |
90 | } |
91 | |
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 | |
101 | void KPixmapRegionSelectorWidget::resetSelection() |
102 | { |
103 | d->m_selectedRegion = d->m_originalPixmap.rect(); |
104 | d->m_rubberBand->hide(); |
105 | d->updatePixmap(); |
106 | } |
107 | |
108 | QRect KPixmapRegionSelectorWidget::selectedRegion() const |
109 | { |
110 | return d->m_selectedRegion; |
111 | } |
112 | |
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 | |
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(), color: QColor(0, 0, 0, 100)); |
143 | } |
144 | |
145 | QPixmap pixmap = m_linedPixmap; |
146 | painter.begin(&pixmap); |
147 | painter.drawPixmap(p: m_selectedRegion.topLeft(), pm: m_originalPixmap, sr: m_selectedRegion); |
148 | |
149 | painter.end(); |
150 | |
151 | m_label->setPixmap(pixmap); |
152 | |
153 | qApp->sendPostedEvents(receiver: nullptr, event_type: 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 | |
169 | QMenu *KPixmapRegionSelectorWidget::() |
170 | { |
171 | QMenu * = new QMenu(this); |
172 | popup->setObjectName(QStringLiteral("PixmapRegionSelectorPopup" )); |
173 | popup->addSection(text: tr(s: "Image Operations" , c: "@title:menu" )); |
174 | |
175 | popup->addAction(icon: QIcon::fromTheme(QStringLiteral("object-rotate-right" )), |
176 | text: tr(s: "&Rotate Clockwise" , c: "@action:inmenu" ), |
177 | args: this, |
178 | args: &KPixmapRegionSelectorWidget::rotateClockwise); |
179 | popup->addAction(icon: QIcon::fromTheme(QStringLiteral("object-rotate-left" )), |
180 | text: tr(s: "Rotate &Counterclockwise" , c: "@action:inmenu" ), |
181 | args: this, |
182 | args: &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 | |
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(matrix: QTransform().rotate(a: 90.0)); |
198 | } else if (direction == Rotate180) { |
199 | img = img.transformed(matrix: QTransform().rotate(a: 180.0)); |
200 | } else { |
201 | img = img.transformed(matrix: QTransform().rotate(a: 270.0)); |
202 | } |
203 | |
204 | d->m_unzoomedPixmap = QPixmap::fromImage(image: img); |
205 | |
206 | img = d->m_originalPixmap.toImage(); |
207 | if (direction == Rotate90) { |
208 | img = img.transformed(matrix: QTransform().rotate(a: 90.0)); |
209 | } else if (direction == Rotate180) { |
210 | img = img.transformed(matrix: QTransform().rotate(a: 180.0)); |
211 | } else { |
212 | img = img.transformed(matrix: QTransform().rotate(a: 270.0)); |
213 | } |
214 | |
215 | d->m_originalPixmap = QPixmap::fromImage(image: 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(ax: x, ay: y, aw: d->m_selectedRegion.height(), ah: 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(ax: x, ay: y, aw: d->m_selectedRegion.height(), ah: 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 | |
248 | void KPixmapRegionSelectorWidget::rotateClockwise() |
249 | { |
250 | rotate(direction: Rotate90); |
251 | } |
252 | |
253 | void KPixmapRegionSelectorWidget::rotateCounterclockwise() |
254 | { |
255 | rotate(direction: Rotate270); |
256 | } |
257 | |
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 * = createPopupMenu(); |
266 | popup->exec(pos: mev->globalPosition().toPoint()); |
267 | delete popup; |
268 | return true; |
269 | } |
270 | |
271 | QCursor cursor; |
272 | |
273 | if (d->m_selectedRegion.contains(p: 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(startPoint: d->m_tempFirstClick, endPoint: mev->pos())); |
295 | } else if (d->m_state == KPixmapRegionSelectorWidgetPrivate::Moving) { |
296 | int mevx = mev->position().x(); |
297 | int mevy = mev->position().y(); |
298 | bool mouseOutside = false; |
299 | if (mevx < 0) { |
300 | d->m_selectedRegion.translate(dx: -d->m_selectedRegion.x(), dy: 0); |
301 | mouseOutside = true; |
302 | } else if (mevx > d->m_originalPixmap.width()) { |
303 | d->m_selectedRegion.translate(dx: d->m_originalPixmap.width() - d->m_selectedRegion.width() - d->m_selectedRegion.x(), dy: 0); |
304 | mouseOutside = true; |
305 | } |
306 | if (mevy < 0) { |
307 | d->m_selectedRegion.translate(dx: 0, dy: -d->m_selectedRegion.y()); |
308 | mouseOutside = true; |
309 | } else if (mevy > d->m_originalPixmap.height()) { |
310 | d->m_selectedRegion.translate(dx: 0, dy: 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(dx: mev->position().x() - d->m_tempFirstClick.x(), // |
319 | dy: mev->position().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(dx: -d->m_selectedRegion.x(), dy: 0); |
324 | } else if (d->m_selectedRegion.right() > d->m_originalPixmap.width()) { |
325 | d->m_selectedRegion.translate(dx: -(d->m_selectedRegion.right() - d->m_originalPixmap.width()), dy: 0); |
326 | } |
327 | |
328 | if (d->m_selectedRegion.y() < 0) { |
329 | d->m_selectedRegion.translate(dx: 0, dy: -d->m_selectedRegion.y()); |
330 | } else if (d->m_selectedRegion.bottom() > d->m_originalPixmap.height()) { |
331 | d->m_selectedRegion.translate(dx: 0, dy: -(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(watched: obj, event: ev); |
354 | return false; |
355 | } |
356 | |
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(x: startPoint.x() - endPoint.x()); |
371 | int h = abs(x: 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 | |
434 | 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 | |
442 | QImage KPixmapRegionSelectorWidget::selectedImage() const |
443 | { |
444 | QImage origImage = d->m_unzoomedPixmap.toImage(); |
445 | return origImage.copy(rect: unzoomedSelectedRegion()); |
446 | } |
447 | |
448 | void KPixmapRegionSelectorWidget::setSelectionAspectRatio(int width, int height) |
449 | { |
450 | d->m_forcedAspectRatio = width / double(height); |
451 | } |
452 | |
453 | void KPixmapRegionSelectorWidget::setFreeSelectionAspectRatio() |
454 | { |
455 | d->m_forcedAspectRatio = 0; |
456 | } |
457 | |
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: image.scaled(w: width, h: height, aspectMode: Qt::KeepAspectRatio, mode: 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(w: d->m_label->width(), h: d->m_label->height()); |
493 | } |
494 | |
495 | #include "moc_kpixmapregionselectorwidget.cpp" |
496 | |