1 | // Copyright (C) 2016 The Qt Company Ltd. |
2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only |
3 | |
4 | #include "qsizegrip.h" |
5 | |
6 | #include "qapplication.h" |
7 | #include "qevent.h" |
8 | #include "qstylepainter.h" |
9 | #include "qwindow.h" |
10 | #include <qpa/qplatformwindow.h> |
11 | #include "qstyle.h" |
12 | #include "qstyleoption.h" |
13 | #include "qlayout.h" |
14 | #include "qdebug.h" |
15 | |
16 | #include <private/qwidget_p.h> |
17 | #include "private/qapplication_p.h" |
18 | #include <qpa/qplatformtheme.h> |
19 | #include <QtWidgets/qabstractscrollarea.h> |
20 | |
21 | QT_BEGIN_NAMESPACE |
22 | |
23 | static QWidget *qt_sizegrip_topLevelWidget(QWidget* w) |
24 | { |
25 | while (w && !w->isWindow() && w->windowType() != Qt::SubWindow) |
26 | w = w->parentWidget(); |
27 | return w; |
28 | } |
29 | |
30 | class QSizeGripPrivate : public QWidgetPrivate |
31 | { |
32 | Q_DECLARE_PUBLIC(QSizeGrip) |
33 | public: |
34 | QSizeGripPrivate(); |
35 | void init(); |
36 | QPoint p; |
37 | QRect r; |
38 | int d; |
39 | int dxMax; |
40 | int dyMax; |
41 | Qt::Corner m_corner; |
42 | bool gotMousePress; |
43 | QPointer<QWidget> tlw; |
44 | |
45 | Qt::Corner corner() const; |
46 | inline bool atBottom() const |
47 | { |
48 | return m_corner == Qt::BottomRightCorner || m_corner == Qt::BottomLeftCorner; |
49 | } |
50 | |
51 | inline bool atLeft() const |
52 | { |
53 | return m_corner == Qt::BottomLeftCorner || m_corner == Qt::TopLeftCorner; |
54 | } |
55 | |
56 | void updateTopLevelWidget() |
57 | { |
58 | Q_Q(QSizeGrip); |
59 | QWidget *w = qt_sizegrip_topLevelWidget(w: q); |
60 | if (tlw == w) |
61 | return; |
62 | if (tlw) |
63 | tlw->removeEventFilter(obj: q); |
64 | tlw = w; |
65 | if (tlw) |
66 | tlw->installEventFilter(filterObj: q); |
67 | } |
68 | |
69 | // This slot is invoked by QLayout when the size grip is added to |
70 | // a layout or reparented after the tlw is shown. This re-implementation is basically |
71 | // the same as QWidgetPrivate::_q_showIfNotHidden except that it checks |
72 | // for Qt::WindowFullScreen and Qt::WindowMaximized as well. |
73 | void _q_showIfNotHidden() |
74 | { |
75 | Q_Q(QSizeGrip); |
76 | bool showSizeGrip = !(q->isHidden() && q->testAttribute(attribute: Qt::WA_WState_ExplicitShowHide)); |
77 | updateTopLevelWidget(); |
78 | if (tlw && showSizeGrip) { |
79 | Qt::WindowStates sizeGripNotVisibleState = Qt::WindowFullScreen; |
80 | sizeGripNotVisibleState |= Qt::WindowMaximized; |
81 | // Don't show the size grip if the tlw is maximized or in full screen mode. |
82 | showSizeGrip = !(tlw->windowState() & sizeGripNotVisibleState); |
83 | } |
84 | if (showSizeGrip) |
85 | q->setVisible(true); |
86 | } |
87 | |
88 | bool m_platformSizeGrip; |
89 | }; |
90 | |
91 | QSizeGripPrivate::QSizeGripPrivate() |
92 | : dxMax(0) |
93 | , dyMax(0) |
94 | , gotMousePress(false) |
95 | , tlw(nullptr) |
96 | , m_platformSizeGrip(false) |
97 | { |
98 | } |
99 | |
100 | Qt::Corner QSizeGripPrivate::corner() const |
101 | { |
102 | Q_Q(const QSizeGrip); |
103 | QWidget *tlw = qt_sizegrip_topLevelWidget(w: const_cast<QSizeGrip *>(q)); |
104 | const QPoint sizeGripPos = q->mapTo(tlw, QPoint(0, 0)); |
105 | bool isAtBottom = sizeGripPos.y() >= tlw->height() / 2; |
106 | bool isAtLeft = sizeGripPos.x() <= tlw->width() / 2; |
107 | if (isAtLeft) |
108 | return isAtBottom ? Qt::BottomLeftCorner : Qt::TopLeftCorner; |
109 | else |
110 | return isAtBottom ? Qt::BottomRightCorner : Qt::TopRightCorner; |
111 | } |
112 | |
113 | /*! |
114 | \class QSizeGrip |
115 | |
116 | \brief The QSizeGrip class provides a resize handle for resizing top-level windows. |
117 | |
118 | \ingroup mainwindow-classes |
119 | \ingroup basicwidgets |
120 | \inmodule QtWidgets |
121 | |
122 | This widget works like the standard Windows resize handle. In the |
123 | X11 version this resize handle generally works differently from |
124 | the one provided by the system if the X11 window manager does not |
125 | support necessary modern post-ICCCM specifications. |
126 | |
127 | Put this widget anywhere in a widget tree and the user can use it |
128 | to resize the top-level window or any widget with the Qt::SubWindow |
129 | flag set. Generally, this should be in the lower right-hand corner. |
130 | |
131 | Note that QStatusBar already uses this widget, so if you have a |
132 | status bar (e.g., you are using QMainWindow), then you don't need |
133 | to use this widget explicitly. The same goes for QDialog, for which |
134 | you can just call \l {QDialog::setSizeGripEnabled()} |
135 | {QDialog::setSizeGripEnabled()}. |
136 | |
137 | On some platforms the size grip automatically hides itself when the |
138 | window is shown full screen or maximised. |
139 | |
140 | \note On macOS, size grips are no longer part of the human interface |
141 | guideline, and won't show unless used in a QMdiSubWindow. Set another |
142 | style on size grips that you want to be visible in main windows. |
143 | |
144 | \table 50% |
145 | \row \li \inlineimage fusion-statusbar-sizegrip.png Screenshot of a Fusion style size grip |
146 | \li A size grip widget at the bottom-right corner of a main window, shown in the |
147 | \l{Qt Widget Gallery}{Fusion widget style}. |
148 | \endtable |
149 | |
150 | The QSizeGrip class inherits QWidget and reimplements the \l |
151 | {QWidget::mousePressEvent()}{mousePressEvent()} and \l |
152 | {QWidget::mouseMoveEvent()}{mouseMoveEvent()} functions to feature |
153 | the resize functionality, and the \l |
154 | {QWidget::paintEvent()}{paintEvent()} function to render the |
155 | size grip widget. |
156 | |
157 | \sa QStatusBar, QWidget::windowState() |
158 | */ |
159 | |
160 | |
161 | /*! |
162 | Constructs a resize corner as a child widget of the given \a |
163 | parent. |
164 | */ |
165 | QSizeGrip::QSizeGrip(QWidget * parent) |
166 | : QWidget(*new QSizeGripPrivate, parent, { }) |
167 | { |
168 | Q_D(QSizeGrip); |
169 | d->init(); |
170 | } |
171 | |
172 | |
173 | void QSizeGripPrivate::init() |
174 | { |
175 | Q_Q(QSizeGrip); |
176 | m_corner = q->isLeftToRight() ? Qt::BottomRightCorner : Qt::BottomLeftCorner; |
177 | |
178 | #if !defined(QT_NO_CURSOR) |
179 | q->setCursor(m_corner == Qt::TopLeftCorner || m_corner == Qt::BottomRightCorner |
180 | ? Qt::SizeFDiagCursor : Qt::SizeBDiagCursor); |
181 | #endif |
182 | q->setSizePolicy(QSizePolicy(QSizePolicy::Minimum, QSizePolicy::Fixed)); |
183 | updateTopLevelWidget(); |
184 | } |
185 | |
186 | |
187 | /*! |
188 | Destroys this size grip. |
189 | */ |
190 | QSizeGrip::~QSizeGrip() |
191 | { |
192 | } |
193 | |
194 | /*! |
195 | \reimp |
196 | */ |
197 | QSize QSizeGrip::sizeHint() const |
198 | { |
199 | QStyleOption opt(0); |
200 | opt.initFrom(w: this); |
201 | return style()->sizeFromContents(ct: QStyle::CT_SizeGrip, opt: &opt, contentsSize: QSize(13, 13), w: this); |
202 | } |
203 | |
204 | /*! |
205 | Paints the resize grip. |
206 | |
207 | Resize grips are usually rendered as small diagonal textured lines |
208 | in the lower-right corner. The paint event is passed in the \a |
209 | event parameter. |
210 | */ |
211 | void QSizeGrip::paintEvent(QPaintEvent *event) |
212 | { |
213 | Q_UNUSED(event); |
214 | Q_D(QSizeGrip); |
215 | QStylePainter painter(this); |
216 | QStyleOptionSizeGrip opt; |
217 | opt.initFrom(w: this); |
218 | opt.corner = d->m_corner; |
219 | painter.drawControl(ce: QStyle::CE_SizeGrip, opt); |
220 | } |
221 | |
222 | /*! |
223 | \fn void QSizeGrip::mousePressEvent(QMouseEvent * event) |
224 | |
225 | Receives the mouse press events for the widget, and primes the |
226 | resize operation. The mouse press event is passed in the \a event |
227 | parameter. |
228 | */ |
229 | |
230 | static Qt::Edges edgesFromCorner(Qt::Corner corner) |
231 | { |
232 | switch (corner) { |
233 | case Qt::TopLeftCorner: return Qt::TopEdge | Qt::LeftEdge; |
234 | case Qt::TopRightCorner: return Qt::TopEdge | Qt::RightEdge; |
235 | case Qt::BottomLeftCorner: return Qt::BottomEdge | Qt::LeftEdge; |
236 | case Qt::BottomRightCorner: return Qt::BottomEdge | Qt::RightEdge; |
237 | } |
238 | return Qt::Edges{}; |
239 | } |
240 | |
241 | static bool usePlatformSizeGrip(const QWidget *tlw) |
242 | { |
243 | const QString &platformName = QGuiApplication::platformName(); |
244 | if (platformName.contains(s: u"xcb" )) // ### FIXME QTBUG-69716 |
245 | return false; |
246 | if (tlw->testAttribute(attribute: Qt::WA_TranslucentBackground) |
247 | && platformName == u"windows" ) { |
248 | return false; // QTBUG-90628, flicker when using translucency |
249 | } |
250 | return true; |
251 | } |
252 | |
253 | void QSizeGrip::mousePressEvent(QMouseEvent * e) |
254 | { |
255 | if (e->button() != Qt::LeftButton) { |
256 | QWidget::mousePressEvent(event: e); |
257 | return; |
258 | } |
259 | |
260 | Q_D(QSizeGrip); |
261 | QWidget *tlw = qt_sizegrip_topLevelWidget(w: this); |
262 | d->p = e->globalPosition().toPoint(); |
263 | d->gotMousePress = true; |
264 | d->r = tlw->geometry(); |
265 | |
266 | // Does the platform provide size grip support? |
267 | d->m_platformSizeGrip = false; |
268 | if (tlw->isWindow() |
269 | && tlw->windowHandle() |
270 | && !(tlw->windowFlags() & Qt::X11BypassWindowManagerHint) |
271 | && !tlw->testAttribute(attribute: Qt::WA_DontShowOnScreen) |
272 | && !tlw->hasHeightForWidth() |
273 | && usePlatformSizeGrip(tlw)) { |
274 | QPlatformWindow *platformWindow = tlw->windowHandle()->handle(); |
275 | const Qt::Edges edges = edgesFromCorner(corner: d->m_corner); |
276 | d->m_platformSizeGrip = platformWindow->startSystemResize(edges); |
277 | } |
278 | |
279 | if (d->m_platformSizeGrip) |
280 | return; |
281 | |
282 | // Find available desktop/workspace geometry. |
283 | QRect availableGeometry; |
284 | bool hasVerticalSizeConstraint = true; |
285 | bool hasHorizontalSizeConstraint = true; |
286 | if (tlw->isWindow()) { |
287 | if (QGuiApplicationPrivate::platformTheme()->themeHint(hint: QPlatformTheme::InteractiveResizeAcrossScreens).toBool()) |
288 | availableGeometry = tlw->screen()->availableVirtualGeometry(); |
289 | else |
290 | availableGeometry = QWidgetPrivate::availableScreenGeometry(widget: tlw); |
291 | } |
292 | else { |
293 | const QWidget *tlwParent = tlw->parentWidget(); |
294 | // Check if tlw is inside QAbstractScrollArea/QScrollArea. |
295 | // If that's the case tlw->parentWidget() will return the viewport |
296 | // and tlw->parentWidget()->parentWidget() will return the scroll area. |
297 | #if QT_CONFIG(scrollarea) |
298 | QAbstractScrollArea *scrollArea = qobject_cast<QAbstractScrollArea *>(object: tlwParent->parentWidget()); |
299 | if (scrollArea) { |
300 | hasHorizontalSizeConstraint = scrollArea->horizontalScrollBarPolicy() == Qt::ScrollBarAlwaysOff; |
301 | hasVerticalSizeConstraint = scrollArea->verticalScrollBarPolicy() == Qt::ScrollBarAlwaysOff; |
302 | } |
303 | #endif // QT_CONFIG(scrollarea) |
304 | availableGeometry = tlwParent->contentsRect(); |
305 | } |
306 | |
307 | // Find frame geometries, title bar height, and decoration sizes. |
308 | const QRect frameGeometry = tlw->frameGeometry(); |
309 | const int titleBarHeight = qMax(a: tlw->geometry().y() - frameGeometry.y(), b: 0); |
310 | const int bottomDecoration = qMax(a: frameGeometry.height() - tlw->height() - titleBarHeight, b: 0); |
311 | const int leftRightDecoration = qMax(a: (frameGeometry.width() - tlw->width()) / 2, b: 0); |
312 | |
313 | // Determine dyMax depending on whether the sizegrip is at the bottom |
314 | // of the widget or not. |
315 | if (d->atBottom()) { |
316 | if (hasVerticalSizeConstraint) |
317 | d->dyMax = availableGeometry.bottom() - d->r.bottom() - bottomDecoration; |
318 | else |
319 | d->dyMax = INT_MAX; |
320 | } else { |
321 | if (hasVerticalSizeConstraint) |
322 | d->dyMax = availableGeometry.y() - d->r.y() + titleBarHeight; |
323 | else |
324 | d->dyMax = -INT_MAX; |
325 | } |
326 | |
327 | // In RTL mode, the size grip is to the left; find dxMax from the desktop/workspace |
328 | // geometry, the size grip geometry and the width of the decoration. |
329 | if (d->atLeft()) { |
330 | if (hasHorizontalSizeConstraint) |
331 | d->dxMax = availableGeometry.x() - d->r.x() + leftRightDecoration; |
332 | else |
333 | d->dxMax = -INT_MAX; |
334 | } else { |
335 | if (hasHorizontalSizeConstraint) |
336 | d->dxMax = availableGeometry.right() - d->r.right() - leftRightDecoration; |
337 | else |
338 | d->dxMax = INT_MAX; |
339 | } |
340 | } |
341 | |
342 | |
343 | /*! |
344 | \fn void QSizeGrip::mouseMoveEvent(QMouseEvent * event) |
345 | Resizes the top-level widget containing this widget. The mouse |
346 | move event is passed in the \a event parameter. |
347 | */ |
348 | void QSizeGrip::mouseMoveEvent(QMouseEvent * e) |
349 | { |
350 | Q_D(QSizeGrip); |
351 | if (e->buttons() != Qt::LeftButton || d->m_platformSizeGrip) { |
352 | QWidget::mouseMoveEvent(event: e); |
353 | return; |
354 | } |
355 | |
356 | QWidget* tlw = qt_sizegrip_topLevelWidget(w: this); |
357 | if (!d->gotMousePress || tlw->testAttribute(attribute: Qt::WA_WState_ConfigPending)) |
358 | return; |
359 | |
360 | QPoint np(e->globalPosition().toPoint()); |
361 | |
362 | // Don't extend beyond the available geometry; bound to dyMax and dxMax. |
363 | QSize ns; |
364 | if (d->atBottom()) |
365 | ns.rheight() = d->r.height() + qMin(a: np.y() - d->p.y(), b: d->dyMax); |
366 | else |
367 | ns.rheight() = d->r.height() - qMax(a: np.y() - d->p.y(), b: d->dyMax); |
368 | |
369 | if (d->atLeft()) |
370 | ns.rwidth() = d->r.width() - qMax(a: np.x() - d->p.x(), b: d->dxMax); |
371 | else |
372 | ns.rwidth() = d->r.width() + qMin(a: np.x() - d->p.x(), b: d->dxMax); |
373 | |
374 | ns = QLayout::closestAcceptableSize(w: tlw, s: ns); |
375 | |
376 | QPoint p; |
377 | QRect nr(p, ns); |
378 | if (d->atBottom()) { |
379 | if (d->atLeft()) |
380 | nr.moveTopRight(p: d->r.topRight()); |
381 | else |
382 | nr.moveTopLeft(p: d->r.topLeft()); |
383 | } else { |
384 | if (d->atLeft()) |
385 | nr.moveBottomRight(p: d->r.bottomRight()); |
386 | else |
387 | nr.moveBottomLeft(p: d->r.bottomLeft()); |
388 | } |
389 | |
390 | tlw->setGeometry(nr); |
391 | } |
392 | |
393 | /*! |
394 | \reimp |
395 | */ |
396 | void QSizeGrip::mouseReleaseEvent(QMouseEvent *mouseEvent) |
397 | { |
398 | if (mouseEvent->button() == Qt::LeftButton) { |
399 | Q_D(QSizeGrip); |
400 | d->gotMousePress = false; |
401 | d->p = QPoint(); |
402 | } else { |
403 | QWidget::mouseReleaseEvent(event: mouseEvent); |
404 | } |
405 | } |
406 | |
407 | /*! |
408 | \reimp |
409 | */ |
410 | void QSizeGrip::moveEvent(QMoveEvent * /*moveEvent*/) |
411 | { |
412 | Q_D(QSizeGrip); |
413 | // We're inside a resize operation; no update necessary. |
414 | if (!d->p.isNull()) |
415 | return; |
416 | |
417 | d->m_corner = d->corner(); |
418 | #if !defined(QT_NO_CURSOR) |
419 | setCursor(d->m_corner == Qt::TopLeftCorner || d->m_corner == Qt::BottomRightCorner |
420 | ? Qt::SizeFDiagCursor : Qt::SizeBDiagCursor); |
421 | #endif |
422 | } |
423 | |
424 | /*! |
425 | \reimp |
426 | */ |
427 | void QSizeGrip::showEvent(QShowEvent *showEvent) |
428 | { |
429 | QWidget::showEvent(event: showEvent); |
430 | } |
431 | |
432 | /*! |
433 | \reimp |
434 | */ |
435 | void QSizeGrip::hideEvent(QHideEvent *hideEvent) |
436 | { |
437 | QWidget::hideEvent(event: hideEvent); |
438 | } |
439 | |
440 | /*! |
441 | \reimp |
442 | */ |
443 | void QSizeGrip::setVisible(bool visible) |
444 | { |
445 | QWidget::setVisible(visible); |
446 | } |
447 | |
448 | /*! \reimp */ |
449 | bool QSizeGrip::eventFilter(QObject *o, QEvent *e) |
450 | { |
451 | Q_D(QSizeGrip); |
452 | if ((isHidden() && testAttribute(attribute: Qt::WA_WState_ExplicitShowHide)) |
453 | || e->type() != QEvent::WindowStateChange |
454 | || o != d->tlw) { |
455 | return QWidget::eventFilter(watched: o, event: e); |
456 | } |
457 | Qt::WindowStates sizeGripNotVisibleState = Qt::WindowFullScreen; |
458 | sizeGripNotVisibleState |= Qt::WindowMaximized; |
459 | // Don't show the size grip if the tlw is maximized or in full screen mode. |
460 | setVisible(!(d->tlw->windowState() & sizeGripNotVisibleState)); |
461 | setAttribute(Qt::WA_WState_ExplicitShowHide, on: false); |
462 | return QWidget::eventFilter(watched: o, event: e); |
463 | } |
464 | |
465 | /*! |
466 | \reimp |
467 | */ |
468 | bool QSizeGrip::event(QEvent *event) |
469 | { |
470 | return QWidget::event(event); |
471 | } |
472 | |
473 | QT_END_NAMESPACE |
474 | |
475 | #include "moc_qsizegrip.cpp" |
476 | |