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