Changeset View
Changeset View
Standalone View
Standalone View
src/QuickEditor/QuickEditor.cpp
Show All 15 Lines | |||||
16 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, | 16 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, | ||
17 | * Boston, MA 02110-1301, USA. | 17 | * Boston, MA 02110-1301, USA. | ||
18 | */ | 18 | */ | ||
19 | 19 | | |||
20 | #include "QuickEditor.h" | 20 | #include "QuickEditor.h" | ||
21 | 21 | | |||
22 | #include "SpectacleConfig.h" | 22 | #include "SpectacleConfig.h" | ||
23 | 23 | | |||
24 | #include <KDeclarative/KDeclarative> | 24 | const qreal QuickEditor::mouseAreaSize = 20.0; | ||
25 | #include <kdeclarative_version.h> | 25 | const qreal QuickEditor::cornerHandleRadius = 8.0; | ||
26 | const qreal QuickEditor::midHandleRadius = 5.0; | ||||
27 | const int QuickEditor::selectionSizeThreshold = 100; | ||||
28 | | ||||
29 | const int QuickEditor::selectionBoxPaddingX = 5; | ||||
30 | const int QuickEditor::selectionBoxPaddingY = 4; | ||||
31 | const int QuickEditor::selectionBoxMarginY = 2; | ||||
32 | | ||||
33 | const int QuickEditor::bottomHelpBoxPaddingX = 12; | ||||
34 | const int QuickEditor::bottomHelpBoxPaddingY = 8; | ||||
35 | const int QuickEditor::bottomHelpBoxPairSpacing = 6; | ||||
36 | const int QuickEditor::bottomHelpBoxLineHeight = 24; | ||||
26 | 37 | | |||
27 | #include <QPixmap> | 38 | const int QuickEditor::magZoom = 5; | ||
28 | #include <QQuickImageProvider> | 39 | const int QuickEditor::magPixels = 16; | ||
29 | #include <QQuickItem> | | |||
30 | #include <QQuickItemGrabResult> | | |||
31 | #include <QQuickView> | | |||
32 | #include <QSize> | | |||
33 | 40 | | |||
34 | struct QuickEditor::ImageStore : public QQuickImageProvider | 41 | QuickEditor::QuickEditor(const QPixmap& pixmap, QObject* parent) : | ||
42 | mMaskColour(QColor::fromRgbF(0, 0, 0, 0.15)), | ||||
43 | mStrokeColour(palette().highlight().color()), | ||||
44 | mCrossColour(QColor::fromRgbF(mStrokeColour.redF(), mStrokeColour.greenF(), mStrokeColour.blueF(), 0.7)), | ||||
45 | mLabelBackgroundColour(QColor::fromRgbF( | ||||
46 | palette().light().color().redF(), | ||||
47 | palette().light().color().greenF(), | ||||
48 | palette().light().color().blueF(), | ||||
49 | 0.85 | ||||
50 | )), | ||||
51 | mLabelForegroundColour(palette().windowText().color()), | ||||
52 | mMidHelpText(tr("Click and drag to draw a selection rectangle,\nor press Esc to quit")), | ||||
53 | mMidHelpTextFont(font()), | ||||
54 | mBottomHelpText({ | ||||
55 | {QStaticText(tr("Enter, double-click:")), QStaticText(tr("Take screenshot"))}, | ||||
56 | {QStaticText(tr("Shift:")), QStaticText(tr("Hold to toggle magnifier"))}, | ||||
57 | {QStaticText(tr("Right-click:")), QStaticText(tr("Reset selection"))}, | ||||
58 | {QStaticText(tr("Esc:")), QStaticText(tr("Cancel"))}, | ||||
59 | }), | ||||
60 | mBottomHelpTextFont(font()), | ||||
61 | mBottomHelpGridLeftWidth(0), | ||||
62 | mMouseDragState(MouseState::None), | ||||
63 | mPixmap(pixmap), | ||||
64 | mMagnifierAllowed(false), | ||||
65 | mShowMagnifier(SpectacleConfig::instance()->showMagnifierChecked()), | ||||
66 | mToggleMagnifier(false) | ||||
35 | { | 67 | { | ||
36 | ImageStore(const QPixmap &pixmap) : | 68 | Q_UNUSED(parent); | ||
37 | QQuickImageProvider(QQuickImageProvider::Pixmap), | 69 | | ||
38 | mPixmap(pixmap) | 70 | SpectacleConfig *config = SpectacleConfig::instance(); | ||
39 | {} | 71 | if (config->useLightRegionMaskColour()) { | ||
72 | mMaskColour = QColor(255, 255, 255, 100); | ||||
73 | } | ||||
74 | | ||||
75 | setMouseTracking(true); | ||||
76 | setAttribute(Qt::WA_StaticContents); | ||||
77 | setWindowFlags(Qt::WindowStaysOnTopHint); | ||||
78 | setAttribute(Qt::WA_TranslucentBackground); | ||||
79 | showFullScreen(); | ||||
80 | | ||||
81 | const qreal dprI = 1.0 / devicePixelRatioF(); | ||||
82 | setGeometry(0, 0, mPixmap.width() * dprI, mPixmap.height() * dprI); | ||||
83 | | ||||
84 | if (config->rememberLastRectangularRegion()) { | ||||
85 | QRect cropRegion = config->cropRegion(); | ||||
86 | if (!cropRegion.isEmpty()) { | ||||
87 | mSelection = QRectF( | ||||
88 | cropRegion.x() * dprI, | ||||
89 | cropRegion.y() * dprI, | ||||
90 | cropRegion.width() * dprI, | ||||
91 | cropRegion.height() * dprI | ||||
92 | ).intersected(geometry()); | ||||
93 | } | ||||
94 | setMouseCursor(QCursor::pos()); | ||||
95 | } else { | ||||
96 | setCursor(Qt::CrossCursor); | ||||
97 | } | ||||
98 | | ||||
99 | mMidHelpTextFont.setPointSize(12); | ||||
40 | 100 | | |||
41 | QPixmap requestPixmap(const QString &id, QSize *size, const QSize &requestedSize) Q_DECL_OVERRIDE | 101 | for (auto& pair : mBottomHelpText) { | ||
102 | for (auto item : { pair.first, pair.second }) { | ||||
103 | item.prepare(QTransform(), mBottomHelpTextFont); | ||||
104 | item.setPerformanceHint(QStaticText::AggressiveCaching); | ||||
105 | } | ||||
106 | } | ||||
107 | layoutBottomHelpText(); | ||||
108 | | ||||
109 | update(); | ||||
110 | } | ||||
111 | | ||||
112 | inline void QuickEditor::acceptSelection() | ||||
42 | { | 113 | { | ||
43 | Q_UNUSED(id); | 114 | if (!mSelection.isEmpty()) { | ||
115 | const qreal dpr = devicePixelRatioF(); | ||||
116 | QRect scaledCropRegion = QRect( | ||||
117 | qRound(mSelection.x() * dpr), | ||||
118 | qRound(mSelection.y() * dpr), | ||||
119 | qRound(mSelection.width() * dpr), | ||||
120 | qRound(mSelection.height() * dpr) | ||||
121 | ); | ||||
122 | SpectacleConfig::instance()->setCropRegion(scaledCropRegion); | ||||
123 | emit grabDone(mPixmap.copy(scaledCropRegion)); | ||||
124 | delete this; | ||||
125 | } | ||||
126 | } | ||||
44 | 127 | | |||
45 | if (size) { | 128 | void QuickEditor::keyPressEvent(QKeyEvent* event) | ||
46 | *size = mPixmap.size(); | 129 | { | ||
130 | switch(event->key()) { | ||||
131 | case Qt::Key_Escape: | ||||
132 | emit grabCancelled(); | ||||
133 | delete this; | ||||
134 | break; | ||||
135 | case Qt::Key_Return: | ||||
136 | case Qt::Key_Enter: | ||||
137 | acceptSelection(); | ||||
138 | default: | ||||
139 | break; | ||||
140 | } | ||||
141 | if (event->modifiers() & Qt::ShiftModifier) { | ||||
142 | mToggleMagnifier = true; | ||||
143 | update(); | ||||
144 | } | ||||
145 | event->accept(); | ||||
47 | } | 146 | } | ||
48 | 147 | | |||
49 | if (requestedSize.isEmpty()) { | 148 | void QuickEditor::keyReleaseEvent(QKeyEvent* event) | ||
50 | return mPixmap; | 149 | { | ||
150 | if (mToggleMagnifier && !(event->modifiers() & Qt::ShiftModifier)) { | ||||
151 | mToggleMagnifier = false; | ||||
152 | update(); | ||||
153 | } | ||||
154 | event->accept(); | ||||
51 | } | 155 | } | ||
52 | 156 | | |||
53 | return mPixmap.scaled(requestedSize, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation); | 157 | void QuickEditor::mousePressEvent(QMouseEvent* event) | ||
158 | { | ||||
159 | if (event->button() & Qt::LeftButton) { | ||||
160 | const QPointF& pos = event->screenPos(); | ||||
161 | mMousePos = pos; | ||||
162 | mMagnifierAllowed = true; | ||||
163 | mMouseDragState = whereIsTheMouse(pos); | ||||
164 | switch(mMouseDragState) { | ||||
165 | case MouseState::Outside: | ||||
166 | mStartPos = pos; | ||||
167 | break; | ||||
168 | case MouseState::Inside: | ||||
169 | mStartPos = pos; | ||||
170 | mMagnifierAllowed = false; | ||||
171 | mInitialTopLeft = mSelection.topLeft(); | ||||
172 | setCursor(Qt::ClosedHandCursor); | ||||
173 | break; | ||||
174 | case MouseState::Top: | ||||
175 | case MouseState::Left: | ||||
176 | case MouseState::TopLeft: | ||||
177 | mStartPos = mSelection.bottomRight(); | ||||
178 | break; | ||||
179 | case MouseState::Bottom: | ||||
180 | case MouseState::Right: | ||||
181 | case MouseState::BottomRight: | ||||
182 | mStartPos = mSelection.topLeft(); | ||||
183 | break; | ||||
184 | case MouseState::TopRight: | ||||
185 | mStartPos = mSelection.bottomLeft(); | ||||
186 | break; | ||||
187 | case MouseState::BottomLeft: | ||||
188 | mStartPos = mSelection.topRight(); | ||||
189 | default: | ||||
190 | break; | ||||
191 | } | ||||
192 | } | ||||
193 | if (mMagnifierAllowed) { | ||||
194 | update(); | ||||
195 | } | ||||
196 | event->accept(); | ||||
54 | } | 197 | } | ||
55 | 198 | | |||
56 | QPixmap mPixmap; | 199 | void QuickEditor::mouseMoveEvent(QMouseEvent* event) | ||
57 | }; | 200 | { | ||
201 | const QPointF& pos = event->screenPos(); | ||||
202 | mMousePos = pos; | ||||
203 | mMagnifierAllowed = true; | ||||
204 | switch (mMouseDragState) { | ||||
205 | case MouseState::None: { | ||||
206 | setMouseCursor(pos); | ||||
207 | mMagnifierAllowed = false; | ||||
208 | break; | ||||
209 | } | ||||
210 | case MouseState::TopLeft: | ||||
211 | case MouseState::TopRight: | ||||
212 | case MouseState::BottomRight: | ||||
213 | case MouseState::BottomLeft: | ||||
214 | case MouseState::Outside: { | ||||
215 | const qreal dprI = 1.0 / devicePixelRatioF(); | ||||
216 | mSelection.setRect( | ||||
217 | qMin(pos.x(), mStartPos.x()), | ||||
218 | qMin(pos.y(), mStartPos.y()), | ||||
219 | qAbs(pos.x() - mStartPos.x()) + dprI, | ||||
220 | qAbs(pos.y() - mStartPos.y()) + dprI | ||||
221 | ); | ||||
222 | update(); | ||||
223 | break; | ||||
224 | } | ||||
225 | case MouseState::Top: | ||||
226 | case MouseState::Bottom: { | ||||
227 | const qreal dprI = 1.0 / devicePixelRatioF(); | ||||
228 | mSelection.setRect( | ||||
229 | mSelection.x(), | ||||
230 | qMin(pos.y(), mStartPos.y()), | ||||
231 | mSelection.width(), | ||||
232 | qAbs(pos.y() - mStartPos.y()) + dprI | ||||
233 | ); | ||||
234 | update(); | ||||
235 | break; | ||||
236 | } | ||||
237 | case MouseState::Right: | ||||
238 | case MouseState::Left: { | ||||
239 | const qreal dprI = 1.0 / devicePixelRatioF(); | ||||
240 | mSelection.setRect( | ||||
241 | qMin(pos.x(), mStartPos.x()), | ||||
242 | mSelection.y(), | ||||
243 | qAbs(pos.x() - mStartPos.x()) + dprI, | ||||
244 | mSelection.height() | ||||
245 | ); | ||||
246 | update(); | ||||
247 | break; | ||||
248 | } | ||||
249 | case MouseState::Inside: { | ||||
250 | mMagnifierAllowed = false; | ||||
251 | // We use some math here to figure out if the diff with which we | ||||
252 | // move the rectangle with moves it out of bounds, | ||||
253 | // in which case we adjust the diff to not let that happen | ||||
254 | | ||||
255 | // new top left point of the rectangle | ||||
256 | QPointF newTopLeft = pos - mStartPos + mInitialTopLeft; | ||||
257 | | ||||
258 | const qreal dprI = 1.0 / devicePixelRatioF(); | ||||
259 | // the max coordinates of the top left point | ||||
260 | const qreal maxX = width() - mSelection.width() - dprI; | ||||
261 | const qreal maxY = height() - mSelection.height() - dprI; | ||||
262 | | ||||
263 | if (newTopLeft.x() < 0) { | ||||
264 | // tweak startPos to prevent rectangle from getting stuck | ||||
265 | mStartPos.setX(mStartPos.x() + newTopLeft.x()); | ||||
266 | newTopLeft.setX(0); | ||||
267 | } else { | ||||
268 | const int xOffset = newTopLeft.x() - maxX; | ||||
269 | if (xOffset > 0) { | ||||
270 | mStartPos.setX(mStartPos.x() + xOffset); | ||||
271 | newTopLeft.setX(maxX); | ||||
272 | } | ||||
273 | } | ||||
58 | 274 | | |||
59 | struct QuickEditor::QuickEditorPrivate | 275 | if (newTopLeft.y() < 0) { | ||
276 | mStartPos.setY(mStartPos.y() + newTopLeft.y()); | ||||
277 | newTopLeft.setY(0); | ||||
278 | } else { | ||||
279 | const qreal yOffset = newTopLeft.y() - maxY; | ||||
280 | if (yOffset > 0) { | ||||
281 | mStartPos.setY(mStartPos.y() + yOffset); | ||||
282 | newTopLeft.setY(maxY); | ||||
283 | } | ||||
284 | } | ||||
285 | | ||||
286 | if (newTopLeft != mSelection.topLeft()) { | ||||
287 | mSelection.moveTo(newTopLeft); | ||||
288 | update(); | ||||
289 | } | ||||
290 | } | ||||
291 | default: | ||||
292 | break; | ||||
293 | } | ||||
294 | | ||||
295 | event->accept(); | ||||
296 | } | ||||
297 | | ||||
298 | void QuickEditor::mouseReleaseEvent(QMouseEvent* event) | ||||
60 | { | 299 | { | ||
61 | KDeclarative::KDeclarative *mDecl; | 300 | const auto button = event->button(); | ||
62 | QQuickView *mQuickView; | 301 | if (button == Qt::LeftButton && mMouseDragState == MouseState::Inside) { | ||
63 | QQmlEngine *mQmlEngine; | 302 | setCursor(Qt::OpenHandCursor); | ||
64 | QRect mGrabRect; | 303 | } else if (button == Qt::RightButton) { | ||
65 | QSharedPointer<QQuickItemGrabResult> mCurrentGrabResult; | 304 | mSelection.setWidth(0); | ||
66 | }; | 305 | mSelection.setHeight(0); | ||
306 | } | ||||
307 | event->accept(); | ||||
308 | mMouseDragState = MouseState::None; | ||||
309 | update(); | ||||
310 | } | ||||
67 | 311 | | |||
68 | QuickEditor::QuickEditor(const QPixmap &pixmap, QObject *parent) : | 312 | void QuickEditor::mouseDoubleClickEvent(QMouseEvent* event) | ||
69 | QObject(parent), | 313 | { | ||
70 | mImageStore(new ImageStore(pixmap)), | 314 | event->accept(); | ||
71 | d_ptr(new QuickEditorPrivate) | 315 | if (event->button() == Qt::LeftButton && mSelection.contains(event->pos())) { | ||
72 | { | 316 | acceptSelection(); | ||
73 | Q_D(QuickEditor); | 317 | } | ||
74 | 318 | } | |||
75 | d->mQmlEngine = new QQmlEngine(); | | |||
76 | d->mDecl = new KDeclarative::KDeclarative; | | |||
77 | d->mDecl->setDeclarativeEngine(d->mQmlEngine); | | |||
78 | | ||||
79 | #if KDECLARATIVE_VERSION >= QT_VERSION_CHECK(5, 45, 0) | | |||
80 | d->mDecl->setupEngine(d->mQmlEngine); | | |||
81 | d->mDecl->setupContext(); | | |||
82 | #else | | |||
83 | d->mDecl->setupBindings(); | | |||
84 | #endif | | |||
85 | | ||||
86 | d->mQmlEngine->addImageProvider(QStringLiteral("snapshot"), mImageStore); | | |||
87 | | ||||
88 | d->mQuickView = new QQuickView(d->mQmlEngine, 0); | | |||
89 | d->mQuickView->setClearBeforeRendering(false); | | |||
90 | d->mQuickView->setSource(QUrl(QStringLiteral("qrc:///QuickEditor/EditorRoot.qml"))); | | |||
91 | | ||||
92 | d->mQuickView->setFlags(Qt::BypassWindowManagerHint | Qt::WindowStaysOnTopHint | Qt::FramelessWindowHint | Qt::Tool); | | |||
93 | d->mQuickView->setGeometry(0, 0, pixmap.width(), pixmap.height()); | | |||
94 | d->mQuickView->showFullScreen(); | | |||
95 | | ||||
96 | // connect up the signals | | |||
97 | QQuickItem *rootItem = d->mQuickView->rootObject(); | | |||
98 | connect(rootItem, SIGNAL(acceptImage(int, int, int, int)), this, SLOT(acceptImageHandler(int, int, int, int))); | | |||
99 | connect(rootItem, SIGNAL(cancelImage()), this, SIGNAL(grabCancelled())); | | |||
100 | 319 | | |||
101 | // set up initial config | 320 | void QuickEditor::paintEvent(QPaintEvent*) | ||
102 | SpectacleConfig *config = SpectacleConfig::instance(); | 321 | { | ||
103 | if (config->rememberLastRectangularRegion()) { | 322 | QPainter painter(this); | ||
104 | auto pixelRatio = d->mQuickView->devicePixelRatio(); | 323 | painter.setRenderHints(QPainter::TextAntialiasing | QPainter::Antialiasing); | ||
105 | QRect cropRegion = config->cropRegion(); | 324 | const QRect& fullRect = geometry(); | ||
106 | if (!cropRegion.isEmpty()) { | 325 | painter.drawPixmap(fullRect, mPixmap); | ||
107 | QMetaObject::invokeMethod( | 326 | if (!mSelection.size().isEmpty() || mMouseDragState != MouseState::None) { | ||
108 | rootItem, "setInitialSelection", | 327 | painter.setPen(mStrokeColour); | ||
109 | Q_ARG(QVariant, cropRegion.x() / pixelRatio), | 328 | painter.drawRect(mSelection); | ||
110 | Q_ARG(QVariant, cropRegion.y() / pixelRatio), | 329 | | ||
111 | Q_ARG(QVariant, cropRegion.width() / pixelRatio), | 330 | QRectF top(0, 0, fullRect.width(), mSelection.top()); | ||
112 | Q_ARG(QVariant, cropRegion.height() / pixelRatio) | 331 | QRectF right(mSelection.right(), mSelection.top(), fullRect.width() - mSelection.right(), mSelection.height()); | ||
332 | QRectF bottom(0, mSelection.bottom(), fullRect.width(), fullRect.height() - mSelection.bottom()); | ||||
333 | QRectF left(0, mSelection.top(), mSelection.left(), mSelection.height()); | ||||
334 | for (auto& rect : { top, right, bottom, left }) { | ||||
335 | painter.fillRect(rect, mMaskColour); | ||||
336 | } | ||||
337 | | ||||
338 | drawSelectionSizeTooltip(painter); | ||||
339 | if (mMouseDragState == MouseState::None) { // mouse is up | ||||
340 | if ((mSelection.width() > 20) && (mSelection.height() > 20)) { | ||||
341 | drawDragHandles(painter); | ||||
342 | } | ||||
343 | | ||||
344 | drawBottomHelpText(painter); | ||||
345 | } else if (mMagnifierAllowed && (mShowMagnifier ^ mToggleMagnifier)) { | ||||
346 | drawMagnifier(painter); | ||||
347 | } | ||||
348 | } else { | ||||
349 | drawMidHelpText(painter); | ||||
350 | } | ||||
351 | } | ||||
352 | | ||||
353 | inline void QuickEditor::layoutBottomHelpText() | ||||
354 | { | ||||
355 | const QRect& fullRect = geometry(); | ||||
356 | int mMaxRightWidth = 0; | ||||
357 | int width = 0; | ||||
358 | int height = 0; | ||||
359 | mBottomHelpGridLeftWidth = 0; | ||||
360 | for (auto& item : mBottomHelpText) { | ||||
361 | auto& left = item.first; | ||||
362 | auto& right = item.second; | ||||
363 | const auto leftSize = left.size().toSize(); | ||||
364 | const auto rightSize = right.size().toSize(); | ||||
365 | mBottomHelpGridLeftWidth = qMax(mBottomHelpGridLeftWidth, leftSize.width()); | ||||
366 | mMaxRightWidth = qMax(mMaxRightWidth, rightSize.width()); | ||||
367 | width = qMax(width, mBottomHelpGridLeftWidth + mMaxRightWidth + bottomHelpBoxPairSpacing); | ||||
368 | height += bottomHelpBoxLineHeight; | ||||
369 | } | ||||
370 | mBottomHelpContentPos.setX((fullRect.width() - width) / 2); | ||||
371 | mBottomHelpContentPos.setY(fullRect.height() - height - 8); | ||||
372 | mBottomHelpGridLeftWidth += mBottomHelpContentPos.x(); | ||||
373 | mBottomHelpBorderBox.setRect( | ||||
374 | mBottomHelpContentPos.x() - bottomHelpBoxPaddingX, | ||||
375 | mBottomHelpContentPos.y() - bottomHelpBoxPaddingY, | ||||
376 | width + bottomHelpBoxPaddingX * 2, | ||||
377 | height + bottomHelpBoxPaddingY * 2 | ||||
113 | ); | 378 | ); | ||
114 | } | 379 | } | ||
380 | | ||||
381 | inline void QuickEditor::drawBottomHelpText(QPainter &painter) | ||||
382 | { | ||||
383 | if (mSelection.intersects(mBottomHelpBorderBox)) return; | ||||
384 | | ||||
385 | painter.setBrush(mLabelBackgroundColour); | ||||
386 | painter.setPen(mLabelForegroundColour); | ||||
387 | painter.setFont(mBottomHelpTextFont); | ||||
388 | painter.drawRoundedRect(mBottomHelpBorderBox, 4, 4); | ||||
389 | | ||||
390 | int topOffset = mBottomHelpContentPos.y(); | ||||
391 | for (auto& item : mBottomHelpText) { | ||||
392 | auto& left = item.first; | ||||
393 | auto& right = item.second; | ||||
394 | const auto leftSize = left.size().toSize(); | ||||
395 | const auto drawY = topOffset + (bottomHelpBoxLineHeight - leftSize.height()) / 2; | ||||
396 | painter.drawStaticText(mBottomHelpGridLeftWidth - leftSize.width(), drawY, left); | ||||
397 | painter.drawStaticText(mBottomHelpGridLeftWidth + bottomHelpBoxPairSpacing, drawY, right); | ||||
398 | topOffset += bottomHelpBoxLineHeight; | ||||
399 | } | ||||
115 | } | 400 | } | ||
116 | 401 | | |||
117 | rootItem->setProperty("showMagnifier", config->showMagnifierChecked()); | 402 | inline void QuickEditor::drawDragHandles(QPainter& painter) | ||
403 | { | ||||
404 | const qreal leftX = mSelection.x(); | ||||
405 | const qreal width = mSelection.width(); | ||||
406 | const qreal centerX = leftX + width / 2.0; | ||||
407 | const qreal rightX = leftX + width; | ||||
408 | | ||||
409 | const qreal topY = mSelection.y(); | ||||
410 | const qreal height = mSelection.height(); | ||||
411 | const qreal centerY = topY + height / 2.0; | ||||
412 | const qreal bottomY = topY + height; | ||||
413 | | ||||
414 | // start a path | ||||
415 | QPainterPath path; | ||||
416 | | ||||
417 | const qreal cornerHandleDiameter = 2 * cornerHandleRadius; | ||||
418 | // top-left handle | ||||
419 | path.moveTo(leftX, topY); | ||||
420 | path.arcTo(leftX - cornerHandleRadius, topY - cornerHandleRadius, cornerHandleDiameter, cornerHandleDiameter, 0, -90); | ||||
421 | | ||||
422 | // top-right handle | ||||
423 | path.moveTo(rightX, topY); | ||||
424 | path.arcTo(rightX - cornerHandleRadius, topY - cornerHandleRadius, cornerHandleDiameter, cornerHandleDiameter, 180, 90); | ||||
425 | | ||||
426 | // bottom-left handle | ||||
427 | path.moveTo(leftX, bottomY); | ||||
428 | path.arcTo(leftX - cornerHandleRadius, bottomY - cornerHandleRadius, cornerHandleDiameter, cornerHandleDiameter, 0, 90); | ||||
429 | | ||||
430 | // bottom-right handle | ||||
431 | path.moveTo(rightX, bottomY); | ||||
432 | path.arcTo(rightX - cornerHandleRadius, bottomY - cornerHandleRadius, cornerHandleDiameter, cornerHandleDiameter, 180, -90); | ||||
433 | | ||||
434 | const qreal midHandleDiameter = 2 * midHandleRadius; | ||||
435 | // top-center handle | ||||
436 | path.moveTo(centerX, topY); | ||||
437 | path.arcTo(centerX - midHandleRadius, topY - midHandleRadius, midHandleDiameter, midHandleDiameter, 0, -180); | ||||
438 | | ||||
439 | // right-center handle | ||||
440 | path.moveTo(rightX, centerY); | ||||
441 | path.arcTo(rightX - midHandleRadius, centerY - midHandleRadius, midHandleDiameter, midHandleDiameter, 90, 180); | ||||
442 | | ||||
443 | // bottom-center handle | ||||
444 | path.moveTo(centerX, bottomY); | ||||
445 | path.arcTo(centerX - midHandleRadius, bottomY - midHandleRadius, midHandleDiameter, midHandleDiameter, 0, 180); | ||||
446 | | ||||
447 | // left-center handle | ||||
448 | path.moveTo(leftX, centerY); | ||||
449 | path.arcTo(leftX - midHandleRadius, centerY - midHandleRadius, midHandleDiameter, midHandleDiameter, 90, -180); | ||||
450 | | ||||
451 | // draw the path | ||||
452 | painter.fillPath(path, mStrokeColour); | ||||
453 | } | ||||
118 | 454 | | |||
119 | if (config->useLightRegionMaskColour()) { | 455 | inline void QuickEditor::drawMagnifier(QPainter &painter) | ||
120 | rootItem->setProperty("maskColour", QColor(255, 255, 255, 100)); | 456 | { | ||
457 | const int pixels = 2 * magPixels + 1; | ||||
458 | int magX = mMousePos.x() * devicePixelRatioF() - magPixels; | ||||
459 | int offsetX = 0; | ||||
460 | if (magX < 0) { | ||||
461 | offsetX = magX; | ||||
462 | magX = 0; | ||||
463 | } else { | ||||
464 | const int maxX = mPixmap.width() - pixels; | ||||
465 | if (magX > maxX) { | ||||
466 | offsetX = magX - maxX; | ||||
467 | magX = maxX; | ||||
468 | } | ||||
469 | } | ||||
470 | int magY = mMousePos.y() * devicePixelRatioF() - magPixels; | ||||
471 | int offsetY = 0; | ||||
472 | if (magY < 0) { | ||||
473 | offsetY = magY; | ||||
474 | magY = 0; | ||||
475 | } else { | ||||
476 | const int maxY = mPixmap.height() - pixels; | ||||
477 | if (magY > maxY) { | ||||
478 | offsetY = magY - maxY; | ||||
479 | magY = maxY; | ||||
480 | } | ||||
481 | } | ||||
482 | QRectF magniRect(magX, magY, pixels, pixels); | ||||
483 | qDebug() << pixels * magZoom << mMousePos.x() << width() - pixels * magZoom; | ||||
484 | const qreal gg = 32; | ||||
485 | qreal drawPosX = mMousePos.x() + gg + pixels * magZoom / 2; | ||||
486 | if (drawPosX > width() - pixels * magZoom / 2) { | ||||
487 | drawPosX = mMousePos.x() - gg - pixels * magZoom / 2; | ||||
488 | } | ||||
489 | qreal drawPosY = mMousePos.y() + gg + pixels * magZoom / 2; | ||||
490 | if (drawPosY > height() - pixels * magZoom / 2) { | ||||
491 | drawPosY = mMousePos.y() - gg - pixels * magZoom / 2; | ||||
492 | } | ||||
493 | QPointF drawPos(drawPosX, drawPosY); | ||||
494 | QRectF crossHairTop(drawPos.x() + magZoom * (offsetX - 0.5), drawPos.y() - magZoom * (magPixels + 0.5), magZoom, magZoom * (magPixels + offsetY)); | ||||
495 | QRectF crossHairRight(drawPos.x() + magZoom * (0.5 + offsetX), drawPos.y() + magZoom * (offsetY - 0.5), magZoom * (magPixels - offsetX), magZoom); | ||||
496 | QRectF crossHairBottom(drawPos.x() + magZoom * (offsetX - 0.5), drawPos.y() + magZoom * (0.5 + offsetY), magZoom, magZoom * (magPixels - offsetY)); | ||||
497 | QRectF crossHairLeft(drawPos.x() - magZoom * (magPixels + 0.5), drawPos.y() + magZoom * (offsetY - 0.5), magZoom * (magPixels + offsetX), magZoom); | ||||
498 | const auto frag = QPainter::PixmapFragment::create(drawPos, magniRect, magZoom, magZoom); | ||||
499 | painter.drawPixmapFragments(&frag, 1, mPixmap, QPainter::OpaqueHint); | ||||
500 | painter.setCompositionMode(QPainter::CompositionMode_SourceOver); | ||||
501 | for (auto& rect : { crossHairTop, crossHairRight, crossHairBottom, crossHairLeft }) { | ||||
502 | painter.fillRect(rect, mCrossColour); | ||||
121 | } | 503 | } | ||
122 | } | 504 | } | ||
123 | 505 | | |||
124 | QuickEditor::~QuickEditor() | 506 | inline void QuickEditor::drawMidHelpText(QPainter &painter) | ||
125 | { | 507 | { | ||
126 | Q_D(QuickEditor); | 508 | const QRect& fullRect = geometry(); | ||
127 | delete d->mQuickView; | 509 | painter.fillRect(fullRect, mMaskColour); | ||
128 | delete d->mDecl; | 510 | painter.setFont(mMidHelpTextFont); | ||
129 | delete d->mQmlEngine; | 511 | QRect textSize = painter.boundingRect(fullRect, Qt::AlignCenter, mMidHelpText); | ||
512 | QPoint pos((fullRect.width() - textSize.width()) / 2, (fullRect.height() - textSize.height()) / 2); | ||||
513 | | ||||
514 | painter.setBrush(mLabelBackgroundColour); | ||||
515 | painter.setPen(mLabelForegroundColour); | ||||
516 | painter.drawRoundedRect(QRect(pos.x() - 20, pos.y() - 20, textSize.width() + 40, textSize.height() + 40), 4, 4); | ||||
130 | 517 | | |||
131 | delete d_ptr; | 518 | painter.setCompositionMode(QPainter::CompositionMode_Source); | ||
519 | painter.drawText(QRect(pos, textSize.size()), Qt::AlignCenter, mMidHelpText); | ||||
132 | } | 520 | } | ||
133 | 521 | | |||
134 | void QuickEditor::acceptImageHandler(int x, int y, int width, int height) | 522 | inline void QuickEditor::drawSelectionSizeTooltip(QPainter &painter) | ||
135 | { | 523 | { | ||
136 | Q_D(QuickEditor); | 524 | // Set the selection size and finds the most appropriate position: | ||
525 | // - vertically centered inside the selection if the box is not covering the a large part of selection | ||||
526 | // - on top of the selection if the selection x position fits the box height plus some margin | ||||
527 | // - at the bottom otherwise | ||||
528 | const qreal dpr = devicePixelRatioF(); | ||||
529 | QString selectionSizeText = QString(tr("%1x%2")).arg(qRound(mSelection.width() * dpr)).arg(qRound(mSelection.height() * dpr)); | ||||
530 | const QRect& fullRect = geometry(); | ||||
531 | const QRectF selectionSizeTextRect = painter.boundingRect(QRectF(), 0, selectionSizeText); | ||||
532 | | ||||
533 | const qreal selectionBoxWidth = selectionSizeTextRect.width() + selectionBoxPaddingX * 2; | ||||
534 | const qreal selectionBoxHeight = selectionSizeTextRect.height() + selectionBoxPaddingY * 2; | ||||
535 | const qreal selectionBoxX = qBound( | ||||
536 | 0.0, | ||||
537 | mSelection.x() + (mSelection.width() - selectionSizeTextRect.width()) / 2 - selectionBoxPaddingX, | ||||
538 | fullRect.width() - selectionBoxWidth | ||||
539 | ); | ||||
540 | qreal selectionBoxY; | ||||
541 | if ((mSelection.width() > selectionSizeThreshold) && (mSelection.height() > selectionSizeThreshold)) { | ||||
542 | // show inside the box | ||||
543 | selectionBoxY = mSelection.y() + (mSelection.height() - selectionSizeTextRect.height()) / 2; | ||||
544 | } else { | ||||
545 | // show on top by default | ||||
546 | selectionBoxY = mSelection.y() - selectionBoxHeight - selectionBoxMarginY; | ||||
547 | if (selectionBoxY < 0) { | ||||
548 | // show at the bottom | ||||
549 | selectionBoxY = mSelection.y() + mSelection.height() + selectionBoxMarginY; | ||||
550 | } | ||||
551 | } | ||||
137 | 552 | | |||
138 | if ((x == -1) && (y == -1) && (width == -1) && (height == -1)) { | 553 | // Now do the actual box, border, and text drawing | ||
139 | SpectacleConfig::instance()->setCropRegion(QRect()); | 554 | painter.setBrush(mLabelBackgroundColour); | ||
140 | emit grabCancelled(); | 555 | painter.setPen(mLabelForegroundColour); | ||
141 | return; | 556 | const QRectF selectionBoxRect( | ||
557 | selectionBoxX, | ||||
558 | selectionBoxY, | ||||
559 | selectionBoxWidth, | ||||
560 | selectionBoxHeight | ||||
561 | ); | ||||
562 | painter.drawRect(selectionBoxRect); | ||||
563 | painter.setCompositionMode(QPainter::CompositionMode_Source); | ||||
564 | painter.drawText(selectionBoxRect, Qt::AlignCenter, selectionSizeText); | ||||
142 | } | 565 | } | ||
143 | 566 | | |||
144 | auto pixelRatio = d->mQuickView->devicePixelRatio(); | 567 | inline void QuickEditor::setMouseCursor(const QPointF& pos) | ||
145 | d->mGrabRect = QRect(x * pixelRatio, y * pixelRatio, width * pixelRatio, height * pixelRatio); | 568 | { | ||
146 | SpectacleConfig::instance()->setCropRegion(d->mGrabRect); | 569 | MouseState mouseState = whereIsTheMouse(pos); | ||
570 | if (mouseState == MouseState::Outside) { | ||||
571 | setCursor(Qt::CrossCursor); | ||||
572 | } else if (MouseState::TopLeftOrBottomRight & mouseState) { | ||||
573 | setCursor(Qt::SizeFDiagCursor); | ||||
574 | } else if (MouseState::TopRightOrBottomLeft & mouseState) { | ||||
575 | setCursor(Qt::SizeBDiagCursor); | ||||
576 | } else if (MouseState::TopOrBottom & mouseState) { | ||||
577 | setCursor(Qt::SizeVerCursor); | ||||
578 | } else if (MouseState::RightOrLeft & mouseState) { | ||||
579 | setCursor(Qt::SizeHorCursor); | ||||
580 | } else { | ||||
581 | setCursor(Qt::OpenHandCursor); | ||||
582 | } | ||||
583 | } | ||||
584 | | ||||
585 | QuickEditor::MouseState QuickEditor::whereIsTheMouse(const QPointF& pos) | ||||
586 | { | ||||
587 | if (mSelection.contains(pos)) { | ||||
588 | const qreal verSize = qMin(mouseAreaSize, mSelection.height() / 2); | ||||
589 | const qreal horSize = qMin(mouseAreaSize, mSelection.width() / 2); | ||||
147 | 590 | | |||
148 | d->mQuickView->hide(); | 591 | auto withinThreshold = [](const qreal offset, const qreal size) { | ||
149 | emit grabDone(mImageStore->mPixmap.copy(d->mGrabRect), d->mGrabRect); | 592 | return offset <= size && offset >= 0; | ||
593 | }; | ||||
594 | | ||||
595 | const bool withinTopEdge = withinThreshold(pos.y() - mSelection.top(), verSize); | ||||
596 | const bool withinRightEdge = withinThreshold(mSelection.right() - pos.x(), horSize); | ||||
597 | const bool withinBottomEdge = !withinTopEdge && withinThreshold(mSelection.bottom() - pos.y(), verSize); | ||||
598 | const bool withinLeftEdge = !withinRightEdge && withinThreshold(pos.x() - mSelection.left(), horSize); | ||||
599 | | ||||
600 | if (withinTopEdge) { | ||||
601 | if (withinRightEdge) { | ||||
602 | return MouseState::TopRight; | ||||
603 | } else if (withinLeftEdge) { | ||||
604 | return MouseState::TopLeft; | ||||
605 | } else { | ||||
606 | return MouseState::Top; | ||||
607 | } | ||||
608 | } else if (withinBottomEdge) { | ||||
609 | if (withinRightEdge) { | ||||
610 | return MouseState::BottomRight; | ||||
611 | } else if (withinLeftEdge) { | ||||
612 | return MouseState::BottomLeft; | ||||
613 | } else { | ||||
614 | return MouseState::Bottom; | ||||
615 | } | ||||
616 | } else if (withinRightEdge) { | ||||
617 | return MouseState::Right; | ||||
618 | } else if (withinLeftEdge) { | ||||
619 | return MouseState::Left; | ||||
620 | } else { | ||||
621 | return MouseState::Inside; | ||||
622 | } | ||||
623 | } else { | ||||
624 | return MouseState::Outside; | ||||
625 | } | ||||
150 | } | 626 | } |