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