Changeset 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 | | |||
ngraham: Hmm, we still can't find a better way to handle this? I'm not sure if this text is really… | |||||
@ngraham I'm working on a better way to do this abalaji: @ngraham I'm working on a better way to do this | |||||
27 | #include <QPixmap> | 38 | QuickEditor::QuickEditor(const QPixmap& pixmap, QObject* parent) : | ||
28 | #include <QQuickImageProvider> | 39 | mMaskColour(QColor::fromRgbF(0, 0, 0, 0.15)), | ||
29 | #include <QQuickItem> | 40 | mStrokeColor(palette().highlight().color()), | ||
30 | #include <QQuickItemGrabResult> | 41 | mLabelBackgroundColour(QColor::fromRgbF( | ||
31 | #include <QQuickView> | 42 | palette().light().color().redF(), | ||
32 | #include <QSize> | 43 | palette().light().color().greenF(), | ||
44 | palette().light().color().blueF(), | ||||
There's a special way we handle markup and line breaks in strings; change i18n("some\ntext") to xi18nc("@info", "some<nl/>text") For more info, see https://api.kde.org/frameworks/ki18n/html/prg_guide.html#kuit_markup ngraham: There's a special way we handle markup and line breaks in strings; change `i18n("some\ntext")`… | |||||
45 | 0.85 | ||||
46 | )), | ||||
47 | mLabelForegroundColour(palette().windowText().color()), | ||||
48 | mMidHelpText(tr("Click and drag to draw a selection rectangle,\nor press Esc to quit")), | ||||
broulik: Use `i18n()` from `KLocalizedString` | |||||
49 | mMidHelpTextFont(font()), | ||||
50 | mBottomHelpText({ | ||||
What are all these s for? This seems like the wrong way to do whatever you're trying to do here. ngraham: What are all these ` `s for? This seems like the wrong way to do whatever you're trying to… | |||||
I'm using QStaticText for performance so the text layout information is not recalculated with each paint. As it turns out, the only way to easily accomplish linebreaks with QStaticText is using rich-text and <br>, but QStaticText with rich-text has a weird issue where it considers the width of everything before the first <br> (the first line) to be the maximum width for the entire piece of text, and thus wraps the text on each subsequent line to fit this width. I'm using non-breaking spaces to force it to put them on a single line. Try getting rid of all the 's and you'll see what I mean. I'll try and figure out a different solution for this. abalaji: I'm using `QStaticText` for performance so the text layout information is not recalculated with… | |||||
51 | {QStaticText(tr("Enter, double-click:")), QStaticText(tr("Take screenshot"))}, | ||||
52 | {QStaticText(tr("Shift:")), QStaticText(tr("Hold to toggle magnifier"))}, | ||||
53 | {QStaticText(tr("Right-click:")), QStaticText(tr("Reset selection"))}, | ||||
54 | {QStaticText(tr("Esc:")), QStaticText(tr("Cancel"))}, | ||||
55 | }), | ||||
ngraham: All of this text needs to be translated; use `i18n()` like Kai indicated. | |||||
56 | mBottomHelpTextFont(font()), | ||||
57 | mBottomHelpGridLeftWidth(0), | ||||
58 | mMouseDragState(MouseState::None), | ||||
59 | mPixmap(pixmap) | ||||
60 | { | ||||
61 | Q_UNUSED(parent); | ||||
62 | | ||||
ngraham: This is missing everything about the arrow keys. | |||||
abalaji: Sure, on it | |||||
63 | SpectacleConfig *config = SpectacleConfig::instance(); | ||||
64 | if (config->useLightRegionMaskColour()) { | ||||
65 | mMaskColour = QColor(255, 255, 255, 100); | ||||
66 | } | ||||
67 | | ||||
68 | setMouseTracking(true); | ||||
69 | setAttribute(Qt::WA_StaticContents); | ||||
70 | setWindowFlags(Qt::WindowStaysOnTopHint); | ||||
71 | setAttribute(Qt::WA_TranslucentBackground); | ||||
72 | showFullScreen(); | ||||
Call QWidget constructor: QuickEditor::QuickEditor(const QPixmap &pixmap, QWidget *parent) : QWidget(parent) broulik: Call `QWidget` constructor:
```
QuickEditor::QuickEditor(const QPixmap &pixmap, QWidget… | |||||
73 | | ||||
74 | const qreal dprI = 1.0 / devicePixelRatioF(); | ||||
75 | setGeometry(0, 0, mPixmap.width() * dprI, mPixmap.height() * dprI); | ||||
33 | 76 | | |||
34 | struct QuickEditor::ImageStore : public QQuickImageProvider | 77 | if (config->rememberLastRectangularRegion()) { | ||
78 | QRect cropRegion = config->cropRegion(); | ||||
79 | if (!cropRegion.isEmpty()) { | ||||
80 | mSelection = QRectF( | ||||
81 | cropRegion.x() * dprI, | ||||
If we can make the full-screen overlay window not always stay on top (i.e. you can alt-tab out of it or switch virtual desktops while it's open) then that would probably be sufficient to fully fix https://bugs.kde.org/show_bug.cgi?id=374009 in conjunction with existing improvements that this patch provides. ngraham: If we can make the full-screen overlay window not always stay on top (i.e. you can alt-tab out… | |||||
82 | cropRegion.y() * dprI, | ||||
83 | cropRegion.width() * dprI, | ||||
84 | cropRegion.height() * dprI | ||||
85 | ).intersected(geometry()); | ||||
86 | } | ||||
87 | setMouseCursor(QCursor::pos()); | ||||
88 | } else { | ||||
89 | setCursor(Qt::CrossCursor); | ||||
90 | } | ||||
91 | | ||||
92 | mMidHelpTextFont.setPointSize(12); | ||||
93 | | ||||
94 | for (auto& pair : mBottomHelpText) { | ||||
95 | for (auto item : { pair.first, pair.second }) { | ||||
96 | item.prepare(QTransform(), mBottomHelpTextFont); | ||||
97 | item.setPerformanceHint(QStaticText::AggressiveCaching); | ||||
ngraham: Don't hardcode font sizes | |||||
98 | } | ||||
99 | } | ||||
100 | layoutBottomHelpText(); | ||||
101 | | ||||
102 | update(); | ||||
103 | } | ||||
104 | | ||||
105 | inline void QuickEditor::acceptSelection() | ||||
35 | { | 106 | { | ||
36 | ImageStore(const QPixmap &pixmap) : | 107 | if (!mSelection.isEmpty()) { | ||
37 | QQuickImageProvider(QQuickImageProvider::Pixmap), | 108 | const qreal dpr = devicePixelRatioF(); | ||
38 | mPixmap(pixmap) | 109 | QRect scaledCropRegion = QRect( | ||
39 | {} | 110 | qRound(mSelection.x() * dpr), | ||
111 | qRound(mSelection.y() * dpr), | ||||
112 | qRound(mSelection.width() * dpr), | ||||
113 | qRound(mSelection.height() * dpr) | ||||
114 | ); | ||||
115 | SpectacleConfig::instance()->setCropRegion(scaledCropRegion); | ||||
116 | emit grabDone(mPixmap.copy(scaledCropRegion)); | ||||
117 | delete this; | ||||
118 | } | ||||
119 | } | ||||
40 | 120 | | |||
41 | QPixmap requestPixmap(const QString &id, QSize *size, const QSize &requestedSize) Q_DECL_OVERRIDE | 121 | void QuickEditor::keyPressEvent(QKeyEvent* event) | ||
42 | { | 122 | { | ||
43 | Q_UNUSED(id); | 123 | switch(event->key()) { | ||
124 | case Qt::Key_Escape: | ||||
125 | emit grabCancelled(); | ||||
126 | delete this; | ||||
Can this be done by the user of this class? Self-deletion is typically not a good idea broulik: Can this be done by the user of this class? Self-deletion is typically not a good idea | |||||
abalaji: Yeah this is stupid, I'll fix it | |||||
127 | break; | ||||
128 | case Qt::Key_Return: | ||||
129 | case Qt::Key_Enter: | ||||
130 | acceptSelection(); | ||||
131 | default: | ||||
132 | break; | ||||
133 | } | ||||
134 | } | ||||
44 | 135 | | |||
45 | if (size) { | 136 | void QuickEditor::mousePressEvent(QMouseEvent* event) | ||
46 | *size = mPixmap.size(); | 137 | { | ||
138 | if (event->button() & Qt::LeftButton) { | ||||
139 | const QPointF& pos = event->screenPos(); | ||||
140 | mMouseDragState = whereIsTheMouse(pos); | ||||
141 | switch(mMouseDragState) { | ||||
142 | case MouseState::Outside: | ||||
143 | mStartPos = pos; | ||||
144 | break; | ||||
145 | case MouseState::Inside: | ||||
146 | mStartPos = pos; | ||||
147 | mInitialTopLeft = mSelection.topLeft(); | ||||
148 | setCursor(Qt::ClosedHandCursor); | ||||
149 | break; | ||||
150 | case MouseState::Top: | ||||
151 | case MouseState::Left: | ||||
152 | case MouseState::TopLeft: | ||||
153 | mStartPos = mSelection.bottomRight(); | ||||
154 | break; | ||||
155 | case MouseState::Bottom: | ||||
156 | case MouseState::Right: | ||||
157 | case MouseState::BottomRight: | ||||
158 | mStartPos = mSelection.topLeft(); | ||||
159 | break; | ||||
160 | case MouseState::TopRight: | ||||
161 | mStartPos = mSelection.bottomLeft(); | ||||
162 | break; | ||||
163 | case MouseState::BottomLeft: | ||||
164 | mStartPos = mSelection.topRight(); | ||||
165 | default: | ||||
166 | break; | ||||
167 | } | ||||
168 | } | ||||
169 | event->accept(); | ||||
47 | } | 170 | } | ||
48 | 171 | | |||
49 | if (requestedSize.isEmpty()) { | 172 | void QuickEditor::mouseMoveEvent(QMouseEvent* event) | ||
50 | return mPixmap; | 173 | { | ||
174 | const QPointF& pos = event->screenPos(); | ||||
175 | switch (mMouseDragState) { | ||||
176 | case MouseState::None: { | ||||
177 | setMouseCursor(pos); | ||||
178 | break; | ||||
179 | } | ||||
180 | case MouseState::TopLeft: | ||||
181 | case MouseState::TopRight: | ||||
182 | case MouseState::BottomRight: | ||||
183 | case MouseState::BottomLeft: | ||||
184 | case MouseState::Outside: { | ||||
185 | const qreal dprI = 1.0 / devicePixelRatioF(); | ||||
186 | mSelection.setRect( | ||||
187 | qMin(pos.x(), mStartPos.x()), | ||||
188 | qMin(pos.y(), mStartPos.y()), | ||||
189 | qAbs(pos.x() - mStartPos.x()) + dprI, | ||||
190 | qAbs(pos.y() - mStartPos.y()) + dprI | ||||
191 | ); | ||||
192 | update(); | ||||
193 | break; | ||||
194 | } | ||||
195 | case MouseState::Top: | ||||
196 | case MouseState::Bottom: { | ||||
197 | const qreal dprI = 1.0 / devicePixelRatioF(); | ||||
198 | mSelection.setRect( | ||||
199 | mSelection.x(), | ||||
200 | qMin(pos.y(), mStartPos.y()), | ||||
201 | mSelection.width(), | ||||
202 | qAbs(pos.y() - mStartPos.y()) + dprI | ||||
203 | ); | ||||
204 | update(); | ||||
205 | break; | ||||
206 | } | ||||
207 | case MouseState::Right: | ||||
208 | case MouseState::Left: { | ||||
209 | const qreal dprI = 1.0 / devicePixelRatioF(); | ||||
210 | mSelection.setRect( | ||||
211 | qMin(pos.x(), mStartPos.x()), | ||||
212 | mSelection.y(), | ||||
213 | qAbs(pos.x() - mStartPos.x()) + dprI, | ||||
214 | mSelection.height() | ||||
215 | ); | ||||
216 | update(); | ||||
217 | break; | ||||
218 | } | ||||
219 | case MouseState::Inside: { | ||||
220 | // We use some math here to figure out if the diff with which we | ||||
221 | // move the rectangle with moves it out of bounds, | ||||
222 | // in which case we adjust the diff to not let that happen | ||||
223 | | ||||
224 | // new top left point of the rectangle | ||||
225 | QPointF newTopLeft = pos - mStartPos + mInitialTopLeft; | ||||
226 | | ||||
227 | // the max coordinates of the top left point | ||||
228 | const qreal maxX = width() - mSelection.width(); | ||||
229 | const qreal maxY = height() - mSelection.height(); | ||||
230 | | ||||
231 | if (newTopLeft.x() < 0) { | ||||
232 | // tweak startPos to prevent rectangle from getting stuck | ||||
233 | mStartPos.setX(mStartPos.x() + newTopLeft.x()); | ||||
234 | newTopLeft.setX(0); | ||||
235 | } else { | ||||
236 | const int xOffset = newTopLeft.x() - maxX; | ||||
237 | if (xOffset > 0) { | ||||
238 | mStartPos.setX(mStartPos.x() + xOffset); | ||||
239 | newTopLeft.setX(maxX); | ||||
240 | } | ||||
51 | } | 241 | } | ||
52 | 242 | | |||
53 | return mPixmap.scaled(requestedSize, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation); | 243 | if (newTopLeft.y() < 0) { | ||
244 | mStartPos.setY(mStartPos.y() + newTopLeft.y()); | ||||
245 | newTopLeft.setY(0); | ||||
246 | } else { | ||||
247 | const qreal yOffset = newTopLeft.y() - maxY; | ||||
248 | if (yOffset > 0) { | ||||
249 | mStartPos.setY(mStartPos.y() + yOffset); | ||||
250 | newTopLeft.setY(maxY); | ||||
251 | } | ||||
54 | } | 252 | } | ||
55 | 253 | | |||
56 | QPixmap mPixmap; | 254 | if (newTopLeft != mSelection.topLeft()) { | ||
57 | }; | 255 | mSelection.moveTo(newTopLeft); | ||
256 | update(); | ||||
257 | } | ||||
258 | } | ||||
259 | default: | ||||
260 | break; | ||||
261 | } | ||||
262 | event->accept(); | ||||
263 | } | ||||
58 | 264 | | |||
59 | struct QuickEditor::QuickEditorPrivate | 265 | void QuickEditor::mouseReleaseEvent(QMouseEvent* event) | ||
60 | { | 266 | { | ||
61 | KDeclarative::KDeclarative *mDecl; | 267 | const auto button = event->button(); | ||
62 | QQuickView *mQuickView; | 268 | if (button == Qt::LeftButton && mMouseDragState == MouseState::Inside) { | ||
63 | QQmlEngine *mQmlEngine; | 269 | setCursor(Qt::OpenHandCursor); | ||
64 | QRect mGrabRect; | 270 | } else if (button == Qt::RightButton) { | ||
65 | QSharedPointer<QQuickItemGrabResult> mCurrentGrabResult; | 271 | mSelection.setWidth(0); | ||
66 | }; | 272 | mSelection.setHeight(0); | ||
273 | } | ||||
274 | event->accept(); | ||||
275 | mMouseDragState = MouseState::None; | ||||
276 | update(); | ||||
277 | } | ||||
67 | 278 | | |||
68 | QuickEditor::QuickEditor(const QPixmap &pixmap, QObject *parent) : | 279 | void QuickEditor::mouseDoubleClickEvent(QMouseEvent* event) | ||
69 | QObject(parent), | 280 | { | ||
70 | mImageStore(new ImageStore(pixmap)), | 281 | event->accept(); | ||
71 | d_ptr(new QuickEditorPrivate) | 282 | if (event->button() == Qt::LeftButton && mSelection.contains(event->pos())) { | ||
72 | { | 283 | acceptSelection(); | ||
73 | Q_D(QuickEditor); | 284 | } | ||
74 | 285 | } | |||
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 | 286 | | |||
101 | // set up initial config | 287 | void QuickEditor::paintEvent(QPaintEvent*) | ||
102 | SpectacleConfig *config = SpectacleConfig::instance(); | 288 | { | ||
103 | if (config->rememberLastRectangularRegion()) { | 289 | QPainter painter(this); | ||
104 | auto pixelRatio = d->mQuickView->devicePixelRatio(); | 290 | painter.setRenderHints(QPainter::TextAntialiasing | QPainter::Antialiasing); | ||
105 | QRect cropRegion = config->cropRegion(); | 291 | const QRect& fullRect = geometry(); | ||
106 | if (!cropRegion.isEmpty()) { | 292 | painter.drawPixmap(fullRect, mPixmap); | ||
107 | QMetaObject::invokeMethod( | 293 | if (!mSelection.size().isEmpty() || mMouseDragState != MouseState::None) { | ||
108 | rootItem, "setInitialSelection", | 294 | painter.setPen(mStrokeColor); | ||
109 | Q_ARG(QVariant, cropRegion.x() / pixelRatio), | 295 | painter.drawRect(mSelection); | ||
110 | Q_ARG(QVariant, cropRegion.y() / pixelRatio), | 296 | | ||
111 | Q_ARG(QVariant, cropRegion.width() / pixelRatio), | 297 | QRectF top(0, 0, fullRect.width(), mSelection.top()); | ||
112 | Q_ARG(QVariant, cropRegion.height() / pixelRatio) | 298 | QRectF right(mSelection.right(), mSelection.top(), fullRect.width() - mSelection.right(), mSelection.height()); | ||
113 | ); | 299 | QRectF bottom(0, mSelection.bottom(), fullRect.width(), fullRect.height() - mSelection.bottom()); | ||
300 | QRectF left(0, mSelection.top(), mSelection.left(), mSelection.height()); | ||||
301 | for (auto& rect : { top, right, bottom, left }) { | ||||
302 | painter.fillRect(rect, mMaskColour); | ||||
114 | } | 303 | } | ||
304 | | ||||
305 | drawSelectionSizeTooltip(painter); | ||||
306 | if (mMouseDragState == MouseState::None) { // mouse is up | ||||
307 | if ((mSelection.width() > 20) && (mSelection.height() > 20)) { | ||||
308 | drawDragHandles(painter); | ||||
115 | } | 309 | } | ||
116 | 310 | | |||
117 | rootItem->setProperty("showMagnifier", config->showMagnifierChecked()); | 311 | drawBottomHelpText(painter); | ||
312 | } | ||||
313 | } else { | ||||
314 | drawMidHelpText(painter); | ||||
315 | } | ||||
316 | } | ||||
118 | 317 | | |||
119 | if (config->useLightRegionMaskColour()) { | 318 | inline void QuickEditor::layoutBottomHelpText() | ||
120 | rootItem->setProperty("maskColour", QColor(255, 255, 255, 100)); | 319 | { | ||
320 | const QRect& fullRect = geometry(); | ||||
321 | int mMaxRightWidth = 0; | ||||
322 | int width = 0; | ||||
323 | int height = 0; | ||||
324 | mBottomHelpGridLeftWidth = 0; | ||||
broulik: `const auto &` | |||||
325 | for (auto& item : mBottomHelpText) { | ||||
326 | auto& left = item.first; | ||||
327 | auto& right = item.second; | ||||
328 | const auto leftSize = left.size().toSize(); | ||||
329 | const auto rightSize = right.size().toSize(); | ||||
330 | mBottomHelpGridLeftWidth = qMax(mBottomHelpGridLeftWidth, leftSize.width()); | ||||
331 | mMaxRightWidth = qMax(mMaxRightWidth, rightSize.width()); | ||||
332 | width = qMax(width, mBottomHelpGridLeftWidth + mMaxRightWidth + bottomHelpBoxPairSpacing); | ||||
333 | height += bottomHelpBoxLineHeight; | ||||
121 | } | 334 | } | ||
335 | mBottomHelpContentPos.setX((fullRect.width() - width) / 2); | ||||
336 | mBottomHelpContentPos.setY(fullRect.height() - height - 8); | ||||
337 | mBottomHelpGridLeftWidth += mBottomHelpContentPos.x(); | ||||
338 | mBottomHelpBorderBox.setRect( | ||||
339 | mBottomHelpContentPos.x() - bottomHelpBoxPaddingX, | ||||
340 | mBottomHelpContentPos.y() - bottomHelpBoxPaddingY, | ||||
341 | width + bottomHelpBoxPaddingX * 2, | ||||
342 | height + bottomHelpBoxPaddingY * 2 | ||||
343 | ); | ||||
122 | } | 344 | } | ||
123 | 345 | | |||
124 | QuickEditor::~QuickEditor() | 346 | inline void QuickEditor::drawBottomHelpText(QPainter &painter) | ||
125 | { | 347 | { | ||
126 | Q_D(QuickEditor); | 348 | if (mSelection.intersects(mBottomHelpBorderBox)) return; | ||
broulik: Use braces even for single-line statements
```
if (...) {
return;
}
``` | |||||
127 | delete d->mQuickView; | | |||
128 | delete d->mDecl; | | |||
129 | delete d->mQmlEngine; | | |||
130 | 349 | | |||
131 | delete d_ptr; | 350 | painter.setBrush(mLabelBackgroundColour); | ||
351 | painter.setPen(mLabelForegroundColour); | ||||
352 | painter.setFont(mBottomHelpTextFont); | ||||
353 | painter.drawRoundedRect(mBottomHelpBorderBox, 4, 4); | ||||
354 | | ||||
355 | int topOffset = mBottomHelpContentPos.y(); | ||||
356 | for (auto& item : mBottomHelpText) { | ||||
357 | auto& left = item.first; | ||||
358 | auto& right = item.second; | ||||
359 | const auto leftSize = left.size().toSize(); | ||||
360 | const auto drawY = topOffset + (bottomHelpBoxLineHeight - leftSize.height()) / 2; | ||||
361 | painter.drawStaticText(mBottomHelpGridLeftWidth - leftSize.width(), drawY, left); | ||||
362 | painter.drawStaticText(mBottomHelpGridLeftWidth + bottomHelpBoxPairSpacing, drawY, right); | ||||
363 | topOffset += bottomHelpBoxLineHeight; | ||||
364 | } | ||||
132 | } | 365 | } | ||
133 | 366 | | |||
134 | void QuickEditor::acceptImageHandler(int x, int y, int width, int height) | 367 | inline void QuickEditor::drawDragHandles(QPainter& painter) | ||
135 | { | 368 | { | ||
136 | Q_D(QuickEditor); | 369 | const qreal leftX = mSelection.x(); | ||
370 | const qreal width = mSelection.width(); | ||||
371 | const qreal centerX = leftX + width / 2.0; | ||||
372 | const qreal rightX = leftX + width; | ||||
373 | | ||||
374 | const qreal topY = mSelection.y(); | ||||
375 | const qreal height = mSelection.height(); | ||||
376 | const qreal centerY = topY + height / 2.0; | ||||
377 | const qreal bottomY = topY + height; | ||||
378 | | ||||
379 | // start a path | ||||
380 | QPainterPath path; | ||||
381 | | ||||
382 | const qreal cornerHandleDiameter = 2 * cornerHandleRadius; | ||||
383 | // top-left handle | ||||
384 | path.moveTo(leftX, topY); | ||||
385 | path.arcTo(leftX - cornerHandleRadius, topY - cornerHandleRadius, cornerHandleDiameter, cornerHandleDiameter, 0, -90); | ||||
386 | | ||||
387 | // top-right handle | ||||
388 | path.moveTo(rightX, topY); | ||||
389 | path.arcTo(rightX - cornerHandleRadius, topY - cornerHandleRadius, cornerHandleDiameter, cornerHandleDiameter, 180, 90); | ||||
390 | | ||||
391 | // bottom-left handle | ||||
392 | path.moveTo(leftX, bottomY); | ||||
393 | path.arcTo(leftX - cornerHandleRadius, bottomY - cornerHandleRadius, cornerHandleDiameter, cornerHandleDiameter, 0, 90); | ||||
394 | | ||||
395 | // bottom-right handle | ||||
396 | path.moveTo(rightX, bottomY); | ||||
397 | path.arcTo(rightX - cornerHandleRadius, bottomY - cornerHandleRadius, cornerHandleDiameter, cornerHandleDiameter, 180, -90); | ||||
398 | | ||||
399 | const qreal midHandleDiameter = 2 * midHandleRadius; | ||||
400 | // top-center handle | ||||
401 | path.moveTo(centerX, topY); | ||||
402 | path.arcTo(centerX - midHandleRadius, topY - midHandleRadius, midHandleDiameter, midHandleDiameter, 0, -180); | ||||
403 | | ||||
404 | // right-center handle | ||||
405 | path.moveTo(rightX, centerY); | ||||
406 | path.arcTo(rightX - midHandleRadius, centerY - midHandleRadius, midHandleDiameter, midHandleDiameter, 90, 180); | ||||
407 | | ||||
408 | // bottom-center handle | ||||
409 | path.moveTo(centerX, bottomY); | ||||
410 | path.arcTo(centerX - midHandleRadius, bottomY - midHandleRadius, midHandleDiameter, midHandleDiameter, 0, 180); | ||||
411 | | ||||
412 | // left-center handle | ||||
413 | path.moveTo(leftX, centerY); | ||||
414 | path.arcTo(leftX - midHandleRadius, centerY - midHandleRadius, midHandleDiameter, midHandleDiameter, 90, -180); | ||||
137 | 415 | | |||
138 | if ((x == -1) && (y == -1) && (width == -1) && (height == -1)) { | 416 | // draw the path | ||
139 | SpectacleConfig::instance()->setCropRegion(QRect()); | 417 | painter.fillPath(path, mStrokeColor); | ||
140 | emit grabCancelled(); | | |||
141 | return; | | |||
142 | } | 418 | } | ||
143 | 419 | | |||
144 | auto pixelRatio = d->mQuickView->devicePixelRatio(); | 420 | inline void QuickEditor::drawMidHelpText(QPainter &painter) | ||
145 | d->mGrabRect = QRect(x * pixelRatio, y * pixelRatio, width * pixelRatio, height * pixelRatio); | 421 | { | ||
146 | SpectacleConfig::instance()->setCropRegion(d->mGrabRect); | 422 | const QRect& fullRect = geometry(); | ||
423 | painter.fillRect(fullRect, mMaskColour); | ||||
424 | painter.setFont(mMidHelpTextFont); | ||||
425 | QRect textSize = painter.boundingRect(fullRect, Qt::AlignCenter, mMidHelpText); | ||||
426 | QPoint pos((fullRect.width() - textSize.width()) / 2, (fullRect.height() - textSize.height()) / 2); | ||||
427 | | ||||
428 | painter.setBrush(mLabelBackgroundColour); | ||||
429 | painter.setPen(mLabelForegroundColour); | ||||
430 | painter.drawRoundedRect(QRect(pos.x() - 20, pos.y() - 20, textSize.width() + 40, textSize.height() + 40), 4, 4); | ||||
147 | 431 | | |||
148 | d->mQuickView->hide(); | 432 | painter.setCompositionMode(QPainter::CompositionMode_Source); | ||
149 | emit grabDone(mImageStore->mPixmap.copy(d->mGrabRect), d->mGrabRect); | 433 | painter.drawText(QRect(pos, textSize.size()), Qt::AlignCenter, mMidHelpText); | ||
434 | } | ||||
435 | | ||||
436 | inline void QuickEditor::drawSelectionSizeTooltip(QPainter &painter) | ||||
437 | { | ||||
438 | // Set the selection size and finds the most appropriate position: | ||||
439 | // - vertically centered inside the selection if the box is not covering the a large part of selection | ||||
440 | // - on top of the selection if the selection x position fits the box height plus some margin | ||||
441 | // - at the bottom otherwise | ||||
442 | const qreal dpr = devicePixelRatioF(); | ||||
443 | QString selectionSizeText = QString(tr("%1x%2")).arg(qRound(mSelection.width() * dpr)).arg(qRound(mSelection.height() * dpr)); | ||||
444 | const QRect& fullRect = geometry(); | ||||
445 | const QRectF selectionSizeTextRect = painter.boundingRect(QRectF(), 0, selectionSizeText); | ||||
446 | | ||||
447 | const qreal selectionBoxWidth = selectionSizeTextRect.width() + selectionBoxPaddingX * 2; | ||||
448 | const qreal selectionBoxHeight = selectionSizeTextRect.height() + selectionBoxPaddingY * 2; | ||||
449 | const qreal selectionBoxX = qBound( | ||||
450 | 0.0, | ||||
451 | mSelection.x() + (mSelection.width() - selectionSizeTextRect.width()) / 2 - selectionBoxPaddingX, | ||||
452 | fullRect.width() - selectionBoxWidth | ||||
453 | ); | ||||
454 | qreal selectionBoxY; | ||||
455 | if ((mSelection.width() > selectionSizeThreshold) && (mSelection.height() > selectionSizeThreshold)) { | ||||
456 | // show inside the box | ||||
457 | selectionBoxY = mSelection.y() + (mSelection.height() - selectionSizeTextRect.height()) / 2; | ||||
458 | } else { | ||||
459 | // show on top by default | ||||
460 | selectionBoxY = mSelection.y() - selectionBoxHeight - selectionBoxMarginY; | ||||
461 | if (selectionBoxY < 0) { | ||||
462 | // show at the bottom | ||||
463 | selectionBoxY = mSelection.y() + mSelection.height() + selectionBoxMarginY; | ||||
464 | } | ||||
465 | } | ||||
466 | | ||||
467 | // Now do the actual box, border, and text drawing | ||||
468 | painter.setBrush(mLabelBackgroundColour); | ||||
469 | painter.setPen(mLabelForegroundColour); | ||||
470 | const QRectF selectionBoxRect( | ||||
471 | selectionBoxX, | ||||
guotao: should divde by 3, not 2, when adjust very small rectangle | |||||
True, makes sense. Then we would actually be able to move the rectangle around when it's small. abalaji: True, makes sense. Then we would actually be able to move the rectangle around when it's small. | |||||
472 | selectionBoxY, | ||||
473 | selectionBoxWidth, | ||||
474 | selectionBoxHeight | ||||
475 | ); | ||||
476 | painter.drawRect(selectionBoxRect); | ||||
477 | painter.setCompositionMode(QPainter::CompositionMode_Source); | ||||
478 | painter.drawText(selectionBoxRect, Qt::AlignCenter, selectionSizeText); | ||||
479 | } | ||||
480 | | ||||
481 | inline void QuickEditor::setMouseCursor(const QPointF& pos) | ||||
482 | { | ||||
483 | MouseState mouseState = whereIsTheMouse(pos); | ||||
484 | if (mouseState == MouseState::Outside) { | ||||
485 | setCursor(Qt::CrossCursor); | ||||
486 | } else if (MouseState::TopLeftOrBottomRight & mouseState) { | ||||
487 | setCursor(Qt::SizeFDiagCursor); | ||||
488 | } else if (MouseState::TopRightOrBottomLeft & mouseState) { | ||||
489 | setCursor(Qt::SizeBDiagCursor); | ||||
490 | } else if (MouseState::TopOrBottom & mouseState) { | ||||
491 | setCursor(Qt::SizeVerCursor); | ||||
492 | } else if (MouseState::RightOrLeft & mouseState) { | ||||
493 | setCursor(Qt::SizeHorCursor); | ||||
494 | } else { | ||||
495 | setCursor(Qt::OpenHandCursor); | ||||
496 | } | ||||
497 | } | ||||
498 | | ||||
499 | QuickEditor::MouseState QuickEditor::whereIsTheMouse(const QPointF& pos) | ||||
500 | { | ||||
501 | if (mSelection.contains(pos)) { | ||||
502 | const qreal verSize = qMin(mouseAreaSize, mSelection.height()); | ||||
503 | const qreal horSize = qMin(mouseAreaSize, mSelection.width()); | ||||
504 | | ||||
505 | auto withinThreshold = [](const qreal offset, const qreal size) { | ||||
506 | return offset <= size && offset >= 0; | ||||
507 | }; | ||||
508 | | ||||
509 | const bool withinTopEdge = withinThreshold(pos.y() - mSelection.top(), verSize); | ||||
510 | const bool withinRightEdge = withinThreshold(mSelection.right() - pos.x(), horSize); | ||||
511 | const bool withinBottomEdge = !withinTopEdge && withinThreshold(mSelection.bottom() - pos.y(), verSize); | ||||
512 | const bool withinLeftEdge = !withinRightEdge && withinThreshold(pos.x() - mSelection.left(), horSize); | ||||
513 | | ||||
514 | if (withinTopEdge) { | ||||
515 | if (withinRightEdge) { | ||||
516 | return MouseState::TopRight; | ||||
517 | } else if (withinLeftEdge) { | ||||
518 | return MouseState::TopLeft; | ||||
519 | } else { | ||||
520 | return MouseState::Top; | ||||
521 | } | ||||
522 | } else if (withinBottomEdge) { | ||||
523 | if (withinRightEdge) { | ||||
524 | return MouseState::BottomRight; | ||||
525 | } else if (withinLeftEdge) { | ||||
526 | return MouseState::BottomLeft; | ||||
527 | } else { | ||||
528 | return MouseState::Bottom; | ||||
529 | } | ||||
530 | } else if (withinRightEdge) { | ||||
531 | return MouseState::Right; | ||||
532 | } else if (withinLeftEdge) { | ||||
533 | return MouseState::Left; | ||||
534 | } else { | ||||
535 | return MouseState::Inside; | ||||
536 | } | ||||
537 | } else { | ||||
538 | return MouseState::Outside; | ||||
539 | } | ||||
150 | } | 540 | } |
Hmm, we still can't find a better way to handle this? I'm not sure if this text is really translatable in its current state.