You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
o3de/Code/Framework/AzQtComponents/AzQtComponents/Components/Widgets/SpinBox.cpp

1683 lines
57 KiB
C++

/*
* 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 <cmath>
#include <AzQtComponents/Components/Widgets/SpinBox.h>
#include <AzQtComponents/Components/Widgets/LineEdit.h>
#include <AzQtComponents/Components/Style.h>
#include <AzQtComponents/Components/StyleManager.h>
#include <AzQtComponents/Components/ConfigHelpers.h>
#include <AzQtComponents/Utilities/Conversions.h>
#include <QApplication>
#include <QDoubleSpinBox>
#include <QEvent>
#include <QLineEdit>
#include <QMouseEvent>
#include <QObject>
#include <QPainter>
#include <QScreen>
#include <QSettings>
#include <QStyle>
#include <QStyleOptionSpinBox>
#include <QMenu>
#include <QTimer>
#include <QWindow>
#include <QTextLayout>
#include <QtWidgets/private/qstylesheetstyle_p.h>
namespace AzQtComponents
{
static const char* g_hoverControlPropertyName = "HoverControl";
static const char* g_hoveredPropertyName = "hovered";
static const char* g_spinBoxUpPressedPropertyName = "SpinBoxUpButtonPressed";
static const char* g_spinBoxDownPressedPropertyName = "SpinBoxDownButtonPressed";
static const char* g_spinBoxValueIncreasingName = "SpinBoxValueIncreasing";
static const char* g_spinBoxValueDecreasingName = "SpinBoxValueDecreasing";
static const char* g_spinBoxScrollIncreasingName = "SpinBoxScrollIncreasing";
static const char* g_spinBoxScrollDecreasingName = "SpinBoxScrollDecreasing";
static const char* g_spinBoxScrollTimerName = "SpinBoxScrollTimer";
static const char* g_spinBoxIntializedValueName = "SpinBoxInitializedValue";
static const char* g_spinBoxMinReachedName = "SpinBoxMinReached";
static const char* g_spinBoxMaxReachedName = "SpinBoxMaxReached";
static const char* g_spinBoxFocusedName = "SpinBoxFocused";
static const char* g_spinBoxDraggingName = "SpinBoxDragging";
static const char* g_spinBoxInitPropertyFlagName = "PropertyInitFlag";
// Decimal precision parameters
static const int g_decimalPrecisonDefault = 7;
static const int g_decimalDisplayPrecisionDefault = 3;
class SpinBoxWatcher : public QObject
{
public:
explicit SpinBoxWatcher(QObject* parent = nullptr);
SpinBox::Config m_config;
bool eventFilter(QObject* watched, QEvent* event) override;
bool isEditing(const QAbstractSpinBox* spinBox) const { return m_spinBoxChanging == spinBox; }
private:
enum State
{
Inactive,
Dragging,
ProcessingArrowButtons
};
int m_xPos = 0;
QPointer<QAbstractSpinBox> m_spinBoxChanging;
State m_state = Inactive;
QAbstractSpinBox* m_mouseFocusedSpinBox = nullptr;
QAbstractSpinBox* m_mouseFocusedSpinBoxSingleClicked = nullptr;
bool filterSpinBoxEvents(QAbstractSpinBox* spinBox, QEvent* event);
bool filterLineEditEvents(QLineEdit* lineEdit, QEvent* event);
bool handleMouseDragStepping(QAbstractSpinBox* spinBox, QEvent* event);
void setInializeSpinboxValue(QAbstractSpinBox* spinBox, bool clearText = false);
QTimer* SpinBoxScrollTimer(QAbstractSpinBox* spinBox);
void initStyleOption(QAbstractSpinBox* spinBox, QStyleOptionSpinBox* styleOption);
QAbstractSpinBox::StepEnabled stepEnabled(QAbstractSpinBox* spinBox);
void emitValueChangeBegan(QAbstractSpinBox* spinBox);
void emitValueChangeEnded(QAbstractSpinBox* spinBox);
void resetCursor(QAbstractSpinBox* spinBox);
};
SpinBoxWatcher::SpinBoxWatcher(QObject* parent)
: QObject(parent)
, m_config(SpinBox::defaultConfig())
{
}
bool SpinBoxWatcher::eventFilter(QObject* watched, QEvent* event)
{
bool filterEvent = false;
if (auto spinBox = qobject_cast<QAbstractSpinBox*>(watched))
{
filterEvent = filterSpinBoxEvents(spinBox, event);
}
else if (auto lineEdit = qobject_cast<QLineEdit*>(watched))
{
filterEvent = filterLineEditEvents(lineEdit, event);
}
return filterEvent ? filterEvent : QObject::eventFilter(watched, event);
}
bool SpinBoxWatcher::filterSpinBoxEvents(QAbstractSpinBox* spinBox, QEvent* event)
{
if (!spinBox || !spinBox->isEnabled())
{
return false;
}
bool filterEvent = false;
switch (event->type())
{
case QEvent::HoverEnter:
case QEvent::HoverLeave:
case QEvent::HoverMove:
{
bool buttonUpPressed = spinBox->property(g_spinBoxUpPressedPropertyName).toBool();
bool buttonDownPressed = spinBox->property(g_spinBoxDownPressedPropertyName).toBool();
// We don't want the scroll icon to show up while clicking and dragging
// on an arrow button
if (!buttonUpPressed && !buttonDownPressed)
{
resetCursor(spinBox);
}
break;
}
case QEvent::Enter:
case QEvent::Leave:
{
if (auto lineEdit = spinBox->findChild<QLineEdit*>(QString(), Qt::FindDirectChildrenOnly))
{
// Recalculate the line edit geometry so that the value runs off the right edge
// if the SpinBox buttons are not visible.
QStyleOptionSpinBox styleOption;
initStyleOption(spinBox, &styleOption);
const auto newGeometry = spinBox->style()->subControlRect(QStyle::CC_SpinBox,
&styleOption,
QStyle::SC_SpinBoxEditField,
spinBox);
if (lineEdit->geometry() != newGeometry)
{
lineEdit->setGeometry(newGeometry);
}
}
// Have to account for Qt not setting this properly for spinboxes
// We manually set a hovered property that can be matched against in the stylesheet
spinBox->setProperty(g_hoveredPropertyName, bool(event->type() == QEvent::Enter));
// Qt, for performance reasons, doesn't re-evaluate css rules when a dynamic property changes
// so we have to force it to.
spinBox->style()->unpolish(spinBox);
spinBox->style()->polish(spinBox);
break;
}
case QEvent::Paint:
{
// Update the mouse cursor. QPaintEvent is used because it occurs when the mouse
// moves between sub-controls, which means we don't have to enable mouseTracking. We
// can't do this in SpinBox::drawSpinBox because there we only have a const QWidget
// pointer.
if (m_state == Inactive)
{
const QPoint pos = spinBox->mapFromGlobal(QCursor::pos());
if (spinBox->rect().contains(pos))
{
resetCursor(spinBox);
}
}
break;
}
case QEvent::FocusIn:
{
spinBox->setProperty(g_spinBoxFocusedName, true);
if (m_config.autoSelectAllOnClickFocus)
{
QFocusEvent* fe = static_cast<QFocusEvent*>(event);
if (fe->reason() == Qt::MouseFocusReason)
{
m_mouseFocusedSpinBox = spinBox;
}
}
break;
}
case QEvent::FocusOut:
{
// Checks whether valueChangeBegun has been emitted and emits valueChangeEnded if
// required. This handles the case where a new value has been typed without pressing
// return or enter, or after a wheel event.
emitValueChangeEnded(spinBox);
spinBox->setProperty(g_spinBoxFocusedName, false);
break;
}
case QEvent::KeyPress:
{
auto keyEvent = static_cast<QKeyEvent*>(event);
// Handle up/down arrows
bool up = false;
switch (keyEvent->key())
{
case Qt::Key_Up:
up = true;
// Fall through
case Qt::Key_Down:
if (!keyEvent->isAutoRepeat())
{
emitValueChangeBegan(spinBox);
}
if (up)
{
spinBox->setProperty(g_spinBoxValueIncreasingName, true);
spinBox->setProperty(g_spinBoxValueDecreasingName, false);
}
else
{
spinBox->setProperty(g_spinBoxValueIncreasingName, false);
spinBox->setProperty(g_spinBoxValueDecreasingName, true);
}
break;
case Qt::Key_Escape:
if (!spinBox->keyboardTracking())
{
// If we're not keyboard tracking, then changes to the text field
// aren't 'committed' until the user hits Enter, Return, tabs out of
// the field, or the spinbox is hidden.
// We want to stop the commit from happening if the user hits escape.
if (spinBox->hasFocus())
{
// Big logic jump here; if the current value has been committed already,
// then everything listening on the signals knows about it already.
// If the current value has NOT been committed, nothing knows about it
// but we don't want to trigger any further updates, because we're resetting
// it.
// So we disable signals here
if (auto azSpinBox = qobject_cast<SpinBox*>(spinBox))
{
QSignalBlocker blocker(azSpinBox);
azSpinBox->setValue(azSpinBox->m_lastValue);
}
else if (auto azDoubleSpinBox = qobject_cast<DoubleSpinBox*>(spinBox))
{
QSignalBlocker blocker(azDoubleSpinBox);
azDoubleSpinBox->setValue(azDoubleSpinBox->m_lastValue);
}
spinBox->selectAll();
}
}
break;
}
// Handle digits
const bool isDigit = (keyEvent->key() >= Qt::Key_0) && (keyEvent->key() <= Qt::Key_9);
const bool isForFloatingPointDecimal = (qobject_cast<QDoubleSpinBox*>(spinBox) && (keyEvent->key() == Qt::Key_Period));
if (isDigit ||
keyEvent->key() == Qt::Key_Backspace ||
isForFloatingPointDecimal
)
{
emitValueChangeBegan(spinBox);
// emitValueChangeEnded is called when Qt::Key_Return or Qt::Key_Enter is released,
// or in QEvent::FocusOut.
}
break;
}
case QEvent::KeyRelease:
{
auto keyEvent = static_cast<QKeyEvent*>(event);
if (!m_spinBoxChanging.isNull() && !keyEvent->isAutoRepeat())
{
switch (keyEvent->key())
{
case Qt::Key_Up:
case Qt::Key_Down:
case Qt::Key_Return:
case Qt::Key_Enter:
emitValueChangeEnded(spinBox);
spinBox->setProperty(g_spinBoxValueDecreasingName, false);
spinBox->setProperty(g_spinBoxValueIncreasingName, false);
spinBox->update();
break;
}
}
break;
}
case QEvent::MouseButtonDblClick:
case QEvent::MouseButtonPress:
{
auto mouseEvent = static_cast<QMouseEvent*>(event);
if (mouseEvent->button() == Qt::LeftButton)
{
QStyleOptionSpinBox styleOption;
initStyleOption(spinBox, &styleOption);
const auto control = spinBox->style()->hitTestComplexControl(QStyle::CC_SpinBox,
&styleOption,
mouseEvent->pos(),
spinBox);
const auto enabledSteps = stepEnabled(spinBox);
if (((control == QStyle::SC_SpinBoxUp) && (enabledSteps & QSpinBox::StepUpEnabled)) ||
((control == QStyle::SC_SpinBoxDown) && (enabledSteps & QSpinBox::StepDownEnabled)))
{
emitValueChangeBegan(spinBox);
// emitValueChangeEnded is called in SpinBoxWatcher::handleMouseDragStepping
m_state = ProcessingArrowButtons;
spinBox->setProperty(g_spinBoxDraggingName, false);
if (control == QStyle::SC_SpinBoxUp)
{
spinBox->setProperty(g_spinBoxUpPressedPropertyName, true);
}
else if (control == QStyle::SC_SpinBoxDown)
{
spinBox->setProperty(g_spinBoxDownPressedPropertyName, true);
}
}
}
break;
}
case QEvent::MouseButtonRelease:
{
spinBox->setProperty(g_spinBoxUpPressedPropertyName, false);
spinBox->setProperty(g_spinBoxDownPressedPropertyName, false);
spinBox->setProperty(g_spinBoxValueDecreasingName, false);
spinBox->setProperty(g_spinBoxValueIncreasingName, false);
break;
}
case QEvent::Wheel:
{
if (!spinBox->hasFocus())
{
// To prevent the event being turned into a focus event, be sure to install an
// AzQtComponents::GlobalEventFilter on your QApplication instance.
event->accept();
return true;
}
auto wheelEvent = static_cast<QWheelEvent*>(event);
// Emulates Qt::ScrollEnd
if (wheelEvent->source() != Qt::MouseEventSynthesizedBySystem)
{
QTimer* timer = SpinBoxScrollTimer(spinBox);
if (!timer)
{
timer = new QTimer(spinBox);
timer->setObjectName(g_spinBoxScrollTimerName);
timer->setSingleShot(true);
timer->setInterval(100);
timer->callOnTimeout([this, timer, spinBox]() {
emitValueChangeEnded(spinBox);
timer->setParent(nullptr);
timer->deleteLater();
});
}
timer->start();
}
setInializeSpinboxValue(spinBox, true);
// Qt::ScrollEnd is only supported on macOS and only applies to trackpads, not scroll
// wheels.
if (wheelEvent->phase() == Qt::ScrollEnd)
{
emitValueChangeEnded(spinBox);
break;
}
const auto angleDelta = wheelEvent->angleDelta();
if (angleDelta.isNull() || !m_spinBoxChanging.isNull())
{
break;
}
else if (angleDelta.y() != 0)
{
const auto enabledSteps = stepEnabled(spinBox);
if (((angleDelta.y() < 0) && (enabledSteps & QSpinBox::StepDownEnabled)) ||
((angleDelta.y() > 0) && (enabledSteps & QSpinBox::StepUpEnabled)))
{
emitValueChangeBegan(spinBox);
// emitValueChangeEnded is called in QEvent::FocusOut
if (angleDelta.y() < 0)
{
spinBox->setProperty(g_spinBoxValueDecreasingName, true);
spinBox->setProperty(g_spinBoxValueIncreasingName, false);
}
else
{
spinBox->setProperty(g_spinBoxValueDecreasingName, false);
spinBox->setProperty(g_spinBoxValueIncreasingName, true);
}
}
}
break;
}
case QEvent::DynamicPropertyChange:
{
auto styleSheet = StyleManager::styleSheetStyle(spinBox);
styleSheet->repolish(spinBox);
break;
}
case QEvent::ShortcutOverride:
{
// This should be handled in the base class, but since that's part of Qt, do it here.
// The Up and Down keys have a function while this widget is in focus, so prevent those shortcuts from firing
auto keyEvent = static_cast<QKeyEvent*>(event);
switch (keyEvent->key())
{
case (Qt::Key_Up):
case (Qt::Key_Down):
event->accept();
return true;
break;
default:
break;
}
break;
}
}
return filterEvent ? filterEvent : handleMouseDragStepping(spinBox, event);
}
bool SpinBoxWatcher::filterLineEditEvents(QLineEdit* lineEdit, QEvent* event)
{
if (!lineEdit || !lineEdit->isEnabled())
{
return false;
}
auto spinBox = qobject_cast<QAbstractSpinBox*>(lineEdit->parent());
if (!spinBox || !spinBox->isEnabled())
{
return false;
}
bool filterEvent = false;
switch (event->type())
{
case QEvent::MouseButtonPress:
{
auto mouseEvent = static_cast<QMouseEvent*>(event);
if (mouseEvent->button() & Qt::MiddleButton)
{
filterSpinBoxEvents(spinBox, event);
// Update cursor without waiting for an Hover event
resetCursor(spinBox);
}
else if (m_config.autoSelectAllOnClickFocus)
{
if (mouseEvent->button() == Qt::LeftButton && m_mouseFocusedSpinBox == spinBox)
{
lineEdit->selectAll();
m_mouseFocusedSpinBox = nullptr;
m_mouseFocusedSpinBoxSingleClicked = spinBox;
return true;
}
m_mouseFocusedSpinBox = nullptr;
}
break;
}
case QEvent::MouseMove:
{
if (m_state == Dragging)
{
filterSpinBoxEvents(spinBox, event);
}
break;
}
case QEvent::MouseButtonRelease:
{
if (m_state == Dragging)
{
filterSpinBoxEvents(spinBox, event);
}
break;
}
case QEvent::Paint:
{
if (lineEdit->property(g_spinBoxIntializedValueName).isValid() && !lineEdit->property(g_spinBoxIntializedValueName).toBool()) {
return true;
}
break;
}
case QEvent::MouseButtonDblClick:
{
auto mouseEvent = static_cast<QMouseEvent*>(event);
if (m_config.autoSelectAllOnClickFocus)
{
if (mouseEvent->button() == Qt::LeftButton && m_mouseFocusedSpinBoxSingleClicked == spinBox)
{
// fake a single click event to place the mouse button
QMouseEvent fake(QEvent::MouseButtonPress, mouseEvent->localPos(), mouseEvent->button(), mouseEvent->buttons(), mouseEvent->modifiers());
QCoreApplication::sendEvent(lineEdit, &fake);
m_mouseFocusedSpinBoxSingleClicked = nullptr;
return true;
}
m_mouseFocusedSpinBoxSingleClicked = nullptr;
}
// Select whole number only on double click
if ((mouseEvent->button() & Qt::LeftButton) && qobject_cast<QDoubleSpinBox*>(spinBox))
{
filterEvent = true;
mouseEvent->accept();
lineEdit->selectAll();
}
break;
}
}
return filterEvent;
}
void SpinBoxWatcher::setInializeSpinboxValue(QAbstractSpinBox* spinBox, bool clearText)
{
if (auto lineEdit = spinBox->findChild<QLineEdit*>(QString(), Qt::FindDirectChildrenOnly)) {
if (lineEdit->property(g_spinBoxIntializedValueName).isValid() && !lineEdit->property(g_spinBoxIntializedValueName).toBool()) {
lineEdit->setProperty(g_spinBoxIntializedValueName, true);
if (clearText) {
lineEdit->setText(QString());
}
}
}
}
QTimer* SpinBoxWatcher::SpinBoxScrollTimer(QAbstractSpinBox* spinBox)
{
QTimer* timer = nullptr;
if (spinBox)
{
timer = spinBox->findChild<QTimer*>(g_spinBoxScrollTimerName, Qt::FindDirectChildrenOnly);
}
return timer;
}
bool SpinBoxWatcher::handleMouseDragStepping(QAbstractSpinBox* spinBox, QEvent* event)
{
if (!spinBox || !event)
{
return false;
}
switch (event->type())
{
case QEvent::KeyPress:
{
setInializeSpinboxValue(spinBox, true);
break;
}
case QEvent::MouseButtonPress:
{
auto mouseEvent = static_cast<QMouseEvent*>(event);
bool buttonUpPressed = spinBox->property(g_spinBoxUpPressedPropertyName).toBool();
bool buttonDownPressed = spinBox->property(g_spinBoxDownPressedPropertyName).toBool();
if (((mouseEvent->button() & Qt::LeftButton) && (m_state == Inactive) && !buttonDownPressed && !buttonUpPressed) ||
(mouseEvent->button() & Qt::MiddleButton))
{
m_xPos = mouseEvent->x();
emitValueChangeBegan(spinBox);
m_state = Dragging;
spinBox->setProperty(g_spinBoxDraggingName, true);
}
setInializeSpinboxValue(spinBox);
break;
}
case QEvent::MouseButtonDblClick:
{
emitValueChangeBegan(spinBox);
break;
}
case QEvent::MouseMove:
{
auto mouseEvent = static_cast<QMouseEvent*>(event);
if ((m_state == Dragging) && (mouseEvent->buttons() & (Qt::LeftButton | Qt::MiddleButton)))
{
const int delta = mouseEvent->x() - m_xPos;
if (qAbs(delta) <= qAbs(m_config.pixelsPerStep))
{
break;
}
m_xPos = mouseEvent->x();
int step = delta > 0 ? 1 : -1;
if (mouseEvent->modifiers() & Qt::ShiftModifier)
{
step *= 10;
}
if (step > 0)
{
spinBox->setProperty(g_spinBoxValueIncreasingName, true);
spinBox->setProperty(g_spinBoxValueDecreasingName, false);
spinBox->setProperty(g_spinBoxScrollIncreasingName, true);
spinBox->setProperty(g_spinBoxScrollDecreasingName, false);
}
else
{
spinBox->setProperty(g_spinBoxValueIncreasingName, false);
spinBox->setProperty(g_spinBoxValueDecreasingName, true);
spinBox->setProperty(g_spinBoxScrollIncreasingName, false);
spinBox->setProperty(g_spinBoxScrollDecreasingName, true);
}
spinBox->stepBy(step);
// Check if we need to move the mouse cursor away from the edge of the screen. A
// native window may not exist.
if (QWindow* windowHandle = spinBox->window()->windowHandle())
{
QScreen* screen = windowHandle->screen();
const QPoint hotspot = spinBox->cursor().hotSpot();
const QRect screenRect = screen->geometry().adjusted(hotspot.x(), hotspot.y(), -hotspot.x(), -hotspot.y());
QPoint screenPos = mouseEvent->screenPos().toPoint();
const int xPos = screenPos.x();
int newXPos = xPos;
if (xPos >= screenRect.right())
{
newXPos = screenRect.right() - 1;
}
else if (xPos <= screenRect.left())
{
newXPos = screenRect.left() + 1;
}
if (newXPos != xPos)
{
// Update our local x position so we can continue to step when the mouse moves
const int screenDelta = xPos - newXPos;
m_xPos -= screenDelta;
// Move the mouse cursor away from the edge of the screen.
screenPos.setX(newXPos);
QCursor::setPos(screen, screenPos);
}
}
}
setInializeSpinboxValue(spinBox);
break;
}
case QEvent::MouseButtonRelease:
{
emitValueChangeEnded(spinBox);
m_state = Inactive;
spinBox->setProperty(g_spinBoxDraggingName, false);
spinBox->update();
resetCursor(spinBox);
break;
}
default:
break;
}
return false;
}
void SpinBoxWatcher::initStyleOption(QAbstractSpinBox* spinBox, QStyleOptionSpinBox* option)
{
Q_ASSERT(spinBox);
Q_ASSERT(option);
if (qobject_cast<QSpinBox*>(spinBox) || qobject_cast<QDoubleSpinBox*>(spinBox))
{
// QAbstractSpinBox::initStyleOption is protected so we have to replicate that logic here instead of using it directly.
enum QSpinBoxPrivateButton {
None = 0x000,
Keyboard = 0x001,
Mouse = 0x002,
Wheel = 0x004,
ButtonMask = 0x008,
Up = 0x010,
Down = 0x020,
DirectionMask = 0x040
};
option->initFrom(spinBox);
option->activeSubControls = QStyle::SC_None;
option->buttonSymbols = spinBox->buttonSymbols();
option->subControls = QStyle::SC_SpinBoxFrame | QStyle::SC_SpinBoxEditField;
bool buttonUpPressed = spinBox->property(g_spinBoxUpPressedPropertyName).toBool();
bool buttonDownPressed = spinBox->property(g_spinBoxDownPressedPropertyName).toBool();
if (option->buttonSymbols != QAbstractSpinBox::NoButtons)
{
option->subControls |= QStyle::SC_SpinBoxUp | QStyle::SC_SpinBoxDown;
if (buttonUpPressed)
{
option->activeSubControls = QStyle::SC_SpinBoxUp;
}
else if (buttonDownPressed)
{
option->activeSubControls = QStyle::SC_SpinBoxDown;
}
}
if (buttonUpPressed || buttonDownPressed)
{
option->state |= QStyle::State_Sunken;
}
else
{
option->activeSubControls = spinBox->property(g_hoverControlPropertyName).value<QStyle::SubControl>();
}
option->stepEnabled = spinBox->style()->styleHint(QStyle::SH_SpinControls_DisableOnBounds)
? stepEnabled(spinBox)
: (QAbstractSpinBox::StepDownEnabled | QAbstractSpinBox::StepUpEnabled);
option->frame = spinBox->hasFrame();
}
else
{
// Initialize it as best we can
option->initFrom(spinBox);
option->frame = spinBox->hasFrame();
option->subControls = QStyle::SC_SpinBoxFrame | QStyle::SC_SpinBoxEditField;
if (!(spinBox->buttonSymbols() & QAbstractSpinBox::NoButtons))
{
option->subControls |= QStyle::SC_SpinBoxUp | QStyle::SC_SpinBoxDown;
}
}
}
QAbstractSpinBox::StepEnabled SpinBoxWatcher::stepEnabled(QAbstractSpinBox* spinBox)
{
Q_ASSERT(spinBox);
// QAbstractSpinBox::stepEnabled is protected; we can access it in the DoubleSpinBox and SpinBox objects, but not for anything else.
QAbstractSpinBox::StepEnabled stepEnabled = QAbstractSpinBox::StepNone;
if (auto azSpinBox = qobject_cast<SpinBox*>(spinBox))
{
stepEnabled = azSpinBox->stepEnabled();
}
else if (auto azDoubleSpinBox = qobject_cast<DoubleSpinBox*>(spinBox))
{
stepEnabled = azDoubleSpinBox->stepEnabled();
}
else
{
if (spinBox->isReadOnly())
{
return QAbstractSpinBox::StepNone;
}
// Assume we can step
stepEnabled |= QAbstractSpinBox::StepUpEnabled;
stepEnabled |= QAbstractSpinBox::StepDownEnabled;
}
spinBox->setProperty(g_spinBoxMinReachedName, !(stepEnabled & QAbstractSpinBox::StepDownEnabled));
spinBox->setProperty(g_spinBoxMaxReachedName, !(stepEnabled & QAbstractSpinBox::StepUpEnabled));
return stepEnabled;
}
void SpinBoxWatcher::emitValueChangeBegan(QAbstractSpinBox* spinBox)
{
if (!m_spinBoxChanging.isNull())
{
Q_ASSERT(m_spinBoxChanging == spinBox);
return;
}
if (auto azSpinBox = qobject_cast<SpinBox*>(spinBox))
{
m_spinBoxChanging = spinBox;
emit azSpinBox->valueChangeBegan();
}
else if (auto azDoubleSpinBox = qobject_cast<DoubleSpinBox*>(spinBox))
{
m_spinBoxChanging = spinBox;
emit azDoubleSpinBox->valueChangeBegan();
}
}
void SpinBoxWatcher::emitValueChangeEnded(QAbstractSpinBox* spinBox)
{
if (m_spinBoxChanging.isNull())
{
return;
}
if (spinBox)
{
spinBox->setProperty(g_spinBoxValueIncreasingName, false);
spinBox->setProperty(g_spinBoxValueDecreasingName, false);
spinBox->setProperty(g_spinBoxScrollIncreasingName, false);
spinBox->setProperty(g_spinBoxScrollDecreasingName, false);
}
if (auto azSpinBox = qobject_cast<SpinBox*>(spinBox))
{
emit azSpinBox->valueChangeEnded();
m_spinBoxChanging.clear();
}
else if (auto azDoubleSpinBox = qobject_cast<DoubleSpinBox*>(spinBox))
{
emit azDoubleSpinBox->valueChangeEnded();
m_spinBoxChanging.clear();
}
}
void SpinBoxWatcher::resetCursor(QAbstractSpinBox* spinBox)
{
QStyleOptionSpinBox styleOption;
initStyleOption(spinBox, &styleOption);
const auto pos = spinBox->mapFromGlobal(QCursor::pos());
const QStyle::SubControl control = spinBox->style()->hitTestComplexControl(QStyle::CC_SpinBox,
&styleOption,
pos,
spinBox);
if (((control == QStyle::SC_SpinBoxUp) || (control == QStyle::SC_SpinBoxDown)) && m_state != Dragging)
{
spinBox->setProperty(g_hoverControlPropertyName, control);
spinBox->setCursor(Qt::ArrowCursor);
}
else
{
const auto enabledSteps = stepEnabled(spinBox);
if (spinBox->property(g_spinBoxScrollIncreasingName).toBool())
{
if (enabledSteps & QSpinBox::StepUpEnabled)
{
spinBox->setCursor(m_config.scrollCursorRight);
}
else
{
spinBox->setCursor(m_config.scrollCursorRightMax);
}
}
else if (spinBox->property(g_spinBoxScrollDecreasingName).toBool())
{
if (enabledSteps & QSpinBox::StepDownEnabled)
{
spinBox->setCursor(m_config.scrollCursorLeft);
}
else
{
spinBox->setCursor(m_config.scrollCursorLeftMax);
}
}
else
{
spinBox->setCursor(m_config.scrollCursor);
}
// Need to update lineEdit cursor in case we started dragging using the
// middle button on top of it
if (auto lineEdit = spinBox->findChild<QLineEdit*>(QString(), Qt::FindDirectChildrenOnly))
{
if (m_state == Dragging)
{
lineEdit->setCursor(spinBox->cursor());
}
else
{
lineEdit->setCursor(Qt::IBeamCursor);
}
}
spinBox->setProperty(g_hoverControlPropertyName, QStyle::SC_None);
}
}
SpinBox::Config SpinBox::loadConfig(QSettings& settings)
{
Config config = defaultConfig();
ConfigHelpers::read<int>(settings, QStringLiteral("PixelsPerStep"), config.pixelsPerStep);
ConfigHelpers::read<QCursor>(settings, QStringLiteral("ScrollCursor"), config.scrollCursor);
ConfigHelpers::read<QCursor>(settings, QStringLiteral("ScrollCursorLeft"), config.scrollCursorLeft);
ConfigHelpers::read<QCursor>(settings, QStringLiteral("ScrollCursorLeftMax"), config.scrollCursorLeftMax);
ConfigHelpers::read<QCursor>(settings, QStringLiteral("ScrollCursorRight"), config.scrollCursorRight);
ConfigHelpers::read<QCursor>(settings, QStringLiteral("ScrollCursorRightMax"), config.scrollCursorRightMax);
ConfigHelpers::read<int>(settings, QStringLiteral("LabelSize"), config.labelSize);
ConfigHelpers::read<bool>(settings, QStringLiteral("AutoSelectAllOnClickFocus"), config.autoSelectAllOnClickFocus);
return config;
}
SpinBox::Config SpinBox::defaultConfig()
{
Config config;
config.pixelsPerStep = 10;
config.scrollCursor = QCursor(QPixmap(":/SpinBox/scrollIconDefault.svg"));
config.scrollCursorLeft = QCursor(QPixmap(":/SpinBox/scrollIconLeft.svg"));
config.scrollCursorLeftMax = QCursor(QPixmap(":/SpinBox/scrollIconLeftMaxed.svg"));
config.scrollCursorRight = QCursor(QPixmap(":/SpinBox/scrollIconRight.svg"));
config.scrollCursorRightMax = QCursor(QPixmap(":/SpinBox/scrollIconRightMaxed.svg"));
config.labelSize = 16;
config.autoSelectAllOnClickFocus = true;
return config;
}
void SpinBox::setHasError(QAbstractSpinBox* spinbox, bool hasError)
{
if (hasError != spinbox->property(HasError).toBool())
{
spinbox->setProperty(HasError, hasError);
}
}
QPointer<SpinBoxWatcher> SpinBox::s_spinBoxWatcher = nullptr;
unsigned int SpinBox::s_watcherReferenceCount = 0;
void SpinBox::initializeWatcher()
{
if (!s_spinBoxWatcher)
{
Q_ASSERT(s_watcherReferenceCount == 0);
s_spinBoxWatcher = new SpinBoxWatcher;
}
++s_watcherReferenceCount;
}
void SpinBox::uninitializeWatcher()
{
Q_ASSERT(!s_spinBoxWatcher.isNull());
Q_ASSERT(s_watcherReferenceCount > 0);
--s_watcherReferenceCount;
if (s_watcherReferenceCount == 0)
{
delete s_spinBoxWatcher;
s_spinBoxWatcher = nullptr;
}
}
bool SpinBox::drawSpinBox(const QProxyStyle* style, const QStyleOption* option, QPainter* painter, const QWidget* widget, const SpinBox::Config& config)
{
Q_UNUSED(config);
if (const auto styleOption = qstyleoption_cast<const QStyleOptionSpinBox*>(option))
{
// Only draw the up and down controls if the spinbox is enabled and has the mouse over it
if (!(styleOption->state & QStyle::State_Enabled) || !(styleOption->state & QStyle::State_MouseOver))
{
QStyleOptionSpinBox copy = *styleOption;
copy.subControls &= ~QStyle::SC_SpinBoxUp;
copy.subControls &= ~QStyle::SC_SpinBoxDown;
style->baseStyle()->drawComplexControl(QStyle::CC_SpinBox, &copy, painter, widget);
return true;
}
else if (widget->property(g_spinBoxValueIncreasingName).toBool())
{
QStyleOptionSpinBox copy = *styleOption;
copy.activeSubControls = QStyle::SC_SpinBoxUp;
copy.state |= QStyle::State_Sunken;
style->baseStyle()->drawComplexControl(QStyle::CC_SpinBox, &copy, painter, widget);
return true;
}
else if (widget->property(g_spinBoxValueDecreasingName).toBool())
{
QStyleOptionSpinBox copy = *styleOption;
copy.activeSubControls = QStyle::SC_SpinBoxDown;
copy.state |= QStyle::State_Sunken;
style->baseStyle()->drawComplexControl(QStyle::CC_SpinBox, &copy, painter, widget);
return true;
}
}
return false;
}
QRect SpinBox::editFieldRect(const QProxyStyle* style, const QStyleOptionComplex* option, const QWidget* widget, const Config& config)
{
Q_UNUSED(config);
if (auto spinBoxOption = qstyleoption_cast<const QStyleOptionSpinBox *>(option))
{
// Long values should run off the right side of the spinbox, so extend the SC_SpinBoxEditField
// to the edge of the spinbox contents but don't overlap with the buttons
const auto baseRect = style->baseStyle()->subControlRect(QStyle::CC_SpinBox,
option,
QStyle::SC_SpinBoxEditField,
widget);
const auto buttonRect = style->baseStyle()->subControlRect(QStyle::CC_SpinBox,
option,
QStyle::SC_SpinBoxUp,
widget);
QRect rect(baseRect);
rect.setRight(buttonRect.x());
rect.adjust(0, 0, -1, 0); // Don't overlap buttonRect or the spinbox border
if (!(spinBoxOption->state & QStyle::State_Enabled) ||
!(spinBoxOption->state & QStyle::State_MouseOver))
{
// The buttons aren't visible, so extend the QLineEdit to the inside of the spinbox border
rect.adjust(0, 0, buttonRect.width(), 0);
}
else
{
// When the buttons are visible, we need remove an extra button width since the buttons are now
// placed horizontally.
rect.adjust(0, 0, -buttonRect.width(), 0);
}
return rect;
}
return QRect();
}
bool SpinBox::polish(QProxyStyle* style, QWidget* widget, const SpinBox::Config& config)
{
Q_UNUSED(style);
Q_UNUSED(config);
Q_ASSERT(!s_spinBoxWatcher.isNull());
if (!qobject_cast<QSpinBox*>(widget) && !qobject_cast<QDoubleSpinBox*>(widget))
{
// QDateTime inherits from QAbstractSpinBox and shouldn't be included here.
return false;
}
if (auto spinBox = qobject_cast<QAbstractSpinBox*>(widget))
{
Style* newStyle = qobject_cast<Style*>(style);
if (newStyle)
{
newStyle->repolishOnSettingsChange(spinBox);
}
s_spinBoxWatcher->m_config = config;
// Initialize spinBox properties, and do it only once
if (!spinBox->property(g_spinBoxInitPropertyFlagName).toBool())
{
spinBox->setProperty(g_spinBoxUpPressedPropertyName, false);
spinBox->setProperty(g_spinBoxDownPressedPropertyName, false);
spinBox->setProperty(g_spinBoxValueIncreasingName, false);
spinBox->setProperty(g_spinBoxValueDecreasingName, false);
spinBox->setProperty(g_spinBoxScrollIncreasingName, false);
spinBox->setProperty(g_spinBoxScrollDecreasingName, false);
spinBox->setProperty(g_spinBoxIntializedValueName, false);
spinBox->setProperty(g_spinBoxMinReachedName, false);
spinBox->setProperty(g_spinBoxMaxReachedName, false);
spinBox->setProperty(g_spinBoxFocusedName, false);
spinBox->setProperty(g_spinBoxDraggingName, false);
spinBox->setProperty(g_spinBoxInitPropertyFlagName, true);
}
spinBox->installEventFilter(s_spinBoxWatcher);
SpinBox::setButtonSymbolsForStyle(spinBox);
if (auto lineEdit = spinBox->findChild<QLineEdit*>(QString(), Qt::FindDirectChildrenOnly))
{
lineEdit->installEventFilter(s_spinBoxWatcher);
LineEdit::setErrorIconEnabled(lineEdit, false);
}
return true;
}
return false;
}
bool SpinBox::unpolish(QProxyStyle* style, QWidget* widget, const SpinBox::Config& config)
{
Q_UNUSED(style);
Q_UNUSED(config);
Q_ASSERT(!s_spinBoxWatcher.isNull());
if (!qobject_cast<QSpinBox*>(widget) && !qobject_cast<QDoubleSpinBox*>(widget))
{
// QDateTime inherits from QAbstractSpinBox and shouldn't be included here.
return false;
}
if (auto spinBox = qobject_cast<QAbstractSpinBox*>(widget))
{
spinBox->removeEventFilter(s_spinBoxWatcher);
SpinBox::setButtonSymbolsForStyle(spinBox);
if (auto lineEdit = spinBox->findChild<QLineEdit*>(QString(), Qt::FindDirectChildrenOnly))
{
lineEdit->removeEventFilter(s_spinBoxWatcher);
}
return true;
}
return false;
}
void SpinBox::setButtonSymbolsForStyle(QAbstractSpinBox* spinBox)
{
if (!spinBox)
{
return;
}
spinBox->setButtonSymbols(QAbstractSpinBox::UpDownArrows);
}
SpinBox::SpinBox(QWidget* parent)
: QSpinBox(parent)
{
#if !defined(AZ_PLATFORM_LINUX)
#if QT_VERSION < QT_VERSION_CHECK(5,11,1)
setShiftIncreasesStepRate(true);
#endif
#endif //!defined(AZ_PLATFORM_LINUX)
m_lineEdit = new internal::SpinBoxLineEdit(this);
connect(m_lineEdit, &internal::SpinBoxLineEdit::globalUndoTriggered, this, &SpinBox::globalUndoTriggered);
connect(m_lineEdit, &internal::SpinBoxLineEdit::globalRedoTriggered, this, &SpinBox::globalRedoTriggered);
connect(this, &SpinBox::cutTriggered, this, [this]()
{
setInitialValueWasSetting(true);
m_lineEdit->cut();
});
connect(this, &SpinBox::copyTriggered, this, [this]()
{
m_lineEdit->copy();
});
connect(this, &SpinBox::pasteTriggered, this, [this]()
{
setInitialValueWasSetting(true);
m_lineEdit->setText(QString());
m_lineEdit->paste();
});
connect(this, &SpinBox::deleteTriggered, this, [this]()
{
setInitialValueWasSetting(true);
m_lineEdit->del();
});
connect(this, QOverload<int>::of(&SpinBox::valueChanged), this, &SpinBox::setLastValue);
setLineEdit(m_lineEdit);
setButtonSymbolsForStyle(this);
setRange(0, 100);
}
void SpinBox::setValue(int value)
{
QSpinBox::setValue(value);
setLastValue(value);
}
QSize SpinBox::minimumSizeHint() const
{
// This prevents the range from affecting the size, allowing the use of user-defined size hints.
QSize size = QSpinBox::sizeHint();
size.setWidth(minimumWidth());
return size;
}
bool SpinBox::isUndoAvailable() const
{
return lineEdit()->isUndoAvailable();
}
bool SpinBox::isRedoAvailable() const
{
return lineEdit()->isRedoAvailable();
}
void SpinBox::focusInEvent(QFocusEvent* event)
{
// Remove the suffix while editing. Check focus reason so we don't clash
// with context menus
if (event->reason() != Qt::PopupFocusReason)
{
m_lastSuffix = suffix();
}
if (m_lastSuffix.length() > 0)
{
// Force the minimum size to the current size to prevent shrinkage when
// the suffix is removed.
m_lastMinimumSize = minimumSize();
setMinimumSize(size());
setSuffix(QString());
}
QSpinBox::focusInEvent(event);
if (event->reason() == Qt::MouseFocusReason)
{
lineEdit()->deselect();
}
}
void SpinBox::focusOutEvent(QFocusEvent* event)
{
QSpinBox::focusOutEvent(event);
// restore the suffix now, if needed
if (event->reason() != Qt::PopupFocusReason && m_lastSuffix.length() > 0)
{
setSuffix(m_lastSuffix);
// Restore the original minimum size
setMinimumSize(m_lastMinimumSize);
m_lastSuffix.clear();
m_lastMinimumSize = {};
}
}
static QAction* findAction(QList<QAction*>& actions, const QString& actionText)
{
// would be much smarter to use QKeySequences to identify the actions we're
// looking for, but QLineEdit doesn't actually use them in the same way
// anything else does, so we can't
for (QAction* action : actions)
{
if (action->text().contains(actionText))
{
return action;
}
}
return nullptr;
}
template <typename SpinBoxType>
void spinBoxContextMenuEvent(QContextMenuEvent* ev, SpinBoxType* spinBox, QLineEdit* lineEdit, uint stepEnabled, bool overrideUndoRedo)
{
// We want to override the context menu's undo/redo in the case that the lineEdit control does not currently have any undo/redo available
// but the QAbstractSpinBox doesn't really expose any way to do that nicely.
// As a result, we have to replicate the context menu code from QAbstractSpinBox instead,
// and also pull out the undo and redo actions so that we can override them.
if (QMenu* menu = lineEdit->createStandardContextMenu())
{
auto menuActions = menu->actions();
QAction* undoAction = findAction(menuActions, QObject::tr("&Undo"));
if (undoAction && overrideUndoRedo)
{
QObject::disconnect(undoAction, &QAction::triggered, nullptr, nullptr);
QObject::connect(undoAction, &QAction::triggered, spinBox, &SpinBoxType::globalUndoTriggered);
}
QAction* redoAction = findAction(menuActions, QObject::tr("&Redo"));
if (redoAction && overrideUndoRedo)
{
QObject::disconnect(redoAction, &QAction::triggered, nullptr, nullptr);
QObject::connect(redoAction, &QAction::triggered, spinBox, &SpinBoxType::globalRedoTriggered);
}
QAction* selectAllAction = findAction(menuActions, QObject::tr("Select All"));
if (selectAllAction)
{
QObject::disconnect(selectAllAction, &QAction::triggered, nullptr, nullptr);
QObject::connect(selectAllAction, &QAction::triggered, spinBox, &QAbstractSpinBox::selectAll);
}
QAction* cutAction = findAction(menuActions, QObject::tr("Cu&t"));
if (cutAction)
{
QObject::disconnect(cutAction, &QAction::triggered, nullptr, nullptr);
QObject::connect(cutAction, &QAction::triggered, spinBox, &SpinBoxType::cutTriggered);
}
QAction* copyAction = findAction(menuActions, QObject::tr("&Copy"));
if (copyAction)
{
QObject::disconnect(copyAction, &QAction::triggered, nullptr, nullptr);
QObject::connect(copyAction, &QAction::triggered, spinBox, &SpinBoxType::copyTriggered);
}
QAction* pasteAction = findAction(menuActions, QObject::tr("&Paste"));
if (pasteAction)
{
QObject::disconnect(pasteAction, &QAction::triggered, nullptr, nullptr);
QObject::connect(pasteAction, &QAction::triggered, spinBox, &SpinBoxType::pasteTriggered);
}
QAction* deleteAction = findAction(menuActions, QObject::tr("Delete"));
if (deleteAction)
{
QObject::disconnect(deleteAction, &QAction::triggered, nullptr, nullptr);
QObject::connect(deleteAction, &QAction::triggered, spinBox, &SpinBoxType::deleteTriggered);
}
menu->addSeparator();
QAction* upAction = menu->addAction(QObject::tr("&Step up"));
upAction->setEnabled(stepEnabled & QAbstractSpinBox::StepUpEnabled);
QObject::connect(upAction, &QAction::triggered, spinBox, [spinBox] {
spinBox->setInitialValueWasSetting(true);
spinBox->stepBy(1);
});
QAction* downAction = menu->addAction(QObject::tr("Step &down"));
downAction->setEnabled(stepEnabled & QAbstractSpinBox::StepDownEnabled);
QObject::connect(downAction, &QAction::triggered, spinBox, [spinBox] {
spinBox->setInitialValueWasSetting(true);
spinBox->stepBy(-1);
});
Q_EMIT spinBox->contextMenuAboutToShow(menu, undoAction, redoAction);
const QPoint pos = (ev->reason() == QContextMenuEvent::Mouse)
? ev->globalPos() : spinBox->mapToGlobal(QPoint(ev->pos().x(), 0)) + QPoint(spinBox->width() / 2, spinBox->height() / 2);
menu->setAttribute(Qt::WA_DeleteOnClose);
menu->popup(pos);
ev->accept();
}
}
void SpinBox::setInitialValueWasSetting(bool b)
{
lineEdit()->setProperty(g_spinBoxIntializedValueName, b);
}
void SpinBox::contextMenuEvent(QContextMenuEvent* ev)
{
spinBoxContextMenuEvent(ev, this, lineEdit(), stepEnabled(), m_lineEdit->overrideUndoRedo());
}
void SpinBox::setLastValue(int v)
{
m_lastValue = v;
}
DoubleSpinBox::DoubleSpinBox(QWidget* parent)
: QDoubleSpinBox(parent)
, m_displayDecimals(g_decimalDisplayPrecisionDefault)
{
#if !defined(AZ_PLATFORM_LINUX)
#if QT_VERSION < QT_VERSION_CHECK(5,11,1)
setShiftIncreasesStepRate(true);
#endif
#endif // !defined(AZ_PLATFORM_LINUX)
m_lineEdit = new internal::SpinBoxLineEdit(this);
connect(m_lineEdit, &internal::SpinBoxLineEdit::globalUndoTriggered, this, &DoubleSpinBox::globalUndoTriggered);
connect(m_lineEdit, &internal::SpinBoxLineEdit::globalRedoTriggered, this, &DoubleSpinBox::globalRedoTriggered);
connect(this, &DoubleSpinBox::cutTriggered, this, [this]()
{
setInitialValueWasSetting(true);
m_lineEdit->cut();
});
connect(this, &DoubleSpinBox::copyTriggered, this, [this]()
{
m_lineEdit->copy();
});
connect(this, &DoubleSpinBox::pasteTriggered, this, [this]()
{
setInitialValueWasSetting(true);
m_lineEdit->setText(QString());
m_lineEdit->paste();
});
connect(this, &DoubleSpinBox::deleteTriggered, this, [this]()
{
setInitialValueWasSetting(true);
m_lineEdit->del();
});
setLineEdit(m_lineEdit);
SpinBox::setButtonSymbolsForStyle(this);
setRange(0, 100);
setDecimals(g_decimalPrecisonDefault);
// Our tooltip will be the full decimal value, so keep it updated
// whenever our value changes
connect(this, QOverload<double>::of(&DoubleSpinBox::valueChanged), this, &DoubleSpinBox::updateToolTip);
connect(this, QOverload<double>::of(&DoubleSpinBox::valueChanged), this, &DoubleSpinBox::setLastValue);
updateToolTip(value());
}
void DoubleSpinBox::setValue(double value)
{
QDoubleSpinBox::setValue(value);
setLastValue(value);
updateToolTip(value);
}
QSize DoubleSpinBox::minimumSizeHint() const
{
// This prevents the range from affecting the size, allowing the use of user-defined size hints.
QSize size = QDoubleSpinBox::sizeHint();
size.setWidth(minimumWidth());
return size;
}
bool DoubleSpinBox::isUndoAvailable() const
{
return lineEdit()->isUndoAvailable();
}
bool DoubleSpinBox::isRedoAvailable() const
{
return lineEdit()->isRedoAvailable();
}
bool DoubleSpinBox::isEditing() const
{
return SpinBox::s_spinBoxWatcher->isEditing(this);
}
void DoubleSpinBox::contextMenuEvent(QContextMenuEvent* ev)
{
spinBoxContextMenuEvent(ev, this, lineEdit(), stepEnabled(), m_lineEdit->overrideUndoRedo());
}
QString DoubleSpinBox::stringValue(double value, bool truncated) const
{
// Determine which decimal precision to use for displaying the value
int numDecimals = decimals();
if (truncated && m_displayDecimals < numDecimals)
{
numDecimals = m_displayDecimals;
}
else if (value == std::floor(value) && (!m_options.testFlag(SHOW_ONE_DECIMAL_PLACE_ALWAYS)))
{
numDecimals = 0;
}
return toString(value, numDecimals, locale(), isGroupSeparatorShown());
}
void DoubleSpinBox::updateToolTip(double value)
{
// Set our tooltip to the full decimal value
setToolTip(stringValue(value));
}
void DoubleSpinBox::setLastValue(double v)
{
m_lastValue = v;
}
void DoubleSpinBox::setInitialValueWasSetting(bool b)
{
lineEdit()->setProperty(g_spinBoxIntializedValueName, b);
}
QString DoubleSpinBox::textFromValue(double value) const
{
// If our widget is focused, then show the full decimal value, otherwise
// show the truncated value
return stringValue(value, !hasFocus());
}
void DoubleSpinBox::focusInEvent(QFocusEvent* event)
{
// We need to set the special value text to an empty string, which
// effectively makes no change, but actually triggers the line edit
// display value to be updated so that when we receive focus to
// begin editing, we display the full decimal precision instead of
// the truncated display value
setSpecialValueText(QString());
// Remove the suffix while editing. Check focus reason so we don't clash
// with context menus
if (event->reason() != Qt::PopupFocusReason)
{
m_lastSuffix = suffix();
}
if (m_lastSuffix.length() > 0)
{
// Force the minimum size to the current size to prevent shrinkage when
// the suffix is removed.
m_lastMinimumSize = minimumSize();
setMinimumSize(size());
setSuffix(QString());
}
QDoubleSpinBox::focusInEvent(event);
if (event->reason() == Qt::MouseFocusReason)
{
lineEdit()->deselect();
}
}
void DoubleSpinBox::focusOutEvent(QFocusEvent* event)
{
QDoubleSpinBox::focusOutEvent(event);
// restore the suffix now, if needed
if (event->reason() != Qt::PopupFocusReason && m_lastSuffix.length() > 0)
{
setSuffix(m_lastSuffix);
// Restore the original minimum size
setMinimumSize(m_lastMinimumSize);
m_lastSuffix.clear();
m_lastMinimumSize = {};
}
}
namespace internal
{
SpinBoxLineEdit::SpinBoxLineEdit(QWidget* parent)
: QLineEdit(parent)
{
setMouseTracking(true);
}
bool SpinBoxLineEdit::overrideUndoRedo() const
{
return !isUndoAvailable() && !isRedoAvailable() && !isReadOnly();
}
bool SpinBoxLineEdit::event(QEvent* ev)
{
switch (ev->type())
{
case QEvent::FocusOut:
{
// Explicitly set the text again on focusOut, so that the undo/redo queue for the line edit clears and the global undo/redo can kick in
if (const auto focusEvent = static_cast<QFocusEvent*>(ev))
{
if (focusEvent->reason() != Qt::PopupFocusReason)
{
setText(text());
}
}
}
break;
}
return QLineEdit::event(ev);
}
void SpinBoxLineEdit::keyPressEvent(QKeyEvent* ev)
{
if (overrideUndoRedo())
{
// QLineEdit overrides the key press event handler
// so we have to trap that too, directly, without assuming
// that QAction's with shortcuts will do it.
if (ev->matches(QKeySequence::Undo))
{
Q_EMIT globalUndoTriggered();
ev->accept();
return;
}
else if (ev->matches(QKeySequence::Redo))
{
Q_EMIT globalRedoTriggered();
ev->accept();
return;
}
else if (ev->matches(QKeySequence::SelectAll))
{
Q_EMIT selectAllTriggered();
ev->accept();
return;
}
}
if (ev->matches(QKeySequence::Cut))
{
Q_EMIT cutTriggered();
}
if (ev->matches(QKeySequence::Copy))
{
Q_EMIT copyTriggered();
}
if (ev->matches(QKeySequence::Paste))
{
Q_EMIT pasteTriggered();
}
if (ev->matches(QKeySequence::Delete))
{
Q_EMIT deleteTriggered();
}
// fall through to the default
QLineEdit::keyPressEvent(ev);
}
static void setLineEditTextFormat(QLineEdit* lineEdit, const QList<QTextLayout::FormatRange>& formats)
{
if (!lineEdit)
return;
QList<QInputMethodEvent::Attribute> attributes;
foreach (const QTextLayout::FormatRange& fr, formats)
{
QInputMethodEvent::AttributeType type = QInputMethodEvent::TextFormat;
int start = fr.start;
int length = fr.length;
QVariant value = fr.format;
attributes.append(QInputMethodEvent::Attribute(type, start, length, value));
}
QInputMethodEvent event(QString(), attributes);
QCoreApplication::sendEvent(lineEdit, &event);
}
static void clearLineEditTextFormat(QLineEdit* lineEdit)
{
setLineEditTextFormat(lineEdit, QList<QTextLayout::FormatRange>());
}
void SpinBoxLineEdit::paintEvent(QPaintEvent* event)
{
QAbstractSpinBox* abstractSpinBox = qobject_cast<QAbstractSpinBox*>(parent());
QSpinBox* spinBox = reinterpret_cast<QSpinBox*>(abstractSpinBox);
bool fixLeftAlignment = !spinBox->property(g_hoveredPropertyName).toBool() && !hasFocus();
int suffixSize = spinBox->suffix().size();
QString beforeText;
int cursorPos = 0;
if (fixLeftAlignment)
{
// Reset the cursorposition to ensure left-aligned values
cursorPos = cursorPosition();
setCursorPosition(0);
}
if (suffixSize)
{
QList<QTextLayout::FormatRange> formats;
int size = text().size();
QTextCharFormat f;
f.setForeground(QColor(0x888888));
QTextLayout::FormatRange fr_task;
fr_task.start = size - suffixSize;
fr_task.length = suffixSize;
fr_task.format = f;
formats.append(fr_task);
setLineEditTextFormat(this, formats);
}
QLineEdit::paintEvent(event);
if (suffixSize)
{
clearLineEditTextFormat(this);
}
if (fixLeftAlignment)
{
setCursorPosition(cursorPos);
}
}
} // namespace internal
} // namespace AzQtComponents
#include "Components/Widgets/moc_SpinBox.cpp"