Changeset View
Standalone View
src/QuickEditor/QuickEditor.cpp
Show All 11 Lines | |||||
12 | * GNU General Public License for more details. | 12 | * GNU General Public License for more details. | ||
13 | * | 13 | * | ||
14 | * You should have received a copy of the GNU Lesser General Public License | 14 | * You should have received a copy of the GNU Lesser General Public License | ||
15 | * along with this program; if not, write to the Free Software | 15 | * along with this program; if not, write to the Free Software | ||
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 <KLocalizedString> | ||
21 | 21 | | |||
22 | #include "QuickEditor.h" | ||||
22 | #include "SpectacleConfig.h" | 23 | #include "SpectacleConfig.h" | ||
23 | 24 | | |||
24 | #include <KDeclarative/KDeclarative> | 25 | const qreal QuickEditor::mouseAreaSize = 20.0; | ||
25 | #include <kdeclarative_version.h> | 26 | const qreal QuickEditor::cornerHandleRadius = 8.0; | ||
27 | const qreal QuickEditor::midHandleRadius = 5.0; | ||||
28 | const int QuickEditor::selectionSizeThreshold = 100; | ||||
29 | | ||||
30 | const int QuickEditor::selectionBoxPaddingX = 5; | ||||
31 | const int QuickEditor::selectionBoxPaddingY = 4; | ||||
32 | const int QuickEditor::selectionBoxMarginY = 2; | ||||
33 | | ||||
34 | // there's some weirdness with QStaticText breaking lines longer than the text before <br> | ||||
35 | // hence use non-breaking spaces and non-breaking hyphens to force everything that belongs in one line together | ||||
36 | QPair<QStaticText, QStaticText> QuickEditor::bottomHelpText[]{ | ||||
37 | {QStaticText(i18n("Enter, double-click:")), QStaticText(i18n("Take screenshot"))}, | ||||
38 | {QStaticText(i18n("Shift:")), QStaticText(i18n("Hold to toggle magnifier<br>while dragging selection handles"))}, | ||||
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 | |||||
39 | {QStaticText(i18n("Arrow keys:")), QStaticText(i18n("Move seletion rectangle<br>Hold Alt to resize, Shift to fine‑tune"))}, | ||||
40 | {QStaticText(i18n("Right-click:")), QStaticText(i18n("Reset selection"))}, | ||||
41 | {QStaticText(i18n("Esc:")), QStaticText(i18n("Cancel"))}, | ||||
42 | }; | ||||
43 | bool QuickEditor::bottomHelpTextPrepared = false; | ||||
44 | const int QuickEditor::bottomHelpBoxPaddingX = 12; | ||||
45 | const int QuickEditor::bottomHelpBoxPaddingY = 8; | ||||
46 | const int QuickEditor::bottomHelpBoxPairSpacing = 6; | ||||
47 | const int QuickEditor::bottomHelpBoxMarginBottom = 5; | ||||
48 | const int QuickEditor::midHelpTextFontSize = 12; | ||||
49 | | ||||
50 | const int QuickEditor::magnifierLargeStep = 15; | ||||
51 | | ||||
52 | const int QuickEditor::magZoom = 5; | ||||
53 | const int QuickEditor::magPixels = 16; | ||||
54 | const int QuickEditor::magOffset = 32; | ||||
55 | | ||||
56 | QuickEditor::QuickEditor(const QPixmap& pixmap, QObject *parent) : | ||||
57 | mMaskColor(QColor::fromRgbF(0, 0, 0, 0.15)), | ||||
58 | mStrokeColor(palette().highlight().color()), | ||||
59 | mCrossColor(QColor::fromRgbF(mStrokeColor.redF(), mStrokeColor.greenF(), mStrokeColor.blueF(), 0.7)), | ||||
60 | mLabelBackgroundColor(QColor::fromRgbF( | ||||
61 | palette().light().color().redF(), | ||||
62 | palette().light().color().greenF(), | ||||
63 | palette().light().color().blueF(), | ||||
64 | 0.85 | ||||
65 | )), | ||||
66 | mLabelForegroundColor(palette().windowText().color()), | ||||
67 | mMidHelpText(i18n("Click and drag to draw a selection rectangle,\nor press Esc to quit")), | ||||
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")`… | |||||
68 | mMidHelpTextFont(font()), | ||||
69 | mBottomHelpTextFont(font()), | ||||
70 | mBottomHelpGridLeftWidth(0), | ||||
71 | mMouseDragState(MouseState::None), | ||||
broulik: Use `i18n()` from `KLocalizedString` | |||||
72 | mPixmap(pixmap), | ||||
73 | mMagnifierAllowed(false), | ||||
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… | |||||
74 | mShowMagnifier(SpectacleConfig::instance()->showMagnifierChecked()), | ||||
75 | mToggleMagnifier(false) | ||||
76 | { | ||||
77 | Q_UNUSED(parent); | ||||
78 | | ||||
ngraham: All of this text needs to be translated; use `i18n()` like Kai indicated. | |||||
79 | SpectacleConfig *config = SpectacleConfig::instance(); | ||||
80 | if (config->useLightRegionMaskColour()) { | ||||
81 | mMaskColor = QColor(255, 255, 255, 100); | ||||
82 | } | ||||
Call QWidget constructor: QuickEditor::QuickEditor(const QPixmap &pixmap, QWidget *parent) : QWidget(parent) broulik: Call `QWidget` constructor:
```
QuickEditor::QuickEditor(const QPixmap &pixmap, QWidget… | |||||
26 | 83 | | |||
27 | #include <QPixmap> | 84 | setMouseTracking(true); | ||
28 | #include <QQuickImageProvider> | 85 | setAttribute(Qt::WA_StaticContents); | ||
ngraham: This is missing everything about the arrow keys. | |||||
abalaji: Sure, on it | |||||
29 | #include <QQuickItem> | 86 | setAttribute(Qt::WA_TranslucentBackground); | ||
30 | #include <QQuickItemGrabResult> | 87 | setWindowFlags(Qt::Tool | Qt::FramelessWindowHint | Qt::NoDropShadowWindowHint); | ||
31 | #include <QQuickView> | 88 | show(); | ||
32 | #include <QSize> | 89 | | ||
90 | dprI = 1.0 / devicePixelRatioF(); | ||||
91 | setGeometry(0, 0, static_cast<int>(mPixmap.width() * dprI), static_cast<int>(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… | |||||
92 | | ||||
93 | if (config->rememberLastRectangularRegion()) { | ||||
94 | QRect cropRegion = config->cropRegion(); | ||||
95 | if (!cropRegion.isEmpty()) { | ||||
96 | mSelection = QRectF( | ||||
97 | cropRegion.x() * dprI, | ||||
98 | cropRegion.y() * dprI, | ||||
99 | cropRegion.width() * dprI, | ||||
100 | cropRegion.height() * dprI | ||||
101 | ).intersected(geometry()); | ||||
102 | } | ||||
103 | setMouseCursor(QCursor::pos()); | ||||
104 | } else { | ||||
105 | setCursor(Qt::CrossCursor); | ||||
106 | } | ||||
107 | | ||||
108 | mMidHelpTextFont.setPointSize(midHelpTextFontSize); | ||||
109 | if (!bottomHelpTextPrepared) { | ||||
110 | bottomHelpTextPrepared = true; | ||||
111 | for (const auto& pair : bottomHelpText) { | ||||
112 | for (auto item : { pair.first, pair.second }) { | ||||
113 | item.prepare(QTransform(), mBottomHelpTextFont); | ||||
114 | item.setPerformanceHint(QStaticText::AggressiveCaching); | ||||
115 | } | ||||
ngraham: Don't hardcode font sizes | |||||
116 | } | ||||
117 | } | ||||
118 | layoutBottomHelpText(); | ||||
119 | | ||||
120 | update(); | ||||
121 | } | ||||
122 | | ||||
123 | void QuickEditor::acceptSelection() | ||||
124 | { | ||||
125 | if (!mSelection.isEmpty()) { | ||||
126 | const qreal dpr = devicePixelRatioF(); | ||||
127 | QRect scaledCropRegion = QRect( | ||||
128 | qRound(mSelection.x() * dpr), | ||||
129 | qRound(mSelection.y() * dpr), | ||||
130 | qRound(mSelection.width() * dpr), | ||||
131 | qRound(mSelection.height() * dpr) | ||||
132 | ); | ||||
133 | SpectacleConfig::instance()->setCropRegion(scaledCropRegion); | ||||
134 | emit grabDone(mPixmap.copy(scaledCropRegion)); | ||||
135 | } | ||||
136 | } | ||||
137 | | ||||
138 | void QuickEditor::keyPressEvent(QKeyEvent* event) | ||||
139 | { | ||||
140 | const auto modifiers = event->modifiers(); | ||||
141 | const bool shiftPressed = modifiers & Qt::ShiftModifier; | ||||
142 | if (shiftPressed) { | ||||
143 | mToggleMagnifier = true; | ||||
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 | |||||
144 | } | ||||
145 | switch(event->key()) { | ||||
146 | case Qt::Key_Escape: | ||||
147 | emit grabCancelled(); | ||||
148 | break; | ||||
149 | case Qt::Key_Return: | ||||
150 | case Qt::Key_Enter: | ||||
151 | acceptSelection(); | ||||
152 | break; | ||||
153 | case Qt::Key_Up: { | ||||
154 | const qreal step = (shiftPressed ? 1 : magnifierLargeStep); | ||||
155 | const int newPos = boundsUp(qRound(mSelection.top() * devicePixelRatioF() - step), false); | ||||
156 | if (modifiers & Qt::AltModifier) { | ||||
157 | mSelection.setBottom(dprI * newPos + mSelection.height()); | ||||
158 | mSelection = mSelection.normalized(); | ||||
159 | } else { | ||||
160 | mSelection.moveTop(dprI * newPos); | ||||
161 | } | ||||
162 | update(); | ||||
163 | break; | ||||
164 | } | ||||
165 | case Qt::Key_Right: { | ||||
166 | const qreal step = (shiftPressed ? 1 : magnifierLargeStep); | ||||
167 | const int newPos = boundsRight(qRound(mSelection.left() * devicePixelRatioF() + step), false); | ||||
168 | if (modifiers & Qt::AltModifier) { | ||||
169 | mSelection.setRight(dprI * newPos + mSelection.width()); | ||||
170 | } else { | ||||
171 | mSelection.moveLeft(dprI * newPos); | ||||
172 | } | ||||
173 | update(); | ||||
174 | break; | ||||
175 | } | ||||
176 | case Qt::Key_Down: { | ||||
177 | const qreal step = (shiftPressed ? 1 : magnifierLargeStep); | ||||
178 | const int newPos = boundsDown(qRound(mSelection.top() * devicePixelRatioF() + step), false); | ||||
179 | if (modifiers & Qt::AltModifier) { | ||||
180 | mSelection.setBottom(dprI * newPos + mSelection.height()); | ||||
181 | } else { | ||||
182 | mSelection.moveTop(dprI * newPos); | ||||
183 | } | ||||
184 | update(); | ||||
185 | break; | ||||
186 | } | ||||
187 | case Qt::Key_Left: { | ||||
188 | const qreal step = (shiftPressed ? 1 : magnifierLargeStep); | ||||
189 | const int newPos = boundsLeft(qRound(mSelection.left() * devicePixelRatioF() - step), false); | ||||
190 | if (modifiers & Qt::AltModifier) { | ||||
191 | mSelection.setRight(dprI * newPos + mSelection.width()); | ||||
192 | mSelection = mSelection.normalized(); | ||||
193 | } else { | ||||
194 | mSelection.moveLeft(dprI * newPos); | ||||
195 | } | ||||
196 | update(); | ||||
197 | break; | ||||
198 | } | ||||
199 | default: | ||||
200 | break; | ||||
201 | } | ||||
202 | event->accept(); | ||||
203 | } | ||||
33 | 204 | | |||
34 | struct QuickEditor::ImageStore : public QQuickImageProvider | 205 | void QuickEditor::keyReleaseEvent(QKeyEvent* event) | ||
35 | { | 206 | { | ||
36 | ImageStore(const QPixmap &pixmap) : | 207 | if (mToggleMagnifier && !(event->modifiers() & Qt::ShiftModifier)) { | ||
37 | QQuickImageProvider(QQuickImageProvider::Pixmap), | 208 | mToggleMagnifier = false; | ||
38 | mPixmap(pixmap) | 209 | update(); | ||
39 | {} | 210 | } | ||
211 | event->accept(); | ||||
212 | } | ||||
40 | 213 | | |||
41 | QPixmap requestPixmap(const QString &id, QSize *size, const QSize &requestedSize) Q_DECL_OVERRIDE | 214 | int QuickEditor::boundsLeft(int newTopLeftX, const bool mouse) | ||
42 | { | 215 | { | ||
43 | Q_UNUSED(id); | 216 | if (newTopLeftX < 0) { | ||
217 | if (mouse) { | ||||
218 | // tweak startPos to prevent rectangle from getting stuck | ||||
219 | mStartPos.setX(mStartPos.x() + newTopLeftX * dprI); | ||||
220 | } | ||||
221 | newTopLeftX = 0; | ||||
222 | } | ||||
44 | 223 | | |||
45 | if (size) { | 224 | return newTopLeftX; | ||
46 | *size = mPixmap.size(); | | |||
47 | } | 225 | } | ||
48 | 226 | | |||
49 | if (requestedSize.isEmpty()) { | 227 | int QuickEditor::boundsRight(int newTopLeftX, const bool mouse) | ||
50 | return mPixmap; | 228 | { | ||
229 | // the max X coordinate of the top left point | ||||
230 | const int realMaxX = qRound((width() - mSelection.width()) * devicePixelRatioF()); | ||||
231 | const int xOffset = newTopLeftX - realMaxX; | ||||
232 | if (xOffset > 0) { | ||||
233 | if (mouse) { | ||||
234 | mStartPos.setX(mStartPos.x() + xOffset * dprI); | ||||
51 | } | 235 | } | ||
236 | newTopLeftX = realMaxX; | ||||
237 | } | ||||
238 | | ||||
239 | return newTopLeftX; | ||||
240 | | ||||
241 | | ||||
52 | 242 | | |||
53 | return mPixmap.scaled(requestedSize, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation); | | |||
54 | } | 243 | } | ||
55 | 244 | | |||
56 | QPixmap mPixmap; | 245 | int QuickEditor::boundsUp(int newTopLeftY, const bool mouse) | ||
57 | }; | 246 | { | ||
247 | if (newTopLeftY < 0) { | ||||
248 | if (mouse) { | ||||
249 | mStartPos.setY(mStartPos.y() + newTopLeftY * dprI); | ||||
250 | } | ||||
251 | newTopLeftY = 0; | ||||
252 | } | ||||
58 | 253 | | |||
59 | class QuickEditor::QuickEditorPrivate | 254 | return newTopLeftY; | ||
255 | } | ||||
256 | | ||||
257 | int QuickEditor::boundsDown(int newTopLeftY, const bool mouse) | ||||
60 | { | 258 | { | ||
61 | public: | 259 | // the max Y coordinate of the top left point | ||
62 | KDeclarative::KDeclarative *mDecl; | 260 | const int realMaxY = qRound((height() - mSelection.height()) * devicePixelRatioF()); | ||
63 | QQuickView *mQuickView; | 261 | const int yOffset = newTopLeftY - realMaxY; | ||
64 | QQmlEngine *mQmlEngine; | 262 | if (yOffset > 0) { | ||
65 | QRect mGrabRect; | 263 | if (mouse) { | ||
66 | QSharedPointer<QQuickItemGrabResult> mCurrentGrabResult; | 264 | mStartPos.setY(mStartPos.y() + yOffset * dprI); | ||
67 | }; | 265 | } | ||
266 | newTopLeftY = realMaxY; | ||||
267 | } | ||||
68 | 268 | | |||
69 | QuickEditor::QuickEditor(const QPixmap &pixmap, QObject *parent) : | 269 | return newTopLeftY; | ||
70 | QObject(parent), | 270 | } | ||
71 | mImageStore(new ImageStore(pixmap)), | | |||
72 | d_ptr(new QuickEditorPrivate) | | |||
73 | { | | |||
74 | Q_D(QuickEditor); | | |||
75 | | ||||
76 | d->mQmlEngine = new QQmlEngine(); | | |||
77 | d->mDecl = new KDeclarative::KDeclarative; | | |||
78 | d->mDecl->setDeclarativeEngine(d->mQmlEngine); | | |||
79 | | ||||
80 | #if KDECLARATIVE_VERSION >= QT_VERSION_CHECK(5, 45, 0) | | |||
81 | d->mDecl->setupEngine(d->mQmlEngine); | | |||
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 | 271 | | |||
broulik: Use braces even for single-line statements
```
if (...) {
return;
}
``` | |||||
102 | // set up initial config | 272 | void QuickEditor::mousePressEvent(QMouseEvent* event) | ||
103 | SpectacleConfig *config = SpectacleConfig::instance(); | 273 | { | ||
104 | if (config->rememberLastRectangularRegion()) { | 274 | if (event->button() & Qt::LeftButton) { | ||
105 | auto pixelRatio = d->mQuickView->devicePixelRatio(); | 275 | const QPointF& pos = event->pos(); | ||
106 | QRect cropRegion = config->cropRegion(); | 276 | mMousePos = pos; | ||
107 | if (!cropRegion.isEmpty()) { | 277 | mMagnifierAllowed = true; | ||
108 | QMetaObject::invokeMethod( | 278 | mMouseDragState = mouseLocation(pos); | ||
109 | rootItem, "setInitialSelection", | 279 | switch(mMouseDragState) { | ||
110 | Q_ARG(QVariant, cropRegion.x() / pixelRatio), | 280 | case MouseState::Outside: | ||
111 | Q_ARG(QVariant, cropRegion.y() / pixelRatio), | 281 | mStartPos = pos; | ||
112 | Q_ARG(QVariant, cropRegion.width() / pixelRatio), | 282 | break; | ||
113 | Q_ARG(QVariant, cropRegion.height() / pixelRatio) | 283 | case MouseState::Inside: | ||
284 | mStartPos = pos; | ||||
285 | mMagnifierAllowed = false; | ||||
286 | mInitialTopLeft = mSelection.topLeft(); | ||||
broulik: `const auto &` | |||||
287 | setCursor(Qt::ClosedHandCursor); | ||||
288 | break; | ||||
289 | case MouseState::Top: | ||||
290 | case MouseState::Left: | ||||
291 | case MouseState::TopLeft: | ||||
292 | mStartPos = mSelection.bottomRight(); | ||||
293 | break; | ||||
294 | case MouseState::Bottom: | ||||
295 | case MouseState::Right: | ||||
296 | case MouseState::BottomRight: | ||||
297 | mStartPos = mSelection.topLeft(); | ||||
298 | break; | ||||
299 | case MouseState::TopRight: | ||||
300 | mStartPos = mSelection.bottomLeft(); | ||||
301 | break; | ||||
302 | case MouseState::BottomLeft: | ||||
303 | mStartPos = mSelection.topRight(); | ||||
304 | break; | ||||
305 | default: | ||||
306 | break; | ||||
307 | } | ||||
308 | } | ||||
309 | if (mMagnifierAllowed) { | ||||
310 | update(); | ||||
311 | } | ||||
312 | event->accept(); | ||||
313 | } | ||||
314 | | ||||
315 | void QuickEditor::mouseMoveEvent(QMouseEvent* event) | ||||
316 | { | ||||
317 | const QPointF& pos = event->pos(); | ||||
318 | mMousePos = pos; | ||||
319 | mMagnifierAllowed = true; | ||||
320 | switch (mMouseDragState) { | ||||
321 | case MouseState::None: { | ||||
322 | setMouseCursor(pos); | ||||
323 | mMagnifierAllowed = false; | ||||
324 | break; | ||||
325 | } | ||||
326 | case MouseState::TopLeft: | ||||
327 | case MouseState::TopRight: | ||||
328 | case MouseState::BottomRight: | ||||
329 | case MouseState::BottomLeft: { | ||||
330 | const bool afterX = pos.x() >= mStartPos.x(); | ||||
331 | const bool afterY = pos.y() >= mStartPos.y(); | ||||
332 | mSelection.setRect( | ||||
333 | afterX ? mStartPos.x() : pos.x(), | ||||
334 | afterY ? mStartPos.y() : pos.y(), | ||||
335 | qAbs(pos.x() - mStartPos.x()) + (afterX ? dprI : 0), | ||||
336 | qAbs(pos.y() - mStartPos.y()) + (afterY ? dprI : 0) | ||||
114 | ); | 337 | ); | ||
338 | update(); | ||||
339 | break; | ||||
115 | } | 340 | } | ||
341 | case MouseState::Outside: { | ||||
342 | mSelection.setRect( | ||||
343 | qMin(pos.x(), mStartPos.x()), | ||||
344 | qMin(pos.y(), mStartPos.y()), | ||||
345 | qAbs(pos.x() - mStartPos.x()) + dprI, | ||||
346 | qAbs(pos.y() - mStartPos.y()) + dprI | ||||
347 | ); | ||||
348 | update(); | ||||
349 | break; | ||||
350 | } | ||||
351 | case MouseState::Top: | ||||
352 | case MouseState::Bottom: { | ||||
353 | const bool afterY = pos.y() >= mStartPos.y(); | ||||
354 | mSelection.setRect( | ||||
355 | mSelection.x(), | ||||
356 | afterY ? mStartPos.y() : pos.y(), | ||||
357 | mSelection.width(), | ||||
358 | qAbs(pos.y() - mStartPos.y()) + (afterY ? dprI : 0) | ||||
359 | ); | ||||
360 | update(); | ||||
361 | break; | ||||
362 | } | ||||
363 | case MouseState::Right: | ||||
364 | case MouseState::Left: { | ||||
365 | const bool afterX = pos.x() >= mStartPos.x(); | ||||
366 | mSelection.setRect( | ||||
367 | afterX ? mStartPos.x() : pos.x(), | ||||
368 | mSelection.y(), | ||||
369 | qAbs(pos.x() - mStartPos.x()) + (afterX ? dprI : 0), | ||||
370 | mSelection.height() | ||||
371 | ); | ||||
372 | update(); | ||||
373 | break; | ||||
374 | } | ||||
375 | case MouseState::Inside: { | ||||
376 | mMagnifierAllowed = false; | ||||
377 | // We use some math here to figure out if the diff with which we | ||||
378 | // move the rectangle with moves it out of bounds, | ||||
379 | // in which case we adjust the diff to not let that happen | ||||
380 | | ||||
381 | const qreal dpr = devicePixelRatioF(); | ||||
382 | // new top left point of the rectangle | ||||
383 | QPoint newTopLeft = ((pos - mStartPos + mInitialTopLeft) * dpr).toPoint(); | ||||
384 | | ||||
385 | int newTopLeftX = boundsLeft(newTopLeft.x()); | ||||
386 | if (newTopLeftX != 0) { | ||||
387 | newTopLeftX = boundsRight(newTopLeftX); | ||||
116 | } | 388 | } | ||
117 | 389 | | |||
118 | rootItem->setProperty("showMagnifier", config->showMagnifierChecked()); | 390 | int newTopLeftY = boundsUp(newTopLeft.y()); | ||
391 | if (newTopLeftY != 0) { | ||||
392 | newTopLeftY = boundsDown(newTopLeftY); | ||||
393 | } | ||||
119 | 394 | | |||
120 | if (config->useLightRegionMaskColour()) { | 395 | const auto newTopLeftF = QPointF(newTopLeftX * dprI, newTopLeftY * dprI); | ||
121 | rootItem->setProperty("maskColour", QColor(255, 255, 255, 100)); | 396 | | ||
397 | mSelection.moveTo(newTopLeftF); | ||||
398 | update(); | ||||
399 | break; | ||||
122 | } | 400 | } | ||
401 | default: | ||||
402 | break; | ||||
123 | } | 403 | } | ||
124 | 404 | | |||
125 | QuickEditor::~QuickEditor() | 405 | event->accept(); | ||
406 | } | ||||
407 | | ||||
408 | void QuickEditor::mouseReleaseEvent(QMouseEvent* event) | ||||
126 | { | 409 | { | ||
127 | Q_D(QuickEditor); | 410 | const auto button = event->button(); | ||
128 | delete d->mQuickView; | 411 | if (button == Qt::LeftButton && mMouseDragState == MouseState::Inside) { | ||
129 | delete d->mDecl; | 412 | setCursor(Qt::OpenHandCursor); | ||
130 | delete d->mQmlEngine; | 413 | } else if (button == Qt::RightButton) { | ||
414 | mSelection.setWidth(0); | ||||
415 | mSelection.setHeight(0); | ||||
416 | } | ||||
417 | event->accept(); | ||||
418 | mMouseDragState = MouseState::None; | ||||
419 | update(); | ||||
420 | } | ||||
131 | 421 | | |||
132 | delete d_ptr; | 422 | void QuickEditor::mouseDoubleClickEvent(QMouseEvent* event) | ||
423 | { | ||||
424 | event->accept(); | ||||
425 | if (event->button() == Qt::LeftButton && mSelection.contains(event->pos())) { | ||||
426 | acceptSelection(); | ||||
427 | } | ||||
133 | } | 428 | } | ||
134 | 429 | | |||
135 | void QuickEditor::acceptImageHandler(int x, int y, int width, int height) | 430 | void QuickEditor::paintEvent(QPaintEvent*) | ||
136 | { | 431 | { | ||
137 | Q_D(QuickEditor); | 432 | QPainter painter(this); | ||
433 | painter.setRenderHints(QPainter::Antialiasing); | ||||
434 | QBrush brush(mPixmap); | ||||
435 | brush.setTransform(QTransform().scale(dprI, dprI)); | ||||
436 | painter.setBackground(brush); | ||||
437 | painter.eraseRect(geometry()); | ||||
438 | if (!mSelection.size().isEmpty() || mMouseDragState != MouseState::None) { | ||||
439 | painter.fillRect(mSelection, mStrokeColor); | ||||
440 | const QRectF innerRect = mSelection.adjusted(1, 1, -1, -1); | ||||
441 | if (innerRect.width() > 0 && innerRect.height() > 0) { | ||||
442 | painter.eraseRect(mSelection.adjusted(1, 1, -1, -1)); | ||||
443 | } | ||||
138 | 444 | | |||
139 | if ((x == -1) && (y == -1) && (width == -1) && (height == -1)) { | 445 | QRectF top(0, 0, width(), mSelection.top()); | ||
140 | SpectacleConfig::instance()->setCropRegion(QRect()); | 446 | QRectF right(mSelection.right(), mSelection.top(), width() - mSelection.right(), mSelection.height()); | ||
141 | emit grabCancelled(); | 447 | QRectF bottom(0, mSelection.bottom(), width(), height() - mSelection.bottom()); | ||
448 | QRectF left(0, mSelection.top(), mSelection.left(), mSelection.height()); | ||||
449 | for (const auto& rect : { top, right, bottom, left }) { | ||||
450 | painter.fillRect(rect, mMaskColor); | ||||
451 | } | ||||
452 | | ||||
453 | drawSelectionSizeTooltip(painter); | ||||
454 | if (mMouseDragState == MouseState::None) { // mouse is up | ||||
455 | if ((mSelection.width() > 20) && (mSelection.height() > 20)) { | ||||
456 | drawDragHandles(painter); | ||||
457 | } | ||||
458 | | ||||
459 | } else if (mMagnifierAllowed && (mShowMagnifier ^ mToggleMagnifier)) { | ||||
460 | drawMagnifier(painter); | ||||
461 | } | ||||
462 | drawBottomHelpText(painter); | ||||
463 | } else { | ||||
464 | drawMidHelpText(painter); | ||||
465 | } | ||||
466 | } | ||||
467 | | ||||
468 | void QuickEditor::layoutBottomHelpText() | ||||
469 | { | ||||
470 | int maxRightWidth = 0; | ||||
471 | int contentWidth = 0; | ||||
472 | int contentHeight = 0; | ||||
473 | mBottomHelpGridLeftWidth = 0; | ||||
474 | int i = 0; | ||||
475 | for (const auto& item : bottomHelpText) { | ||||
476 | const auto& left = item.first; | ||||
477 | const auto& right = item.second; | ||||
478 | const auto leftSize = left.size().toSize(); | ||||
479 | const auto rightSize = right.size().toSize(); | ||||
480 | mBottomHelpGridLeftWidth = qMax(mBottomHelpGridLeftWidth, leftSize.width()); | ||||
481 | maxRightWidth = qMax(maxRightWidth, rightSize.width()); | ||||
482 | contentWidth = qMax(contentWidth, mBottomHelpGridLeftWidth + maxRightWidth + bottomHelpBoxPairSpacing); | ||||
483 | contentHeight += rightSize.height() + (++i != bottomHelpLength ? bottomHelpBoxMarginBottom : 0); | ||||
484 | } | ||||
485 | mBottomHelpContentPos.setX((width() - contentWidth) / 2); | ||||
486 | mBottomHelpContentPos.setY(height() - contentHeight - 8); | ||||
487 | mBottomHelpGridLeftWidth += mBottomHelpContentPos.x(); | ||||
488 | mBottomHelpBorderBox.setRect( | ||||
489 | mBottomHelpContentPos.x() - bottomHelpBoxPaddingX, | ||||
490 | mBottomHelpContentPos.y() - bottomHelpBoxPaddingY, | ||||
491 | contentWidth + bottomHelpBoxPaddingX * 2, | ||||
492 | contentHeight + bottomHelpBoxPaddingY * 2 - 1 | ||||
493 | ); | ||||
494 | } | ||||
495 | | ||||
496 | void QuickEditor::drawBottomHelpText(QPainter &painter) | ||||
497 | { | ||||
498 | if (mSelection.intersects(mBottomHelpBorderBox)) { | ||||
142 | return; | 499 | return; | ||
143 | } | 500 | } | ||
144 | 501 | | |||
145 | auto pixelRatio = d->mQuickView->devicePixelRatio(); | 502 | painter.setBrush(mLabelBackgroundColor); | ||
146 | d->mGrabRect = QRect(x * pixelRatio, y * pixelRatio, width * pixelRatio, height * pixelRatio); | 503 | painter.setPen(mLabelForegroundColor); | ||
147 | SpectacleConfig::instance()->setCropRegion(d->mGrabRect); | 504 | painter.setFont(mBottomHelpTextFont); | ||
505 | painter.setRenderHint(QPainter::Antialiasing, false); | ||||
506 | painter.drawRect(mBottomHelpBorderBox); | ||||
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. | |||||
507 | painter.setRenderHint(QPainter::Antialiasing, true); | ||||
508 | | ||||
509 | int topOffset = mBottomHelpContentPos.y(); | ||||
510 | int i = 0; | ||||
511 | for (const auto& item : bottomHelpText) { | ||||
512 | const auto& left = item.first; | ||||
513 | const auto& right = item.second; | ||||
514 | const auto leftSize = left.size().toSize(); | ||||
515 | const auto rightSize = right.size().toSize(); | ||||
516 | painter.drawStaticText(mBottomHelpGridLeftWidth - leftSize.width(), topOffset, left); | ||||
517 | painter.drawStaticText(mBottomHelpGridLeftWidth + bottomHelpBoxPairSpacing, topOffset, right); | ||||
518 | if (++i != bottomHelpLength) { | ||||
519 | topOffset += rightSize.height() + bottomHelpBoxMarginBottom; | ||||
520 | } | ||||
521 | } | ||||
522 | } | ||||
523 | | ||||
524 | void QuickEditor::drawDragHandles(QPainter& painter) | ||||
525 | { | ||||
526 | const qreal left = mSelection.x(); | ||||
527 | const qreal width = mSelection.width(); | ||||
528 | const qreal centerX = left + width / 2.0; | ||||
529 | const qreal right = left + width; | ||||
530 | | ||||
531 | const qreal top = mSelection.y(); | ||||
532 | const qreal height = mSelection.height(); | ||||
533 | const qreal centerY = top + height / 2.0; | ||||
534 | const qreal bottom = top + height; | ||||
535 | | ||||
536 | // start a path | ||||
537 | QPainterPath path; | ||||
538 | | ||||
539 | const qreal cornerHandleDiameter = 2 * cornerHandleRadius; | ||||
540 | | ||||
541 | // x and y coordinates of handle arcs | ||||
542 | const qreal leftHandle = left - cornerHandleRadius; | ||||
543 | const qreal topHandle = top - cornerHandleRadius; | ||||
544 | const qreal rightHandle = right - cornerHandleRadius; | ||||
545 | const qreal bottomHandle = bottom - cornerHandleRadius; | ||||
546 | const qreal centerHandleX = centerX - midHandleRadius; | ||||
547 | const qreal centerHandleY = centerY - midHandleRadius; | ||||
548 | | ||||
549 | // top-left handle | ||||
550 | path.moveTo(left, top); | ||||
551 | path.arcTo(leftHandle, topHandle, cornerHandleDiameter, cornerHandleDiameter, 0, -90); | ||||
552 | | ||||
553 | // top-right handle | ||||
554 | path.moveTo(right, top); | ||||
555 | path.arcTo(rightHandle, topHandle, cornerHandleDiameter, cornerHandleDiameter, 180, 90); | ||||
556 | | ||||
557 | // bottom-left handle | ||||
558 | path.moveTo(left, bottom); | ||||
559 | path.arcTo(leftHandle, bottomHandle, cornerHandleDiameter, cornerHandleDiameter, 0, 90); | ||||
560 | | ||||
561 | // bottom-right handle | ||||
562 | path.moveTo(right, bottom); | ||||
563 | path.arcTo(rightHandle, bottomHandle, cornerHandleDiameter, cornerHandleDiameter, 180, -90); | ||||
564 | | ||||
565 | const qreal midHandleDiameter = 2 * midHandleRadius; | ||||
566 | // top-center handle | ||||
567 | path.moveTo(centerX, top); | ||||
568 | path.arcTo(centerHandleX, top - midHandleRadius, midHandleDiameter, midHandleDiameter, 0, -180); | ||||
569 | | ||||
570 | // right-center handle | ||||
571 | path.moveTo(right, centerY); | ||||
572 | path.arcTo(right - midHandleRadius, centerHandleY, midHandleDiameter, midHandleDiameter, 90, 180); | ||||
573 | | ||||
574 | // bottom-center handle | ||||
575 | path.moveTo(centerX, bottom); | ||||
576 | path.arcTo(centerHandleX, bottom - midHandleRadius, midHandleDiameter, midHandleDiameter, 0, 180); | ||||
577 | | ||||
578 | // left-center handle | ||||
579 | path.moveTo(left, centerY); | ||||
580 | path.arcTo(left - midHandleRadius, centerHandleY, midHandleDiameter, midHandleDiameter, 90, -180); | ||||
581 | | ||||
582 | // draw the path | ||||
583 | painter.fillPath(path, mStrokeColor); | ||||
584 | } | ||||
585 | | ||||
586 | void QuickEditor::drawMagnifier(QPainter &painter) | ||||
587 | { | ||||
588 | const int pixels = 2 * magPixels + 1; | ||||
589 | int magX = static_cast<int>(mMousePos.x() * devicePixelRatioF() - magPixels); | ||||
590 | int offsetX = 0; | ||||
591 | if (magX < 0) { | ||||
592 | offsetX = magX; | ||||
593 | magX = 0; | ||||
594 | } else { | ||||
595 | const int maxX = mPixmap.width() - pixels; | ||||
596 | if (magX > maxX) { | ||||
597 | offsetX = magX - maxX; | ||||
598 | magX = maxX; | ||||
599 | } | ||||
600 | } | ||||
601 | int magY = static_cast<int>(mMousePos.y() * devicePixelRatioF() - magPixels); | ||||
602 | int offsetY = 0; | ||||
603 | if (magY < 0) { | ||||
604 | offsetY = magY; | ||||
605 | magY = 0; | ||||
606 | } else { | ||||
607 | const int maxY = mPixmap.height() - pixels; | ||||
608 | if (magY > maxY) { | ||||
609 | offsetY = magY - maxY; | ||||
610 | magY = maxY; | ||||
611 | } | ||||
612 | } | ||||
613 | QRectF magniRect(magX, magY, pixels, pixels); | ||||
148 | 614 | | |||
149 | d->mQuickView->hide(); | 615 | qreal drawPosX = mMousePos.x() + magOffset + pixels * magZoom / 2; | ||
150 | emit grabDone(mImageStore->mPixmap.copy(d->mGrabRect), d->mGrabRect); | 616 | if (drawPosX > width() - pixels * magZoom / 2) { | ||
617 | drawPosX = mMousePos.x() - magOffset - pixels * magZoom / 2; | ||||
618 | } | ||||
619 | qreal drawPosY = mMousePos.y() + magOffset + pixels * magZoom / 2; | ||||
620 | if (drawPosY > height() - pixels * magZoom / 2) { | ||||
621 | drawPosY = mMousePos.y() - magOffset - pixels * magZoom / 2; | ||||
622 | } | ||||
623 | QPointF drawPos(drawPosX, drawPosY); | ||||
624 | QRectF crossHairTop(drawPos.x() + magZoom * (offsetX - 0.5), drawPos.y() - magZoom * (magPixels + 0.5), magZoom, magZoom * (magPixels + offsetY)); | ||||
625 | QRectF crossHairRight(drawPos.x() + magZoom * (0.5 + offsetX), drawPos.y() + magZoom * (offsetY - 0.5), magZoom * (magPixels - offsetX), magZoom); | ||||
626 | QRectF crossHairBottom(drawPos.x() + magZoom * (offsetX - 0.5), drawPos.y() + magZoom * (0.5 + offsetY), magZoom, magZoom * (magPixels - offsetY)); | ||||
627 | QRectF crossHairLeft(drawPos.x() - magZoom * (magPixels + 0.5), drawPos.y() + magZoom * (offsetY - 0.5), magZoom * (magPixels + offsetX), magZoom); | ||||
628 | QRectF crossHairBorder(drawPos.x() - magZoom * (magPixels + 0.5) - 1, drawPos.y() - magZoom * (magPixels + 0.5) - 1, pixels * magZoom + 2, pixels * magZoom + 2); | ||||
629 | const auto frag = QPainter::PixmapFragment::create(drawPos, magniRect, magZoom, magZoom); | ||||
630 | | ||||
631 | painter.fillRect(crossHairBorder, mLabelForegroundColor); | ||||
632 | painter.drawPixmapFragments(&frag, 1, mPixmap, QPainter::OpaqueHint); | ||||
633 | painter.setCompositionMode(QPainter::CompositionMode_SourceOver); | ||||
634 | for (auto& rect : { crossHairTop, crossHairRight, crossHairBottom, crossHairLeft }) { | ||||
635 | painter.fillRect(rect, mCrossColor); | ||||
636 | } | ||||
637 | } | ||||
638 | | ||||
639 | void QuickEditor::drawMidHelpText(QPainter &painter) | ||||
640 | { | ||||
641 | painter.fillRect(geometry(), mMaskColor); | ||||
642 | painter.setFont(mMidHelpTextFont); | ||||
643 | QRect textSize = painter.boundingRect(QRect(), Qt::AlignCenter, mMidHelpText); | ||||
644 | QPoint pos((width() - textSize.width()) / 2, (height() - textSize.height()) / 2); | ||||
645 | | ||||
646 | painter.setBrush(mLabelBackgroundColor); | ||||
647 | QPen pen(mLabelForegroundColor); | ||||
648 | pen.setWidth(2); | ||||
649 | painter.setPen(pen); | ||||
650 | painter.drawRoundedRect(QRect(pos.x() - 20, pos.y() - 20, textSize.width() + 40, textSize.height() + 40), 4, 4); | ||||
651 | | ||||
652 | painter.setCompositionMode(QPainter::CompositionMode_Source); | ||||
653 | painter.drawText(QRect(pos, textSize.size()), Qt::AlignCenter, mMidHelpText); | ||||
654 | } | ||||
655 | | ||||
656 | void QuickEditor::drawSelectionSizeTooltip(QPainter &painter) | ||||
657 | { | ||||
658 | // Set the selection size and finds the most appropriate position: | ||||
659 | // - vertically centered inside the selection if the box is not covering the a large part of selection | ||||
660 | // - on top of the selection if the selection x position fits the box height plus some margin | ||||
661 | // - at the bottom otherwise | ||||
662 | const qreal dpr = devicePixelRatioF(); | ||||
663 | QString selectionSizeText = ki18n("%1×%2").subs(qRound(mSelection.width() * dpr)).subs(qRound(mSelection.height() * dpr)).toString(); | ||||
664 | const QRect selectionSizeTextRect = painter.boundingRect(QRect(), 0, selectionSizeText); | ||||
665 | | ||||
666 | const int selectionBoxWidth = selectionSizeTextRect.width() + selectionBoxPaddingX * 2; | ||||
667 | const int selectionBoxHeight = selectionSizeTextRect.height() + selectionBoxPaddingY * 2; | ||||
668 | const int selectionBoxX = qBound( | ||||
669 | 0, | ||||
670 | static_cast<int>(mSelection.x()) + (static_cast<int>(mSelection.width()) - selectionSizeTextRect.width()) / 2 - selectionBoxPaddingX, | ||||
671 | width() - selectionBoxWidth | ||||
672 | ); | ||||
673 | int selectionBoxY; | ||||
674 | if ((mSelection.width() >= selectionSizeThreshold) && (mSelection.height() >= selectionSizeThreshold)) { | ||||
675 | // show inside the box | ||||
676 | selectionBoxY = static_cast<int>(mSelection.y() + (mSelection.height() - selectionSizeTextRect.height()) / 2); | ||||
677 | } else { | ||||
678 | // show on top by default | ||||
679 | selectionBoxY = static_cast<int>(mSelection.y() - selectionBoxHeight - selectionBoxMarginY); | ||||
680 | if (selectionBoxY < 0) { | ||||
681 | // show at the bottom | ||||
682 | selectionBoxY = static_cast<int>(mSelection.y() + mSelection.height() + selectionBoxMarginY); | ||||
683 | } | ||||
684 | } | ||||
685 | | ||||
686 | // Now do the actual box, border, and text drawing | ||||
687 | painter.setBrush(mLabelBackgroundColor); | ||||
688 | painter.setPen(mLabelForegroundColor); | ||||
689 | const QRect selectionBoxRect( | ||||
690 | selectionBoxX, | ||||
691 | selectionBoxY, | ||||
692 | selectionBoxWidth, | ||||
693 | selectionBoxHeight | ||||
694 | ); | ||||
695 | | ||||
696 | painter.setRenderHint(QPainter::Antialiasing, false); | ||||
697 | painter.drawRect(selectionBoxRect); | ||||
698 | painter.setRenderHint(QPainter::Antialiasing, true); | ||||
699 | painter.drawText(selectionBoxRect, Qt::AlignCenter, selectionSizeText); | ||||
700 | } | ||||
701 | | ||||
702 | void QuickEditor::setMouseCursor(const QPointF& pos) | ||||
703 | { | ||||
704 | MouseState mouseState = mouseLocation(pos); | ||||
705 | if (mouseState == MouseState::Outside) { | ||||
706 | setCursor(Qt::CrossCursor); | ||||
707 | } else if (MouseState::TopLeftOrBottomRight & mouseState) { | ||||
708 | setCursor(Qt::SizeFDiagCursor); | ||||
709 | } else if (MouseState::TopRightOrBottomLeft & mouseState) { | ||||
710 | setCursor(Qt::SizeBDiagCursor); | ||||
711 | } else if (MouseState::TopOrBottom & mouseState) { | ||||
712 | setCursor(Qt::SizeVerCursor); | ||||
713 | } else if (MouseState::RightOrLeft & mouseState) { | ||||
714 | setCursor(Qt::SizeHorCursor); | ||||
715 | } else { | ||||
716 | setCursor(Qt::OpenHandCursor); | ||||
717 | } | ||||
718 | } | ||||
719 | | ||||
720 | QuickEditor::MouseState QuickEditor::mouseLocation(const QPointF& pos) | ||||
721 | { | ||||
722 | if (mSelection.contains(pos)) { | ||||
723 | const qreal verSize = qMin(mouseAreaSize, mSelection.height() / 2); | ||||
724 | const qreal horSize = qMin(mouseAreaSize, mSelection.width() / 2); | ||||
725 | | ||||
726 | auto withinThreshold = [](const qreal offset, const qreal size) { | ||||
727 | return offset <= size && offset >= 0; | ||||
728 | }; | ||||
729 | | ||||
730 | const bool withinTopEdge = withinThreshold(pos.y() - mSelection.top(), verSize); | ||||
731 | const bool withinRightEdge = withinThreshold(mSelection.right() - pos.x(), horSize); | ||||
732 | const bool withinBottomEdge = !withinTopEdge && withinThreshold(mSelection.bottom() - pos.y(), verSize); | ||||
733 | const bool withinLeftEdge = !withinRightEdge && withinThreshold(pos.x() - mSelection.left(), horSize); | ||||
734 | | ||||
735 | if (withinTopEdge) { | ||||
736 | if (withinRightEdge) { | ||||
737 | return MouseState::TopRight; | ||||
738 | } else if (withinLeftEdge) { | ||||
739 | return MouseState::TopLeft; | ||||
740 | } else { | ||||
741 | return MouseState::Top; | ||||
742 | } | ||||
743 | } else if (withinBottomEdge) { | ||||
744 | if (withinRightEdge) { | ||||
745 | return MouseState::BottomRight; | ||||
746 | } else if (withinLeftEdge) { | ||||
747 | return MouseState::BottomLeft; | ||||
748 | } else { | ||||
749 | return MouseState::Bottom; | ||||
750 | } | ||||
751 | } else if (withinRightEdge) { | ||||
752 | return MouseState::Right; | ||||
753 | } else if (withinLeftEdge) { | ||||
754 | return MouseState::Left; | ||||
755 | } else { | ||||
756 | return MouseState::Inside; | ||||
757 | } | ||||
758 | } else { | ||||
759 | return MouseState::Outside; | ||||
760 | } | ||||
151 | } | 761 | } |
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.