1083 lines
30 KiB
C++
1083 lines
30 KiB
C++
/***************************************************************************
|
||
* Copyright (c) 2014 Werner Mayer <wmayer[at]users.sourceforge.net> *
|
||
* *
|
||
* This file is part of the FreeCAD CAx development system. *
|
||
* *
|
||
* This library is free software; you can redistribute it and/or *
|
||
* modify it under the terms of the GNU Library General Public *
|
||
* License as published by the Free Software Foundation; either *
|
||
* version 2 of the License, or (at your option) any later version. *
|
||
* *
|
||
* This library is distributed in the hope that it will be useful, *
|
||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||
* GNU Library General Public License for more details. *
|
||
* *
|
||
* You should have received a copy of the GNU Library General Public *
|
||
* License along with this library; see the file COPYING.LIB. If not, *
|
||
* write to the Free Software Foundation, Inc., 51 Franklin Street, *
|
||
* Fifth Floor, Boston, MA 02110-1301, USA *
|
||
* *
|
||
***************************************************************************/
|
||
|
||
#include <limits>
|
||
#include <QApplication>
|
||
#include <QDebug>
|
||
#include <QFocusEvent>
|
||
#include <QFontMetrics>
|
||
#include <QLineEdit>
|
||
#include <QRegularExpression>
|
||
#include <QRegularExpressionMatch>
|
||
#include <QStyle>
|
||
#include <QStyleOptionSpinBox>
|
||
#include <QToolTip>
|
||
|
||
#include <sstream>
|
||
|
||
#include <App/Application.h>
|
||
#include <App/Document.h>
|
||
#include <App/DocumentObject.h>
|
||
#include <App/ExpressionParser.h>
|
||
#include <Base/Exception.h>
|
||
#include <Base/UnitsApi.h>
|
||
#include <Base/Tools.h>
|
||
#include <Base/UnitsSchema.h>
|
||
|
||
#include "QuantitySpinBox.h"
|
||
#include "QuantitySpinBox_p.h"
|
||
#include "Command.h"
|
||
#include "Dialogs/DlgExpressionInput.h"
|
||
#include "Tools.h"
|
||
#include "Widgets.h"
|
||
|
||
|
||
using namespace Gui;
|
||
using namespace App;
|
||
using namespace Base;
|
||
|
||
namespace Gui
|
||
{
|
||
|
||
class QuantitySpinBoxPrivate
|
||
{
|
||
public:
|
||
QuantitySpinBoxPrivate(QuantitySpinBox* q)
|
||
: validInput(true)
|
||
, pendingEmit(false)
|
||
, normalize(true)
|
||
, checkRangeInExpression(false)
|
||
, unitValue(0)
|
||
, maximum(std::numeric_limits<double>::max())
|
||
, minimum(-std::numeric_limits<double>::max())
|
||
, singleStep(1.0)
|
||
, q_ptr(q)
|
||
{}
|
||
~QuantitySpinBoxPrivate() = default;
|
||
|
||
QString stripped(const QString& t, int* pos) const
|
||
{
|
||
QString text = t;
|
||
const int s = text.size();
|
||
text = text.trimmed();
|
||
if (pos) {
|
||
(*pos) -= (s - text.size());
|
||
}
|
||
return text;
|
||
}
|
||
|
||
bool validate(QString& input, Base::Quantity& result, const App::ObjectIdentifier& path) const
|
||
{
|
||
Q_Q(const QuantitySpinBox);
|
||
|
||
// Do not accept empty strings because the parser will consider
|
||
// " unit" as "1 unit" which is not the desired behaviour (see #0004104)
|
||
if (input.isEmpty()) {
|
||
return false;
|
||
}
|
||
|
||
bool success = false;
|
||
QString tmp = input;
|
||
|
||
auto validateInput = [&](QString& tmp) -> QValidator::State {
|
||
QValidator::State state;
|
||
Base::Quantity res = validateAndInterpret(tmp, state, path);
|
||
res.setFormat(quantity.getFormat());
|
||
if (state == QValidator::Acceptable) {
|
||
success = true;
|
||
result = res;
|
||
input = tmp;
|
||
}
|
||
return state;
|
||
};
|
||
|
||
QValidator::State state = validateInput(tmp);
|
||
if (state == QValidator::Intermediate && q->hasExpression()) {
|
||
// Accept the expression as it is but try to add the right unit string
|
||
success = true;
|
||
|
||
Base::Quantity quantity;
|
||
double value;
|
||
if (parseString(input, quantity, value, path)) {
|
||
quantity.setUnit(unit);
|
||
result = quantity;
|
||
|
||
// Now translate the quantity into its string representation using the user-defined
|
||
// unit system
|
||
input = QString::fromStdString(Base::UnitsApi::schemaTranslate(result));
|
||
}
|
||
}
|
||
|
||
return success;
|
||
}
|
||
bool parseString(
|
||
const QString& str,
|
||
Base::Quantity& result,
|
||
double& value,
|
||
const App::ObjectIdentifier& path
|
||
) const
|
||
{
|
||
App::ObjectIdentifier pathtmp = path;
|
||
try {
|
||
QString copy = str;
|
||
copy.remove(locale.groupSeparator());
|
||
|
||
// Expression parser
|
||
std::shared_ptr<Expression> expr(
|
||
ExpressionParser::parse(path.getDocumentObject(), copy.toUtf8().constData())
|
||
);
|
||
if (expr) {
|
||
|
||
std::unique_ptr<Expression> res(expr->eval());
|
||
NumberExpression* n = freecad_cast<NumberExpression*>(res.get());
|
||
if (n) {
|
||
result = n->getQuantity();
|
||
value = result.getValue();
|
||
return true;
|
||
}
|
||
}
|
||
}
|
||
catch (Base::Exception&) {
|
||
return false;
|
||
}
|
||
return false;
|
||
}
|
||
Base::Quantity validateAndInterpret(
|
||
QString& input,
|
||
QValidator::State& state,
|
||
const App::ObjectIdentifier& path
|
||
) const
|
||
{
|
||
Base::Quantity res;
|
||
const double max = this->maximum;
|
||
const double min = this->minimum;
|
||
|
||
QString copy = input;
|
||
double value = min;
|
||
bool ok = false;
|
||
|
||
QChar plus = QLatin1Char('+'), minus = QLatin1Char('-');
|
||
|
||
if (locale.negativeSign() != minus) {
|
||
copy.replace(locale.negativeSign(), minus);
|
||
}
|
||
if (locale.positiveSign() != plus) {
|
||
copy.replace(locale.positiveSign(), plus);
|
||
}
|
||
|
||
QString reverseUnitStr = unitStr;
|
||
std::reverse(reverseUnitStr.begin(), reverseUnitStr.end());
|
||
|
||
// Prep for expression parser
|
||
// This regex matches chunks between +,-,$,^ accounting for matching parenthesis.
|
||
QRegularExpression chunkRe(
|
||
QStringLiteral("(?<=^|[\\+\\-])((\\((?>[^()]|(?2))*\\))|[^\\+\\-\n])*(?=$|[\\+\\-])")
|
||
);
|
||
QRegularExpressionMatchIterator expressionChunk = chunkRe.globalMatch(copy);
|
||
unsigned int lengthOffset = 0;
|
||
while (expressionChunk.hasNext()) {
|
||
QRegularExpressionMatch matchChunk = expressionChunk.next();
|
||
QString origionalChunk = matchChunk.captured(0);
|
||
QString copyChunk = origionalChunk;
|
||
std::reverse(copyChunk.begin(), copyChunk.end());
|
||
|
||
// Reused regex patterns
|
||
static const std::string regexUnits
|
||
= "sAV|VC|lim|nim|im|hpm|[mf]?bl|°|ged|dar|nog|″|′|rroT[uµm]?|K[uµm]?|A[mkM]?|F["
|
||
"pnuµm]?|C|S[uµmkM]?|zH[kMGT]?|H[nuµm]?|mhO[kM]?|J[mk]?|Ve[kM]?|V[mk]?|hWk|sW|"
|
||
"lack?|N[mkM]?|g[uµmk]?|lm?|(?<=\\b|[^a-zA-Z])m[nuµmcdk]?|uoht|ni|\"|'|dy|dc|bW|"
|
||
"T|t|zo|ts|twc|Wk?|aP[kMG]?|is[pk]|h|G|M|tfc|tfqs|tf|s";
|
||
static const std::string regexUnitlessFunctions
|
||
= "soca|nisa|2nata|nata|hsoc|hnis|hnat|soc|nat|nis|pxe|gol|01gol";
|
||
static const std::string regexConstants = "e|ip|lomm|lom";
|
||
static const std::string regexNumber = "\\d+\\s*\\.?\\s*\\d*|\\.\\s*\\d+";
|
||
|
||
// If expression does not contain /*() or ^, this regex will not find anything
|
||
if (copy.contains(QLatin1Char('/')) || copy.contains(QLatin1Char('*'))
|
||
|| copy.contains(QLatin1Char('(')) || copy.contains(QLatin1Char(')'))
|
||
|| copy.contains(QLatin1Char('^'))) {
|
||
// Find units and replace 1/2mm -> 1/2*(1mm), 1^2mm -> 1^2*(1mm)
|
||
QRegularExpression fixUnits(
|
||
QString::fromStdString(
|
||
"(" + regexUnits + ")(\\s*\\)|(?:\\*|(?:\\)(?:(?:\\s*(?:" + regexConstants
|
||
+ "|\\)(?:[^()]|(?R))*\\((?:" + regexUnitlessFunctions + ")|" + regexNumber
|
||
+ "))|(?R))*\\(|(?:\\s*(?:" + regexConstants
|
||
+ "|\\)(?:[^()]|(?R))*\\((?:" + regexUnitlessFunctions + ")|" + regexNumber
|
||
+ "))))+(?:[\\/\\^]|(.*$))(?!(" + regexUnits + ")))"
|
||
)
|
||
);
|
||
QRegularExpressionMatch fixUnitsMatch = fixUnits.match(copyChunk);
|
||
|
||
// 3rd capture group being filled indicates regex bailed out; no match.
|
||
if (fixUnitsMatch.lastCapturedIndex() == 2
|
||
|| (fixUnitsMatch.lastCapturedIndex() == 3
|
||
&& fixUnitsMatch.captured(3).isEmpty())) {
|
||
QString matchUnits = fixUnitsMatch.captured(1);
|
||
QString matchNumbers = fixUnitsMatch.captured(2);
|
||
copyChunk.replace(
|
||
matchUnits + matchNumbers,
|
||
QStringLiteral(")") + matchUnits + QStringLiteral("1(*") + matchNumbers
|
||
);
|
||
}
|
||
}
|
||
|
||
// Add default units to string if none are present
|
||
if (!copyChunk.contains(reverseUnitStr)) { // Fast check
|
||
QRegularExpression unitsRe(
|
||
QString::fromStdString(
|
||
"(?<=\\b|[^a-zA-Z])(" + regexUnits
|
||
+ ")(?=\\b|[^a-zA-Z])|°|″|′|\"|'|\\p{L}\\.\\p{L}|\\[\\p{L}"
|
||
)
|
||
);
|
||
|
||
QRegularExpressionMatch match = unitsRe.match(copyChunk);
|
||
if (!match.hasMatch() && !copyChunk.isEmpty()) { // If no units are found, use
|
||
// default units
|
||
copyChunk.prepend(
|
||
QStringLiteral(")") + reverseUnitStr + QStringLiteral("1(*")
|
||
); // Add units to the end of chunk *(1unit)
|
||
}
|
||
}
|
||
|
||
std::reverse(copyChunk.begin(), copyChunk.end());
|
||
|
||
copy.replace(
|
||
matchChunk.capturedStart() + lengthOffset,
|
||
matchChunk.capturedEnd() - matchChunk.capturedStart(),
|
||
copyChunk
|
||
);
|
||
lengthOffset += copyChunk.length() - origionalChunk.length();
|
||
}
|
||
|
||
ok = parseString(copy, res, value, path);
|
||
|
||
// If result does not have unit: add default unit
|
||
if (res.isDimensionless()) {
|
||
res.setUnit(unit);
|
||
}
|
||
|
||
if (!ok) {
|
||
// input may not be finished
|
||
state = QValidator::Intermediate;
|
||
}
|
||
else if (value >= min && value <= max) {
|
||
state = QValidator::Acceptable;
|
||
}
|
||
else if (max == min) { // when max and min is the same the only non-Invalid input is max
|
||
// (or min)
|
||
state = QValidator::Invalid;
|
||
}
|
||
else {
|
||
if ((value >= 0 && value > max) || (value < 0 && value < min)) {
|
||
state = QValidator::Invalid;
|
||
}
|
||
else {
|
||
state = QValidator::Intermediate;
|
||
}
|
||
}
|
||
if (state != QValidator::Acceptable) {
|
||
res.setValue(max > 0 ? min : max);
|
||
}
|
||
|
||
return res;
|
||
}
|
||
|
||
QLocale locale;
|
||
bool validInput;
|
||
bool pendingEmit;
|
||
bool normalize;
|
||
bool checkRangeInExpression;
|
||
QString validStr;
|
||
Base::Quantity quantity;
|
||
Base::Quantity cached;
|
||
Base::Unit unit;
|
||
double unitValue;
|
||
QString unitStr;
|
||
double maximum;
|
||
double minimum;
|
||
double singleStep;
|
||
QuantitySpinBox* q_ptr;
|
||
std::unique_ptr<Base::UnitsSchema> scheme;
|
||
Q_DECLARE_PUBLIC(QuantitySpinBox)
|
||
};
|
||
} // namespace Gui
|
||
|
||
QuantitySpinBox::QuantitySpinBox(QWidget* parent)
|
||
: QAbstractSpinBox(parent)
|
||
, ExpressionSpinBox(this)
|
||
, d_ptr(new QuantitySpinBoxPrivate(this))
|
||
{
|
||
d_ptr->locale = locale();
|
||
this->setContextMenuPolicy(Qt::DefaultContextMenu);
|
||
connect(lineEdit(), &QLineEdit::textChanged, this, &QuantitySpinBox::userInput);
|
||
connect(this, &QuantitySpinBox::editingFinished, this, [&] { this->handlePendingEmit(true); });
|
||
}
|
||
|
||
QuantitySpinBox::~QuantitySpinBox() = default;
|
||
|
||
void QuantitySpinBox::bind(const App::ObjectIdentifier& _path)
|
||
{
|
||
ExpressionSpinBox::bind(_path);
|
||
}
|
||
|
||
void QuantitySpinBox::showIcon()
|
||
{
|
||
iconLabel->show();
|
||
}
|
||
|
||
QString QuantitySpinBox::boundToName() const
|
||
{
|
||
if (isBound()) {
|
||
std::string path = getPath().toString();
|
||
return QString::fromStdString(path);
|
||
}
|
||
return {};
|
||
}
|
||
|
||
/**
|
||
* @brief Create an object identifier by name.
|
||
*
|
||
* An identifier is written as document#documentobject.property.subproperty1...subpropertyN
|
||
* document# may be dropped, in this case the active document is used.
|
||
*/
|
||
void QuantitySpinBox::setBoundToByName(const QString& name)
|
||
{
|
||
try {
|
||
// get document
|
||
App::Document* doc = App::GetApplication().getActiveDocument();
|
||
QStringList list = name.split(QLatin1Char('#'));
|
||
if (list.size() > 1) {
|
||
doc = App::GetApplication().getDocument(list.front().toLatin1());
|
||
list.pop_front();
|
||
}
|
||
|
||
if (!doc) {
|
||
qDebug() << "No such document";
|
||
return;
|
||
}
|
||
|
||
// first element is assumed to be the document name
|
||
list = list.front().split(QLatin1Char('.'));
|
||
|
||
// get object
|
||
App::DocumentObject* obj = doc->getObject(list.front().toLatin1());
|
||
if (!obj) {
|
||
qDebug() << "No object " << list.front() << " in document";
|
||
return;
|
||
}
|
||
list.pop_front();
|
||
|
||
// the rest of the list defines the property and eventually subproperties
|
||
App::ObjectIdentifier path(obj);
|
||
path.setDocumentName(std::string(doc->getName()), true);
|
||
path.setDocumentObjectName(std::string(obj->getNameInDocument()), true);
|
||
|
||
for (const auto& it : list) {
|
||
path << App::ObjectIdentifier::Component::SimpleComponent(it.toLatin1().constData());
|
||
}
|
||
|
||
if (path.getProperty()) {
|
||
bind(path);
|
||
}
|
||
}
|
||
catch (const Base::Exception& e) {
|
||
qDebug() << e.what();
|
||
}
|
||
}
|
||
|
||
QString Gui::QuantitySpinBox::expressionText() const
|
||
{
|
||
try {
|
||
if (hasExpression()) {
|
||
return QString::fromStdString(getExpressionString());
|
||
}
|
||
}
|
||
catch (const Base::Exception& e) {
|
||
qDebug() << e.what();
|
||
}
|
||
return {};
|
||
}
|
||
|
||
void QuantitySpinBox::evaluateExpression()
|
||
{
|
||
if (isBound() && getExpression()) {
|
||
showValidExpression(Number::SetIfNumber);
|
||
}
|
||
}
|
||
|
||
void Gui::QuantitySpinBox::setNumberExpression(App::NumberExpression* expr)
|
||
{
|
||
updateEdit(getUserString(expr->getQuantity()));
|
||
handlePendingEmit();
|
||
}
|
||
|
||
bool QuantitySpinBox::apply(const std::string& propName)
|
||
{
|
||
if (!ExpressionBinding::apply(propName)) {
|
||
double dValue = value().getValue();
|
||
return assignToProperty(propName, dValue);
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
void QuantitySpinBox::resizeEvent(QResizeEvent* event)
|
||
{
|
||
QAbstractSpinBox::resizeEvent(event);
|
||
resizeWidget();
|
||
}
|
||
|
||
void Gui::QuantitySpinBox::keyPressEvent(QKeyEvent* event)
|
||
{
|
||
Q_D(QuantitySpinBox);
|
||
|
||
const auto isEnter = event->key() == Qt::Key_Enter || event->key() == Qt::Key_Return;
|
||
|
||
if (d->normalize && isEnter && !isNormalized()) {
|
||
// ensure that input is up to date
|
||
handlePendingEmit();
|
||
|
||
normalize();
|
||
return;
|
||
}
|
||
|
||
if (!handleKeyEvent(event->text())) {
|
||
QAbstractSpinBox::keyPressEvent(event);
|
||
}
|
||
|
||
if (isEnter) {
|
||
returnPressed();
|
||
}
|
||
}
|
||
|
||
void Gui::QuantitySpinBox::paintEvent(QPaintEvent*)
|
||
{
|
||
QStyleOptionSpinBox opt;
|
||
initStyleOption(&opt);
|
||
drawControl(opt);
|
||
}
|
||
|
||
void QuantitySpinBox::updateText(const Quantity& quant)
|
||
{
|
||
Q_D(QuantitySpinBox);
|
||
|
||
double dFactor;
|
||
QString txt = getUserString(quant, dFactor, d->unitStr);
|
||
d->unitValue = quant.getValue() / dFactor;
|
||
updateEdit(txt);
|
||
handlePendingEmit();
|
||
}
|
||
|
||
void QuantitySpinBox::updateEdit(const QString& text)
|
||
{
|
||
Q_D(QuantitySpinBox);
|
||
|
||
QLineEdit* edit = lineEdit();
|
||
|
||
bool empty = edit->text().isEmpty();
|
||
int cursor = edit->cursorPosition();
|
||
int selsize = edit->selectedText().size();
|
||
|
||
edit->setText(text);
|
||
|
||
cursor = qBound(0, cursor, qMax(0, edit->displayText().size() - d->unitStr.size()));
|
||
if (selsize > 0) {
|
||
edit->setSelection(0, cursor);
|
||
}
|
||
else {
|
||
edit->setCursorPosition(empty ? 0 : cursor);
|
||
}
|
||
}
|
||
|
||
void QuantitySpinBox::validateInput()
|
||
{
|
||
Q_D(QuantitySpinBox);
|
||
|
||
QValidator::State state;
|
||
QString text = lineEdit()->text();
|
||
const App::ObjectIdentifier& path = getPath();
|
||
d->validateAndInterpret(text, state, path);
|
||
if (state != QValidator::Acceptable) {
|
||
updateEdit(d->validStr);
|
||
}
|
||
|
||
handlePendingEmit();
|
||
}
|
||
|
||
Base::Quantity QuantitySpinBox::value() const
|
||
{
|
||
Q_D(const QuantitySpinBox);
|
||
return d->quantity;
|
||
}
|
||
|
||
double QuantitySpinBox::rawValue() const
|
||
{
|
||
Q_D(const QuantitySpinBox);
|
||
return d->quantity.getValue();
|
||
}
|
||
|
||
void QuantitySpinBox::normalize()
|
||
{
|
||
// this does not really change the value, only the representation
|
||
QSignalBlocker blocker(this);
|
||
|
||
Q_D(const QuantitySpinBox);
|
||
return setValue(d->quantity);
|
||
}
|
||
|
||
bool QuantitySpinBox::isNormalized()
|
||
{
|
||
static const QRegularExpression operators(
|
||
QStringLiteral("[+\\-/*]"),
|
||
QRegularExpression::CaseInsensitiveOption
|
||
);
|
||
|
||
Q_D(const QuantitySpinBox);
|
||
|
||
// this check is two level
|
||
// 1. We consider every string that does not contain operators as normalized
|
||
// 2. If it does contain operators we check if it differs from normalized input - as some
|
||
// operators like - can be allowed even in normalized case.
|
||
return !d->validStr.contains(operators)
|
||
|| d->validStr.toStdString() == d->quantity.getUserString();
|
||
}
|
||
|
||
void QuantitySpinBox::setValue(const Base::Quantity& value)
|
||
{
|
||
Q_D(QuantitySpinBox);
|
||
d->quantity = value;
|
||
// check limits
|
||
if (d->quantity.getValue() > d->maximum) {
|
||
d->quantity.setValue(d->maximum);
|
||
}
|
||
if (d->quantity.getValue() < d->minimum) {
|
||
d->quantity.setValue(d->minimum);
|
||
}
|
||
|
||
d->unit = value.getUnit();
|
||
|
||
updateText(value);
|
||
}
|
||
|
||
void QuantitySpinBox::setValue(double value)
|
||
{
|
||
Q_D(QuantitySpinBox);
|
||
|
||
Base::QuantityFormat currentformat = d->quantity.getFormat();
|
||
auto quantity = Base::Quantity(value, d->unit);
|
||
quantity.setFormat(currentformat);
|
||
|
||
setValue(quantity);
|
||
}
|
||
|
||
bool QuantitySpinBox::autoNormalize() const
|
||
{
|
||
Q_D(const QuantitySpinBox);
|
||
return d->normalize;
|
||
}
|
||
|
||
void QuantitySpinBox::setAutoNormalize(bool normalize)
|
||
{
|
||
Q_D(QuantitySpinBox);
|
||
d->normalize = normalize;
|
||
}
|
||
|
||
bool QuantitySpinBox::hasValidInput() const
|
||
{
|
||
Q_D(const QuantitySpinBox);
|
||
return d->validInput;
|
||
}
|
||
|
||
// Gets called after call of 'validateAndInterpret'
|
||
void QuantitySpinBox::userInput(const QString& text)
|
||
{
|
||
Q_D(QuantitySpinBox);
|
||
|
||
d->pendingEmit = true;
|
||
|
||
QString tmp = text;
|
||
Base::Quantity res;
|
||
const App::ObjectIdentifier& path = getPath();
|
||
if (d->validate(tmp, res, path)) {
|
||
d->validStr = tmp;
|
||
d->validInput = true;
|
||
}
|
||
else {
|
||
d->validInput = false;
|
||
|
||
// only emit signal to reset EditableDatumLabel if the input is truly empty or has
|
||
// no meaningful number don't emit for partially typed numbers like "71." which are
|
||
// temporarily invalid
|
||
const QString trimmedText = text.trimmed();
|
||
static const QRegularExpression partialNumberRegex(QStringLiteral(R"([+-]?(\d+)?(\.,\d*)?)"));
|
||
if (trimmedText.isEmpty() || !trimmedText.contains(partialNumberRegex)) {
|
||
// we have to emit here signal explicitly as validator will not pass
|
||
// this value further but we want to check it to disable isSet flag if
|
||
// it has been set previously
|
||
Q_EMIT valueChanged(d->quantity.getValue());
|
||
}
|
||
return;
|
||
}
|
||
|
||
if (keyboardTracking()) {
|
||
d->cached = res;
|
||
handlePendingEmit(false);
|
||
}
|
||
else {
|
||
d->cached = res;
|
||
}
|
||
}
|
||
|
||
void QuantitySpinBox::openFormulaDialog()
|
||
{
|
||
Q_ASSERT(isBound());
|
||
|
||
Q_D(const QuantitySpinBox);
|
||
auto box = new Gui::Dialog::DlgExpressionInput(getPath(), getExpression(), d->unit, this);
|
||
if (d->checkRangeInExpression) {
|
||
box->setRange(d->minimum, d->maximum);
|
||
}
|
||
QObject::connect(box, &Gui::Dialog::DlgExpressionInput::finished, [this, box]() {
|
||
if (box->result() == QDialog::Accepted) {
|
||
setExpression(box->getExpression());
|
||
}
|
||
else if (box->discardedFormula()) {
|
||
setExpression(std::shared_ptr<Expression>());
|
||
}
|
||
|
||
updateExpression();
|
||
box->deleteLater();
|
||
Q_EMIT showFormulaDialog(false);
|
||
});
|
||
box->show();
|
||
|
||
QPoint pos = mapToGlobal(QPoint(0, 0));
|
||
box->move(pos - box->expressionPosition());
|
||
Gui::adjustDialogPosition(box);
|
||
|
||
Q_EMIT showFormulaDialog(true);
|
||
}
|
||
|
||
void QuantitySpinBox::handlePendingEmit(bool updateUnit /* = true */)
|
||
{
|
||
updateFromCache(true, updateUnit);
|
||
}
|
||
|
||
void QuantitySpinBox::updateFromCache(bool notify, bool updateUnit /* = true */)
|
||
{
|
||
Q_D(QuantitySpinBox);
|
||
if (d->pendingEmit) {
|
||
double factor;
|
||
const Base::Quantity& res = d->cached;
|
||
auto tmpUnit(d->unitStr);
|
||
QString text = getUserString(res, factor, updateUnit ? d->unitStr : tmpUnit);
|
||
d->unitValue = res.getValue() / factor;
|
||
d->quantity = res;
|
||
|
||
// signaling
|
||
if (notify) {
|
||
d->pendingEmit = false;
|
||
Q_EMIT valueChanged(res);
|
||
Q_EMIT valueChanged(res.getValue());
|
||
Q_EMIT textChanged(text);
|
||
}
|
||
}
|
||
}
|
||
|
||
Base::Unit QuantitySpinBox::unit() const
|
||
{
|
||
Q_D(const QuantitySpinBox);
|
||
return d->unit;
|
||
}
|
||
|
||
void QuantitySpinBox::setUnit(const Base::Unit& unit)
|
||
{
|
||
Q_D(QuantitySpinBox);
|
||
|
||
d->unit = unit;
|
||
d->quantity.setUnit(unit);
|
||
updateText(d->quantity);
|
||
}
|
||
|
||
void QuantitySpinBox::setUnitText(const QString& str)
|
||
{
|
||
try {
|
||
Base::Quantity quant = Base::Quantity::parse(str.toStdString());
|
||
setUnit(quant.getUnit());
|
||
}
|
||
catch (const Base::ParserError&) {
|
||
}
|
||
}
|
||
|
||
QString QuantitySpinBox::unitText()
|
||
{
|
||
Q_D(QuantitySpinBox);
|
||
return d->unitStr;
|
||
}
|
||
|
||
double QuantitySpinBox::singleStep() const
|
||
{
|
||
Q_D(const QuantitySpinBox);
|
||
return d->singleStep;
|
||
}
|
||
|
||
void QuantitySpinBox::setSingleStep(double value)
|
||
{
|
||
Q_D(QuantitySpinBox);
|
||
|
||
if (value >= 0) {
|
||
d->singleStep = value;
|
||
}
|
||
}
|
||
|
||
double QuantitySpinBox::minimum() const
|
||
{
|
||
Q_D(const QuantitySpinBox);
|
||
return d->minimum;
|
||
}
|
||
|
||
void QuantitySpinBox::setMinimum(double minimum)
|
||
{
|
||
Q_D(QuantitySpinBox);
|
||
d->minimum = minimum;
|
||
}
|
||
|
||
double QuantitySpinBox::maximum() const
|
||
{
|
||
Q_D(const QuantitySpinBox);
|
||
return d->maximum;
|
||
}
|
||
|
||
void QuantitySpinBox::setMaximum(double maximum)
|
||
{
|
||
Q_D(QuantitySpinBox);
|
||
d->maximum = maximum;
|
||
}
|
||
|
||
void QuantitySpinBox::setRange(double minimum, double maximum)
|
||
{
|
||
Q_D(QuantitySpinBox);
|
||
d->minimum = minimum;
|
||
d->maximum = maximum;
|
||
}
|
||
|
||
void QuantitySpinBox::checkRangeInExpression(bool on)
|
||
{
|
||
Q_D(QuantitySpinBox);
|
||
d->checkRangeInExpression = on;
|
||
}
|
||
|
||
bool QuantitySpinBox::isCheckedRangeInExpresion() const
|
||
{
|
||
Q_D(const QuantitySpinBox);
|
||
return d->checkRangeInExpression;
|
||
}
|
||
|
||
|
||
int QuantitySpinBox::decimals() const
|
||
{
|
||
Q_D(const QuantitySpinBox);
|
||
return d->quantity.getFormat().getPrecision();
|
||
}
|
||
|
||
void QuantitySpinBox::setDecimals(int v)
|
||
{
|
||
Q_D(QuantitySpinBox);
|
||
Base::QuantityFormat f = d->quantity.getFormat();
|
||
f.setPrecision(v);
|
||
d->quantity.setFormat(f);
|
||
updateText(d->quantity);
|
||
}
|
||
|
||
void QuantitySpinBox::setSchema(const int s)
|
||
{
|
||
Q_D(QuantitySpinBox);
|
||
d->scheme = Base::UnitsApi::createSchema(s);
|
||
updateText(d->quantity);
|
||
}
|
||
|
||
void QuantitySpinBox::clearSchema()
|
||
{
|
||
Q_D(QuantitySpinBox);
|
||
d->scheme = nullptr;
|
||
updateText(d->quantity);
|
||
}
|
||
|
||
QString QuantitySpinBox::getUserString(const Base::Quantity& val, double& factor, QString& unitString) const
|
||
{
|
||
Q_D(const QuantitySpinBox);
|
||
std::string unitStr;
|
||
const std::string str = d->scheme ? val.getUserString(d->scheme.get(), factor, unitStr)
|
||
: val.getUserString(factor, unitStr);
|
||
unitString = QString::fromStdString(unitStr);
|
||
return QString::fromStdString(str);
|
||
}
|
||
|
||
QString QuantitySpinBox::getUserString(const Base::Quantity& val) const
|
||
{
|
||
Q_D(const QuantitySpinBox);
|
||
std::string str;
|
||
if (d->scheme) {
|
||
double factor;
|
||
std::string unitString;
|
||
str = val.getUserString(d->scheme.get(), factor, unitString);
|
||
}
|
||
else {
|
||
str = val.getUserString();
|
||
}
|
||
return QString::fromStdString(str);
|
||
}
|
||
|
||
void QuantitySpinBox::setExpression(std::shared_ptr<Expression> expr)
|
||
{
|
||
ExpressionSpinBox::setExpression(expr);
|
||
}
|
||
|
||
QAbstractSpinBox::StepEnabled QuantitySpinBox::stepEnabled() const
|
||
{
|
||
Q_D(const QuantitySpinBox);
|
||
if (isReadOnly() /* || !d->validInput*/) {
|
||
return StepNone;
|
||
}
|
||
if (wrapping()) {
|
||
return StepEnabled(StepUpEnabled | StepDownEnabled);
|
||
}
|
||
StepEnabled ret = StepNone;
|
||
if (d->quantity.getValue() < d->maximum) {
|
||
ret |= StepUpEnabled;
|
||
}
|
||
if (d->quantity.getValue() > d->minimum) {
|
||
ret |= StepDownEnabled;
|
||
}
|
||
return ret;
|
||
}
|
||
|
||
void QuantitySpinBox::stepBy(int steps)
|
||
{
|
||
Q_D(QuantitySpinBox);
|
||
updateFromCache(false);
|
||
|
||
double step = d->singleStep * steps;
|
||
double val = d->unitValue + step;
|
||
if (val > d->maximum) {
|
||
val = d->maximum;
|
||
}
|
||
else if (val < d->minimum) {
|
||
val = d->minimum;
|
||
}
|
||
|
||
Quantity quant(val, d->unitStr.toStdString());
|
||
updateText(quant);
|
||
updateFromCache(true);
|
||
update();
|
||
selectNumber();
|
||
}
|
||
|
||
QSize QuantitySpinBox::sizeForText(const QString& txt) const
|
||
{
|
||
const QFontMetrics fm(fontMetrics());
|
||
int h = lineEdit()->sizeHint().height();
|
||
int w = QtTools::horizontalAdvance(fm, txt);
|
||
|
||
w += 2; // cursor blinking space
|
||
w += iconHeight;
|
||
|
||
QStyleOptionSpinBox opt;
|
||
initStyleOption(&opt);
|
||
QSize hint(w, h);
|
||
QSize size = style()->sizeFromContents(QStyle::CT_SpinBox, &opt, hint, this);
|
||
return size;
|
||
}
|
||
|
||
QSize QuantitySpinBox::sizeHint() const
|
||
{
|
||
return sizeHintCalculator(lineEdit()->sizeHint().height());
|
||
}
|
||
|
||
QSize QuantitySpinBox::minimumSizeHint() const
|
||
{
|
||
return sizeHintCalculator(lineEdit()->minimumSizeHint().height());
|
||
}
|
||
|
||
QSize QuantitySpinBox::sizeHintCalculator(int h) const
|
||
{
|
||
Q_D(const QuantitySpinBox);
|
||
ensurePolished();
|
||
|
||
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;
|
||
|
||
QStyleOptionSpinBox opt;
|
||
initStyleOption(&opt);
|
||
QSize hint(w, h);
|
||
|
||
QSize size = style()->sizeFromContents(QStyle::CT_SpinBox, &opt, hint, this);
|
||
return size;
|
||
}
|
||
|
||
void QuantitySpinBox::showEvent(QShowEvent* event)
|
||
{
|
||
Q_D(QuantitySpinBox);
|
||
|
||
QAbstractSpinBox::showEvent(event);
|
||
|
||
bool selected = lineEdit()->hasSelectedText();
|
||
updateText(d->quantity);
|
||
if (selected) {
|
||
selectNumber();
|
||
}
|
||
}
|
||
|
||
void QuantitySpinBox::hideEvent(QHideEvent* event)
|
||
{
|
||
handlePendingEmit();
|
||
QAbstractSpinBox::hideEvent(event);
|
||
}
|
||
|
||
void QuantitySpinBox::closeEvent(QCloseEvent* event)
|
||
{
|
||
handlePendingEmit();
|
||
QAbstractSpinBox::closeEvent(event);
|
||
}
|
||
|
||
bool QuantitySpinBox::event(QEvent* event)
|
||
{
|
||
return QAbstractSpinBox::event(event);
|
||
}
|
||
|
||
void QuantitySpinBox::focusInEvent(QFocusEvent* event)
|
||
{
|
||
bool hasSel = lineEdit()->hasSelectedText();
|
||
QAbstractSpinBox::focusInEvent(event);
|
||
|
||
if (event->reason() == Qt::TabFocusReason || event->reason() == Qt::BacktabFocusReason
|
||
|| event->reason() == Qt::ShortcutFocusReason) {
|
||
|
||
if (isBound() && getExpression() && lineEdit()->isReadOnly()) {
|
||
auto helpEvent = new QHelpEvent(
|
||
QEvent::ToolTip,
|
||
QPoint(0, rect().height()),
|
||
mapToGlobal(QPoint(0, rect().height()))
|
||
);
|
||
QApplication::postEvent(this, helpEvent);
|
||
lineEdit()->setSelection(0, 0);
|
||
}
|
||
else {
|
||
if (!hasSel) {
|
||
selectNumber();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
void QuantitySpinBox::focusOutEvent(QFocusEvent* event)
|
||
{
|
||
Q_D(const QuantitySpinBox);
|
||
|
||
validateInput();
|
||
|
||
if (d->normalize) {
|
||
normalize();
|
||
}
|
||
|
||
QToolTip::hideText();
|
||
QAbstractSpinBox::focusOutEvent(event);
|
||
}
|
||
|
||
void QuantitySpinBox::clear()
|
||
{
|
||
QAbstractSpinBox::clear();
|
||
}
|
||
|
||
void QuantitySpinBox::selectNumber()
|
||
{
|
||
QString expr = QStringLiteral("^([%1%2]?[0-9\\%3]*)\\%4?([0-9]+(%5[%1%2]?[0-9]+)?)")
|
||
.arg(locale().negativeSign())
|
||
.arg(locale().positiveSign())
|
||
.arg(locale().groupSeparator())
|
||
.arg(locale().decimalPoint())
|
||
.arg(locale().exponential());
|
||
auto rmatch = QRegularExpression(expr).match(lineEdit()->text());
|
||
if (rmatch.hasMatch()) {
|
||
lineEdit()->setSelection(0, rmatch.capturedLength());
|
||
}
|
||
}
|
||
|
||
QString QuantitySpinBox::textFromValue(const Base::Quantity& value) const
|
||
{
|
||
QString str = getUserString(value);
|
||
if (qAbs(value.getValue()) >= 1000.0) {
|
||
str.remove(locale().groupSeparator());
|
||
}
|
||
return str;
|
||
}
|
||
|
||
Base::Quantity QuantitySpinBox::valueFromText(const QString& text) const
|
||
{
|
||
Q_D(const QuantitySpinBox);
|
||
|
||
QString copy = text;
|
||
QValidator::State state = QValidator::Acceptable;
|
||
const App::ObjectIdentifier& path = getPath();
|
||
Base::Quantity quant = d->validateAndInterpret(copy, state, path);
|
||
if (state != QValidator::Acceptable) {
|
||
fixup(copy);
|
||
quant = d->validateAndInterpret(copy, state, path);
|
||
}
|
||
|
||
return quant;
|
||
}
|
||
|
||
QValidator::State QuantitySpinBox::validate(QString& text, int& pos) const
|
||
{
|
||
Q_D(const QuantitySpinBox);
|
||
Q_UNUSED(pos)
|
||
|
||
QValidator::State state;
|
||
const App::ObjectIdentifier& path = getPath();
|
||
d->validateAndInterpret(text, state, path);
|
||
return state;
|
||
}
|
||
|
||
void QuantitySpinBox::fixup(QString& input) const
|
||
{
|
||
input.remove(locale().groupSeparator());
|
||
}
|
||
|
||
|
||
#include "moc_QuantitySpinBox.cpp"
|
||
#include "moc_QuantitySpinBox_p.cpp"
|