Gui: improve quantitySpinbox / Sketcher OVP (#25978)

* Gui: fix quantity spinbox size not adapting to number of decimals

* Gui: fix quantity spinbox units getting cut (force max chars)

* Gui: editableDatumLabel remove hardcoded values

* fixes

* Gui: sketcher ovp auto adjust size

* Gui: ovp fix cursor jumping behind the unit

* Gui: Refactor changes to QuantitySpinBox

The QuantitySpinBox uses d-pointer (pimpl) to store its private data -
this commit ensures that all state is stored here and exposed via
getters and setters + qproperties.

* Gui: sketcher ovp fix cursor jumping after the unit

* Gui: allow quantitySpinBox to set max digits allowed

Useful for adjustable width in the Sketcher OVP

---------

Co-authored-by: Kacper Donat <kadet1090@gmail.com>
This commit is contained in:
Alfredo Monclus
2026-03-16 14:41:00 -03:00
committed by GitHub
parent d384c9d8cb
commit cfe641fb7d
5 changed files with 185 additions and 79 deletions
+56 -50
View File
@@ -29,9 +29,11 @@
#include <Inventor/nodes/SoSwitch.h>
#include <QEvent>
#include <QFontMetrics>
#include <QKeyEvent>
#include <QPixmap>
#include <QLabel>
#include <QLineEdit>
#include <QHBoxLayout>
#include <QString>
@@ -41,6 +43,8 @@
#include <Gui/View3DInventorViewer.h>
#include "EditableDatumLabel.h"
#include "Base/Console.h"
#include "Gui/QuantitySpinBox.h"
using namespace Gui;
@@ -175,6 +179,8 @@ void EditableDatumLabel::startEdit(double val, QObject* eventFilteringObj, bool
spinBox->setAutoNormalize(false);
spinBox->setKeyboardTracking(true);
spinBox->installEventFilter(this);
spinBox->setAutoAdjustWidth(true);
spinBox->setMaxExpectedDigits(16);
if (eventFilteringObj) {
spinBox->installEventFilter(eventFilteringObj);
@@ -185,10 +191,7 @@ void EditableDatumLabel::startEdit(double val, QObject* eventFilteringObj, bool
}
spinBox->show();
setSpinboxValue(val);
// Note: adjustSize apparently uses the Min/Max values to set the size. So if we don't set them
// to INT_MAX, the spinbox are much too big.
spinBox->adjustSize();
updateGeometry();
setFocusToSpinbox();
const auto validateAndFinish = [this]() {
@@ -217,6 +220,9 @@ void EditableDatumLabel::startEdit(double val, QObject* eventFilteringObj, bool
};
connect(spinBox, qOverload<double>(&QuantitySpinBox::valueChanged), this, validateAndFinish);
if (auto* edit = spinBox->findChild<QLineEdit*>()) {
connect(edit, &QLineEdit::textChanged, this, &EditableDatumLabel::updateGeometry);
}
}
bool EditableDatumLabel::eventFilter(QObject* watched, QEvent* event)
@@ -364,8 +370,9 @@ void EditableDatumLabel::positionSpinbox()
// Update lock icon position inside the spinbox if it exists and is visible
if (lockIconLabel && lockIconLabel->isVisible()) {
int iconSize = 14;
int padding = 4;
const QFontMetrics fm(spinBox->fontMetrics());
int iconSize = fm.height();
int padding = spinBox->getMargin();
QSize spinboxSize = spinBox->size();
lockIconLabel->setGeometry(
spinboxSize.width() - iconSize - padding,
@@ -424,6 +431,14 @@ void EditableDatumLabel::setPlacement(const Base::Placement& plc)
label->norm.setValue(SbVec3f(float(RN.x), float(RN.y), float(RN.z)));
}
void EditableDatumLabel::updateGeometry()
{
if (!spinBox) {
return;
}
spinBox->adjustSize();
}
// NOLINTNEXTLINE
void EditableDatumLabel::setColor(SbColor color)
{
@@ -510,56 +525,47 @@ void EditableDatumLabel::setSpinboxVisibleToMouse(bool val)
void EditableDatumLabel::setLockedAppearance(bool locked)
{
if (!locked) {
this->hasFinishedEditing = false;
}
if (!spinBox) {
return;
}
const QFontMetrics fm(spinBox->fontMetrics());
int iconSize = fm.height();
int padding = spinBox->getMargin();
if (locked) {
if (spinBox) {
spinBox->addIconSpace(true);
spinBox->adjustSize();
// create lock icon label it it doesn't exist, if it does - show it
if (!lockIconLabel) {
lockIconLabel = new QLabel(spinBox);
lockIconLabel->setAttribute(Qt::WA_TransparentForMouseEvents, true);
lockIconLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
// create lock icon label it it doesn't exist, if it does - show it
if (!lockIconLabel) {
lockIconLabel = new QLabel(spinBox);
lockIconLabel->setAttribute(Qt::WA_TransparentForMouseEvents, true);
lockIconLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
// load icon and scale it to fit in spinbox
QPixmap lockIcon = Gui::BitmapFactory().pixmap("Constraint_Lock");
QPixmap scaledIcon
= lockIcon.scaled(iconSize, iconSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
lockIconLabel->setPixmap(scaledIcon);
// load icon and scale it to fit in spinbox
QPixmap lockIcon = Gui::BitmapFactory().pixmap("Constraint_Lock");
QPixmap scaledIcon
= lockIcon.scaled(14, 14, Qt::KeepAspectRatio, Qt::SmoothTransformation);
lockIconLabel->setPixmap(scaledIcon);
// position lock icon inside the spinbox
int iconSize = 14;
int padding = 4;
QSize spinboxSize = spinBox->size();
lockIconLabel->setGeometry(
spinboxSize.width() - iconSize - padding,
(spinboxSize.height() - iconSize) / 2,
iconSize,
iconSize
);
// style spinbox and add padding for lock
QString styleSheet = QString::fromLatin1(
"QSpinBox { "
"padding-right: %1px; "
"}"
)
.arg(iconSize + padding + 2);
spinBox->setStyleSheet(styleSheet);
}
lockIconLabel->show();
// position lock icon inside the spinbox
QSize spinboxSize = spinBox->size();
lockIconLabel->setGeometry(
spinboxSize.width() - iconSize - padding,
(spinboxSize.height() - iconSize) / 2,
iconSize,
iconSize
);
}
lockIconLabel->show();
}
else {
this->hasFinishedEditing = false;
// if spinbox exists, reset its appearance
if (spinBox) {
spinBox->setStyleSheet(QString());
// hide lock icon if it exists for later reuse
if (lockIconLabel) {
lockIconLabel->hide();
}
// hide lock icon if it exists for later reuse
if (lockIconLabel) {
lockIconLabel->hide();
spinBox->addIconSpace(false);
spinBox->adjustSize();
}
}
}
+1
View File
@@ -92,6 +92,7 @@ public:
void setLockedAppearance(bool locked); ///< Sets visual appearance to indicate locked state
///< (finished editing)
void resetLockedState(); ///< Resets both hasFinishedEditing flag and locked appearance
void updateGeometry();
Function getFunction();
+99 -26
View File
@@ -66,6 +66,9 @@ public:
, pendingEmit(false)
, normalize(true)
, checkRangeInExpression(false)
, adjustableWidth(false)
, maxExpectedDigits(4)
, addIconSpace(false)
, unitValue(0)
, maximum(std::numeric_limits<double>::max())
, minimum(-std::numeric_limits<double>::max())
@@ -306,6 +309,9 @@ public:
bool pendingEmit;
bool normalize;
bool checkRangeInExpression;
bool adjustableWidth;
int maxExpectedDigits;
bool addIconSpace;
QString validStr;
Base::Quantity quantity;
Base::Quantity cached;
@@ -341,6 +347,9 @@ void QuantitySpinBox::bind(const App::ObjectIdentifier& _path)
void QuantitySpinBox::showIcon()
{
addIconSpace(true);
adjustSize();
iconLabel->show();
}
@@ -444,6 +453,19 @@ void QuantitySpinBox::resizeEvent(QResizeEvent* event)
{
QAbstractSpinBox::resizeEvent(event);
resizeWidget();
moveCursor();
}
void QuantitySpinBox::moveCursor()
{
Q_D(QuantitySpinBox);
QLineEdit* edit = lineEdit();
int cursor = edit->cursorPosition();
const int maxCursor = qMax(0, edit->displayText().size() - 1 /*space*/ - d->unitStr.size());
if (cursor > maxCursor) {
edit->setCursorPosition(maxCursor);
}
}
void Gui::QuantitySpinBox::keyPressEvent(QKeyEvent* event)
@@ -467,6 +489,9 @@ void Gui::QuantitySpinBox::keyPressEvent(QKeyEvent* event)
if (isEnter) {
returnPressed();
}
else {
moveCursor();
}
}
void Gui::QuantitySpinBox::paintEvent(QPaintEvent*)
@@ -504,7 +529,7 @@ void QuantitySpinBox::updateEdit(const QString& text)
edit->setSelection(0, cursor);
}
else {
edit->setCursorPosition(empty ? 0 : cursor);
moveCursor();
}
}
@@ -601,6 +626,42 @@ void QuantitySpinBox::setAutoNormalize(bool normalize)
d->normalize = normalize;
}
bool QuantitySpinBox::autoAdjustWidth() const
{
Q_D(const QuantitySpinBox);
return d->adjustableWidth;
}
void QuantitySpinBox::setAutoAdjustWidth(bool adjust)
{
Q_D(QuantitySpinBox);
d->adjustableWidth = adjust;
}
bool QuantitySpinBox::isIconSpaceAdded() const
{
Q_D(const QuantitySpinBox);
return d->addIconSpace;
}
void QuantitySpinBox::addIconSpace(bool addIconSpace)
{
Q_D(QuantitySpinBox);
d->addIconSpace = addIconSpace;
}
int QuantitySpinBox::getMaxExpectedDigits()
{
Q_D(const QuantitySpinBox);
return d->maxExpectedDigits;
}
void QuantitySpinBox::setMaxExpectedDigits(int digits)
{
Q_D(QuantitySpinBox);
d->maxExpectedDigits = digits;
}
bool QuantitySpinBox::hasValidInput() const
{
Q_D(const QuantitySpinBox);
@@ -611,7 +672,6 @@ bool QuantitySpinBox::hasValidInput() const
void QuantitySpinBox::userInput(const QString& text)
{
Q_D(QuantitySpinBox);
d->pendingEmit = true;
QString tmp = text;
@@ -911,44 +971,57 @@ QSize QuantitySpinBox::sizeForText(const QString& txt) const
QSize QuantitySpinBox::sizeHint() const
{
return sizeHintCalculator(lineEdit()->sizeHint().height());
Q_D(const QuantitySpinBox);
auto le = lineEdit();
if (le && d->adjustableWidth) {
// limit number of typed characters to keep unit visible
le->setMaxLength(getMaxStrLength(d->maxExpectedDigits));
}
return sizeHintForDigits(d->maxExpectedDigits);
}
QSize QuantitySpinBox::minimumSizeHint() const
{
return sizeHintCalculator(lineEdit()->minimumSizeHint().height());
}
QSize QuantitySpinBox::sizeHintCalculator(int h) const
QSize QuantitySpinBox::sizeHintForDigits(int digits) const
{
Q_D(const QuantitySpinBox);
ensurePolished();
const int maxLen = getMaxStrLength(digits);
int length = maxLen;
if (d->adjustableWidth) {
int currenLen = qMax(lineEdit()->text().length(), 4);
length = currenLen < maxLen ? currenLen : maxLen;
}
QString longestString = QStringLiteral("8").repeated(length);
const QFontMetrics fm(fontMetrics());
int w = 0;
constexpr int maxStrLen = 9;
QString s;
QString fixedContent = QLatin1String(" ");
Base::Quantity q(d->quantity);
q.setValue(d->maximum);
s = textFromValue(q);
s.truncate(maxStrLen);
s += fixedContent;
w = qMax(w, QtTools::horizontalAdvance(fm, s));
w += 2; // cursor blinking space
w += iconHeight;
int w = qMax(0, QtTools::horizontalAdvance(fm, longestString));
w += 4; // cursor blinking space
if (d->addIconSpace) {
w += iconHeight;
}
QStyleOptionSpinBox opt;
initStyleOption(&opt);
QSize hint(w, h);
QSize hint(w, lineEdit()->sizeHint().height());
QSize size = style()->sizeFromContents(QStyle::CT_SpinBox, &opt, hint, this);
return size;
}
int QuantitySpinBox::getMaxStrLength(int digits) const
{
// Calculates the length of the longest string allowed
Q_D(const QuantitySpinBox);
ensurePolished();
QString unit = QString::fromStdString(d->unit.getString());
int decimals = App::GetApplication()
.GetUserParameter()
.GetGroup("BaseApp/Preferences/Units")
->GetInt("Decimals", 2);
return digits + 1 /*separator*/ + decimals + 1 /*space*/ + unit.length();
}
void QuantitySpinBox::showEvent(QShowEvent* event)
{
Q_D(QuantitySpinBox);
+28 -2
View File
@@ -54,6 +54,15 @@ class GuiExport QuantitySpinBox: public QAbstractSpinBox, public ExpressionSpinB
Q_PROPERTY(
bool autoNormalize READ autoNormalize WRITE setAutoNormalize
) // clazy:exclude=qproperty-without-notify
Q_PROPERTY(
bool autoAdjustWidth READ autoAdjustWidth WRITE setAutoAdjustWidth
) // clazy:exclude=qproperty-without-notify
Q_PROPERTY(
bool addIconSpace READ isIconSpaceAdded WRITE addIconSpace
) // clazy:exclude=qproperty-without-notify
Q_PROPERTY(
int maxExpectedDigits READ getMaxExpectedDigits WRITE setMaxExpectedDigits
) // clazy:exclude=qproperty-without-notify
public:
explicit QuantitySpinBox(QWidget* parent = nullptr);
@@ -99,6 +108,11 @@ public:
/// Sets the value of the maximum property
void setMaximum(double max);
/// Adjust how many digits in the integer part we should expect
/// Affects minimum size if width is not adjustable
void setMaxExpectedDigits(int digits);
int getMaxExpectedDigits();
/// Gets the number of decimals
int decimals() const;
/// Sets the number of decimals
@@ -109,6 +123,16 @@ public:
/// Enables or disables automatic normalization on enter
void setAutoNormalize(bool normalize);
/// Returns if automatic width adjustment is enabled for this input
bool autoAdjustWidth() const;
/// Enables or disables automatic width adjustement
void setAutoAdjustWidth(bool adjust);
/// Returns if icon space is added for this input
bool isIconSpaceAdded() const;
/// Enables or disables icon space addition
void addIconSpace(bool addIconSpace);
/// Sets a specific unit schema to handle quantities.
/// The system-wide schema won't be used any more.
void setSchema(int s);
@@ -142,7 +166,6 @@ public:
/// This is a helper function to determine the size this widget requires to fully display the text
QSize sizeForText(const QString&) const;
QSize sizeHint() const override;
QSize minimumSizeHint() const override;
bool event(QEvent* event) override;
void setNumberExpression(App::NumberExpression*) override;
@@ -175,13 +198,16 @@ protected:
void paintEvent(QPaintEvent* event) override;
private:
void moveCursor();
void validateInput() override;
void updateText(const Base::Quantity&);
void updateEdit(const QString& text);
void updateFromCache(bool notify, bool updateUnit = true);
QString getUserString(const Base::Quantity& val, double& factor, QString& unitString) const;
QString getUserString(const Base::Quantity& val) const;
QSize sizeHintCalculator(int height) const;
QSize sizeHintForDigits(int digits) const;
int getMaxStrLength(int digits) const;
Q_SIGNALS:
/** Gets emitted if the user has entered a VALID input
+1 -1
View File
@@ -45,6 +45,7 @@ public:
void bind(const App::ObjectIdentifier& _path) override;
void setExpression(std::shared_ptr<App::Expression> expr) override;
int getMargin();
protected:
/*! Expression handling */
@@ -65,7 +66,6 @@ protected:
virtual void showIcon();
virtual void validateInput();
void resizeWidget();
int getMargin();
bool handleKeyEvent(const QString&);
virtual void openFormulaDialog();