/* * Copyright (c) Contributors to the Open 3D Engine Project. * For complete copyright and license terms please see the LICENSE at the root of this distribution. * * SPDX-License-Identifier: Apache-2.0 OR MIT * */ #include #include #include #include #include namespace AzQtComponents { enum { ChangeIncrement = 10 // How many pixels are added each time we move or resize }; InteractiveWindowGeometryChanger::InteractiveWindowGeometryChanger(QWindow* target, QObject *parent) : QObject(parent) , m_targetWindow(target) , m_originalCursorPos(target ? target->mapFromGlobal(QCursor::pos()) : QPoint()) { if (m_targetWindow) { qApp->setOverrideCursor(Qt::SizeAllCursor); qApp->installEventFilter(this); m_targetWindow->setMouseGrabEnabled(true); // Dismiss the changer if someone does intrusive stuff programatically, like maximizing the window connect(target, &QWindow::visibilityChanged, this, &QObject::deleteLater); connect(target, &QWindow::windowStateChanged, this, &QObject::deleteLater); } else { // this doesn't happen qWarning() << Q_FUNC_INFO << "target window is null"; deleteLater(); Q_ASSERT(false); } } InteractiveWindowGeometryChanger::~InteractiveWindowGeometryChanger() { m_targetWindow->setMouseGrabEnabled(false); qApp->restoreOverrideCursor(); // Restore original cursor position restoreCursorPosition(); } void InteractiveWindowGeometryChanger::restoreCursorPosition() { if (m_targetWindow && m_restoreCursorAtExit) { AzQtComponents::SetCursorPos(m_targetWindow->mapToGlobal(m_originalCursorPos)); } } bool InteractiveWindowGeometryChanger::eventFilter(QObject* watched, QEvent* ev) { if (ev->type() == QEvent::KeyPress) { if (!m_targetWindow) { // The window was deleted by external factors, dismiss our changer deleteLater(); return true; } auto keyPressEv = static_cast(ev); switch (keyPressEv->key()) { case Qt::Key_Left: case Qt::Key_Up: case Qt::Key_Right: case Qt::Key_Down: handleKeyPress(keyPressEv); return true; case Qt::Key_Escape: case Qt::Key_Enter: case Qt::Key_Return: deleteLater(); return true; // consume it default: return QObject::eventFilter(watched, ev); } } else if (ev->type() == QEvent::MouseButtonPress || ev->type() == QEvent::MouseButtonRelease || ev->type() == QEvent::MouseButtonDblClick) { // Any mouse click dismisses the geometry changer deleteLater(); // We don't want to restore the cursor pos, the user just clicked somewhere, would be odd to move it m_restoreCursorAtExit = false; } else if (ev->type() == QEvent::MouseMove) { handleMouseMove(static_cast(ev)); } return QObject::eventFilter(watched, ev); } InteractiveWindowResizer::InteractiveWindowResizer(QWindow* target, QObject* parent) : InteractiveWindowGeometryChanger(target, parent) { } InteractiveWindowResizer::SideToResize InteractiveWindowResizer::keyToSide(int key) const { switch (key) { case Qt::Key_Left: return LeftSide; case Qt::Key_Up: return TopSide; case Qt::Key_Right: return RightSide; case Qt::Key_Down: return BottomSide; default: return NoneSide; } } bool InteractiveWindowResizer::sideIsVertical(SidesToResize side) const { return side & (TopSide | BottomSide); } bool InteractiveWindowResizer::sideIsHorizontal(SidesToResize side) const { return side & (LeftSide | RightSide); } bool InteractiveWindowResizer::sideIsCorner(SidesToResize side) const { return sideIsHorizontal(side) && sideIsVertical(side); } void InteractiveWindowResizer::handleKeyPress(QKeyEvent* ev) { if (m_sideToResize == NoneSide) { // First arrow press just determines which side we're going to resize m_sideToResize = SidesToResize(keyToSide(ev->key())); updateCursor(); return; } else if (!sideIsCorner(m_sideToResize)) { // When resizing left or right and the user presses up or down then we start resizing // a corner instead if (sideIsHorizontal(m_sideToResize) && sideIsVertical(keyToSide(ev->key()))) { // We're reisizing left or right, but user pressed up or down, so we go to corner instead m_sideToResize |= keyToSide(ev->key()); updateCursor(); return; } else if (sideIsVertical(m_sideToResize) && sideIsHorizontal(keyToSide(ev->key()))) { // We're resizing top or bottom, but user pressed left or right, so we go to corner instead m_sideToResize |= keyToSide(ev->key()); updateCursor(); return; } } // Now we do the actual resizing: QRect geometry = m_targetWindow->geometry(); // const QRect originalGeometry = geometry; int dx1 = 0, dx2 = 0, dy1 = 0, dy2 = 0; bool horizontal = false; int signedness = 0; switch (ev->key()) { case Qt::Key_Left: horizontal = true; signedness = -1; break; case Qt::Key_Right: horizontal = true; signedness = 1; break; case Qt::Key_Down: horizontal = false; signedness = 1; break; case Qt::Key_Up: horizontal = false; signedness = -1; break; default: break; } if (horizontal) { if (m_sideToResize & LeftSide) { dx1 = ChangeIncrement * signedness; } else if (m_sideToResize & RightSide) { dx2 = ChangeIncrement * signedness; } } else { if (m_sideToResize & TopSide) { dy1 = ChangeIncrement * signedness; } else if (m_sideToResize & BottomSide) { dy2 = ChangeIncrement * signedness; } } geometry.adjust(dx1, dy1, dx2, dy2); if (geometry.height() >= m_targetWindow->minimumHeight() && geometry.width() >= m_targetWindow->minimumWidth() && geometry.height() <= m_targetWindow->maximumHeight() && geometry.width() <= m_targetWindow->maximumWidth()) { EnsureGeometryWithinScreenTop(geometry); m_targetWindow->setGeometry(geometry); updateCursor(); // Position changed, move cursor to border again } } void InteractiveWindowResizer::handleMouseMove(QMouseEvent* ev) { if (m_sideToResize == NoneSide) { // First arrow press just determines which side we're going to resize, so nothing will happen here return; } // Now we do the actual resizing: QRect geometry = m_targetWindow->geometry(); if (m_sideToResize & LeftSide) { geometry.setLeft(ev->globalX() - 1); } if (m_sideToResize & RightSide) { geometry.setRight(ev->globalX()); } if (m_sideToResize & TopSide) { geometry.setTop(ev->globalY() - 1); } if (m_sideToResize & BottomSide) { geometry.setBottom(ev->globalY()); } if (geometry.height() >= m_targetWindow->minimumHeight() && geometry.width() >= m_targetWindow->minimumWidth() && geometry.height() <= m_targetWindow->maximumHeight() && geometry.width() <= m_targetWindow->maximumWidth()) { EnsureGeometryWithinScreenTop(geometry); m_targetWindow->setGeometry(geometry); updateCursor(); // Position changed, move cursor to border again } } void InteractiveWindowResizer::updateCursor() { // When pressing "Size" in the context menu, the first arrow key press will change the cursor // shape and position it in one of the edges, or corner const auto s = m_sideToResize; // Less verbose QPoint newPos = m_targetWindow->position(); const int x = newPos.x(); const int y = newPos.y(); const int width = m_targetWindow->width(); const int height = m_targetWindow->height(); // Restore our previous cursor override so there's only one override stacked qApp->restoreOverrideCursor(); // The magic +1/-1 bellow is because of QTBUG-58590, shape isn't set otherwise for modal windows if (((s & LeftSide) && (s & TopSide)) || ((s & BottomSide) && (s & RightSide))) // Corner { qApp->setOverrideCursor(Qt::SizeFDiagCursor); newPos.setX((s & LeftSide) ? (x + 1) : (x + width - 1)); newPos.setY((s & TopSide) ? (y + 1) : (y + height - 1)); } else if (((s & LeftSide) && (s & BottomSide)) || ((s & RightSide) && (s & TopSide))) // Corner { qApp->setOverrideCursor(Qt::SizeBDiagCursor); newPos.setX((s & LeftSide) ? (x + 1) : (x + width - 1)); newPos.setY((s & TopSide) ? (y + 1) : (y + height - 1)); } else if (s & (LeftSide | RightSide)) { qApp->setOverrideCursor(Qt::SizeHorCursor); newPos.setY(y + height / 2); newPos.setX((s & LeftSide) ? (x + 1) : (x + width - 1)); } else if (s & (TopSide | BottomSide)) { qApp->setOverrideCursor(Qt::SizeVerCursor); newPos.setX(x + width / 2); newPos.setY((s & TopSide) ? (y + 1) : (y + height - 1)); } AzQtComponents::SetCursorPos(newPos); } InteractiveWindowMover::InteractiveWindowMover(QWindow* target, QObject* parent) : InteractiveWindowGeometryChanger(target, parent) { } void InteractiveWindowMover::handleKeyPress(QKeyEvent* ev) { m_arrowAlreadyPressed = true; QPoint offset(0, 0); switch (ev->key()) { case Qt::Key_Left: offset += QPoint(-ChangeIncrement, 0); break; case Qt::Key_Up: offset += QPoint(0, -ChangeIncrement); break; case Qt::Key_Right: offset += QPoint(ChangeIncrement, 0); break; case Qt::Key_Down: offset += QPoint(0, ChangeIncrement); break; default: Q_ASSERT(false); return; } QRect geometry = m_targetWindow->geometry().translated(offset); EnsureGeometryWithinScreenTop(geometry); m_targetWindow->setGeometry(geometry); // Mouse cursor travels too while we use the arrow keys restoreCursorPosition(); } void InteractiveWindowMover::handleMouseMove(QMouseEvent*) { if (!m_arrowAlreadyPressed) { // Mouse only moves the window if one arrow has been pressed. That's how Windows does it. return; } const QPoint newPos = m_targetWindow->mapFromGlobal(QCursor::pos()); const QPoint offset = newPos - m_originalCursorPos; QRect geometry = m_targetWindow->geometry().translated(offset); EnsureGeometryWithinScreenTop(geometry); m_targetWindow->setGeometry(geometry); } } // namespace AzQtComponents