Merge pull request #22964 from B0cho/bugfix/CORE-Expression-dialog-widgets

Core: Enhance 'Expression editor' input and resize behavior
This commit is contained in:
Kacper Donat
2025-09-07 17:35:29 +02:00
committed by GitHub
9 changed files with 248 additions and 92 deletions

View File

@@ -40,6 +40,7 @@
#include <App/VarSet.h>
#include <Base/Console.h>
#include <Base/Tools.h>
#include <regex>
#include "Dialogs/DlgExpressionInput.h"
#include "ui_DlgExpressionInput.h"
@@ -64,7 +65,6 @@ DlgExpressionInput::DlgExpressionInput(const App::ObjectIdentifier & _path,
, path(_path)
, discarded(false)
, impliedUnit(_impliedUnit)
, minimumWidth(10)
, varSetsVisible(false)
, comboBoxGroup(this)
{
@@ -79,22 +79,22 @@ DlgExpressionInput::DlgExpressionInput(const App::ObjectIdentifier & _path,
initializeVarSets();
// Connect signal(s)
connect(ui->expression, &ExpressionLineEdit::textChanged,
connect(ui->expression, &ExpressionTextEdit::textChanged,
this, &DlgExpressionInput::textChanged);
connect(discardBtn, &QPushButton::clicked,
this, &DlgExpressionInput::setDiscarded);
if (expression) {
ui->expression->setText(QString::fromStdString(expression->toString()));
ui->expression->setPlainText(QString::fromStdString(expression->toString()));
}
else {
QVariant text = parent->property("text");
if (text.canConvert<QString>()) {
ui->expression->setText(text.toString());
ui->expression->setPlainText(text.toString());
}
}
// Set document object on line edit to create auto completer
// Set document object on text edit to create auto completer
DocumentObject * docObj = path.getDocumentObject();
ui->expression->setDocumentObject(docObj);
@@ -114,8 +114,11 @@ DlgExpressionInput::DlgExpressionInput(const App::ObjectIdentifier & _path,
setAttribute(Qt::WA_TranslucentBackground, true);
}
else {
ui->expression->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
ui->horizontalSpacer_3->changeSize(0, 2);
ui->expression->setMinimumWidth(300);
ui->expression->setMinimumHeight(80);
ui->msg->setWordWrap(true);
ui->msg->setMaximumHeight(200);
ui->msg->setMinimumWidth(280);
ui->verticalLayout->setContentsMargins(9, 9, 9, 9);
this->adjustSize();
// It is strange that (at least on Linux) DlgExpressionInput will shrink
@@ -348,20 +351,22 @@ void DlgExpressionInput::checkExpression(const QString& text)
}
numberRange.throwIfOutOfRange(value);
ui->msg->setText(msg);
message = msg.toStdString();
}
else {
ui->msg->setText(QString::fromStdString(result->toString()));
message = result->toString();
}
setMsgText();
}
}
static const bool NoCheckExpr = false;
void DlgExpressionInput::textChanged(const QString &text)
void DlgExpressionInput::textChanged()
{
const QString& text = ui->expression->toPlainText();
if (text.isEmpty()) {
okBtn->setDisabled(true);
discardBtn->setDefault(true);
@@ -371,17 +376,6 @@ void DlgExpressionInput::textChanged(const QString &text)
okBtn->setDefault(true);
try {
//resize the input field according to text size
QFontMetrics fm(ui->expression->font());
int width = QtTools::horizontalAdvance(fm, text) + 15;
if (width < minimumWidth)
ui->expression->setMinimumWidth(minimumWidth);
else
ui->expression->setMinimumWidth(width);
if(this->width() < ui->expression->minimumWidth())
setMinimumWidth(ui->expression->minimumWidth());
checkExpression(text);
if (varSetsVisible) {
// If varsets are visible, check whether the varset info also
@@ -391,7 +385,8 @@ void DlgExpressionInput::textChanged(const QString &text)
}
}
catch (Base::Exception & e) {
ui->msg->setText(QString::fromUtf8(e.what()));
message = e.what();
setMsgText();
QPalette p(ui->msg->palette());
p.setColor(QPalette::WindowText, Qt::red);
ui->msg->setPalette(p);
@@ -405,19 +400,6 @@ void DlgExpressionInput::setDiscarded()
reject();
}
void DlgExpressionInput::setExpressionInputSize(int width, int height)
{
if (ui->expression->minimumHeight() < height) {
ui->expression->setMinimumHeight(height);
}
if (ui->expression->minimumWidth() < width) {
ui->expression->setMinimumWidth(width);
}
minimumWidth = width;
}
void DlgExpressionInput::mouseReleaseEvent(QMouseEvent* event)
{
Q_UNUSED(event);
@@ -917,7 +899,7 @@ void DlgExpressionInput::updateVarSetInfo(bool checkExpr)
if (checkExpr) {
// We have to check the text of the expression as well
try {
checkExpression(ui->expression->text());
checkExpression(ui->expression->toPlainText());
}
catch (Base::Exception&) {
okBtn->setEnabled(false);
@@ -935,4 +917,50 @@ bool DlgExpressionInput::needReportOnVarSet()
return reportGroup(comboBoxGroup.currentText()) || reportName();
}
void DlgExpressionInput::resizeEvent(QResizeEvent *event)
{
// When the dialog is resized, message text may need to be re-wrapped
if (!this->message.empty() && event->size() != event->oldSize()) {
setMsgText();
}
QDialog::resizeEvent(event);
}
void DlgExpressionInput::setMsgText()
{
if (!this->message.size()) {
return;
}
const QFontMetrics msgFontMetrics{ ui->msg->font() };
// find words longer than length of msg widget
// then insert newline to wrap it
std::string wrappedMsg{};
const int msgContentWidth = ui->msg->width() * 0.85; // 0.85 is a magic number for some padding
const int maxWordLength = msgContentWidth / msgFontMetrics.averageCharWidth();
const auto wrappableWordPattern = std::regex{ "\\S{" + std::to_string(maxWordLength) + "}" };
auto it = std::sregex_iterator{ this->message.cbegin(), this->message.cend(), wrappableWordPattern };
const auto itEnd = std::sregex_iterator{};
int lastPos = 0;
for (; it != itEnd; ++it) {
wrappedMsg += this->message.substr(lastPos, it->position() - lastPos);
wrappedMsg += it->str() + "\n";
lastPos = it->position() + it->length();
}
wrappedMsg += this->message.substr(lastPos);
ui->msg->setText(QString::fromStdString(wrappedMsg));
// elide text if it is going out of widget bounds
// note: this is only 'rough elide', as this text is usually not very long;
const int msgLinesLimit = 3;
if (wrappedMsg.size() > msgContentWidth / msgFontMetrics.averageCharWidth() * msgLinesLimit) {
const QString elidedMsg = msgFontMetrics.elidedText(QString::fromStdString(wrappedMsg), Qt::ElideRight, msgContentWidth * msgLinesLimit);
ui->msg->setText(elidedMsg);
}
}
#include "moc_DlgExpressionInput.cpp"

View File

@@ -77,7 +77,6 @@ public:
bool discardedFormula() const { return discarded; }
QPoint expressionPosition() const;
void setExpressionInputSize(int width, int height);
public Q_SLOTS:
void show();
@@ -86,6 +85,7 @@ public Q_SLOTS:
protected:
void mouseReleaseEvent(QMouseEvent* event) override;
void mousePressEvent(QMouseEvent* event) override;
void resizeEvent(QResizeEvent* event) override;
private:
Base::Type getTypePath();
@@ -109,9 +109,10 @@ private:
const App::DocumentObject* obj, QString& message) const;
bool isGroupNameValid(const QString& nameGroup,
QString& message) const;
void setMsgText();
private Q_SLOTS:
void textChanged(const QString & text);
void textChanged();
void setDiscarded();
void onCheckVarSets(int state);
void onVarSetSelected(int index);
@@ -127,7 +128,7 @@ private:
const Base::Unit impliedUnit;
NumberRange numberRange;
int minimumWidth;
std::string message;
bool varSetsVisible;
QPushButton* okBtn = nullptr;

View File

@@ -18,7 +18,7 @@
</property>
<property name="minimumSize">
<size>
<width>300</width>
<width>350</width>
<height>0</height>
</size>
</property>
@@ -45,10 +45,30 @@
<property name="margin" stdset="0">
<number>0</number>
</property>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_5">
<item>
<widget class="Gui::ExpressionTextEdit" name="expression">
<property name="sizePolicy">
<sizepolicy hsizetype="Ignored" vsizetype="MinimumExpanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>10</width>
<height>10</height>
</size>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QFrame" name="ctrlArea">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<sizepolicy hsizetype="Preferred" vsizetype="Minimum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
@@ -71,6 +91,12 @@
</property>
<item>
<widget class="QLabel" name="label">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Result</string>
</property>
@@ -78,6 +104,12 @@
</item>
<item>
<widget class="QLabel" name="msg">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Minimum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="palette">
<palette>
<active>
@@ -130,6 +162,9 @@
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Policy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>0</width>
@@ -140,39 +175,6 @@
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_5">
<item>
<widget class="Gui::ExpressionLineEdit" name="expression">
<property name="sizePolicy">
<sizepolicy hsizetype="Ignored" vsizetype="Ignored">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>10</width>
<height>10</height>
</size>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_3">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>0</width>
<height>2</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
@@ -312,24 +314,41 @@
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::StandardButton::Ok|QDialogButtonBox::StandardButton::Reset</set>
</property>
</widget>
<layout class="QHBoxLayout" name="horizontalLayout_6">
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>0</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::StandardButton::Ok|QDialogButtonBox::StandardButton::Reset</set>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>Gui::ExpressionLineEdit</class>
<extends>QLineEdit</extends>
<class>Gui::ExpressionTextEdit</class>
<extends>QTextEdit</extends>
<header>ExpressionCompleter.h</header>
</customwidget>
</customwidgets>

View File

@@ -888,6 +888,8 @@ void ExpressionCompleter::slotUpdate(const QString& prefix, int pos)
else if (auto itemView = popup()) {
itemView->setVisible(false);
}
Q_EMIT completerSlotUpdated();
}
ExpressionValidator::ExpressionValidator(QObject* parent)
@@ -1085,6 +1087,11 @@ void ExpressionTextEdit::setExactMatch(bool enabled)
}
}
QSize ExpressionTextEdit::sizeHint() const
{
return QSize(200, 30);
}
void ExpressionTextEdit::setDocumentObject(const App::DocumentObject* currentDocObj)
{
if (completer) {
@@ -1111,6 +1118,10 @@ void ExpressionTextEdit::setDocumentObject(const App::DocumentObject* currentDoc
&ExpressionTextEdit::textChanged2,
completer,
&ExpressionCompleter::slotUpdate);
connect(completer,
&ExpressionCompleter::completerSlotUpdated,
this,
&ExpressionTextEdit::adjustCompleterToCursor);
}
}
@@ -1130,6 +1141,7 @@ void ExpressionTextEdit::slotTextChanged()
{
if (!block) {
QTextCursor cursor = textCursor();
completer->popup()->setVisible(false); // hide the completer to avoid flickering
Q_EMIT textChanged2(cursor.block().text(), cursor.positionInBlock());
}
}
@@ -1152,6 +1164,57 @@ void ExpressionTextEdit::slotCompleteText(const QString& completionPrefix)
void ExpressionTextEdit::keyPressEvent(QKeyEvent* e)
{
Base::FlagToggler<bool> flag(block, true);
// Shift+Enter - insert a new line
if ((e->modifiers() & Qt::ShiftModifier) && (e->key() == Qt::Key_Enter || e->key() == Qt::Key_Return)) {
this->setPlainText(this->toPlainText() + QLatin1Char('\n'));
this->moveCursor(QTextCursor::End);
if (completer) {
completer->popup()->setVisible(false);
}
e->accept();
return;
}
// handling if completer is visible
if (completer && completer->popup()->isVisible()) {
switch (e->key()) {
case Qt::Key_Enter:
case Qt::Key_Return:
case Qt::Key_Escape:
case Qt::Key_Backtab:
// default action
e->ignore();
return;
case Qt::Key_Tab:
// if no completion is selected, take top one
if (!completer->popup()->currentIndex().isValid()) {
completer->popup()->setCurrentIndex(completer->popup()->model()->index(0, 0));
}
// insert completion
completer->setCurrentRow(completer->popup()->currentIndex().row());
slotCompleteText(completer->currentCompletion());
// refresh completion list
completer->setCompletionPrefix(completer->currentCompletion());
if (completer->completionCount() == 1) {
completer->popup()->setVisible(false);
}
e->accept();
return;
default:
break;
}
}
// Enter, Return or Tab - request default action
if (e->key() == Qt::Key_Enter || e->key() == Qt::Key_Return || e->key() == Qt::Key_Tab) {
e->ignore();
return;
}
QPlainTextEdit::keyPressEvent(e);
}
@@ -1180,6 +1243,49 @@ void ExpressionTextEdit::contextMenuEvent(QContextMenuEvent* event)
delete menu;
}
void ExpressionTextEdit::adjustCompleterToCursor()
{
if (!completer || !completer->popup()) {
return;
}
// get longest string width
int maxCompletionWidth = 0;
for (int i = 0; i < completer->completionModel()->rowCount(); ++i) {
const QModelIndex index = completer->completionModel()->index(i, 0);
const QString element = completer->completionModel()->data(index).toString();
maxCompletionWidth = std::max(maxCompletionWidth, static_cast<int>(element.size()) * completer->popup()->fontMetrics().averageCharWidth());
}
if (maxCompletionWidth == 0) {
return; // no completions available
}
const QPoint cursorPos = cursorRect(textCursor()).topLeft();
int posX = cursorPos.x();
int posY = cursorPos.y();
completer->popup()->setMaximumWidth(this->viewport()->width() * 0.6);
completer->popup()->setMaximumHeight(this->viewport()->height() * 0.6);
const QSize completerSize { maxCompletionWidth + 40, completer->popup()->size().height() }; // 40 is margin for scrollbar
completer->popup()->resize(completerSize);
// vertical correction
if (posY + completerSize.height() > viewport()->height()) {
posY -= completerSize.height();
} else {
posY += fontMetrics().height();
}
// horizontal correction
if (posX + completerSize.width() > viewport()->width()) {
posX = viewport()->width() - completerSize.width();
}
completer->popup()->move(mapToGlobal(QPoint{ posX, posY }));
completer->popup()->setVisible(true);
}
///////////////////////////////////////////////////////////////////////
ExpressionParameter* ExpressionParameter::instance()

View File

@@ -78,6 +78,9 @@ public:
public Q_SLOTS:
void slotUpdate(const QString &prefix, int pos);
Q_SIGNALS:
void completerSlotUpdated();
private:
void init();
QString pathFromIndex ( const QModelIndex & index ) const override;
@@ -130,6 +133,7 @@ public:
bool completerActive() const;
void hideCompleter();
void setExactMatch(bool enabled=true);
QSize sizeHint() const override;
protected:
void keyPressEvent(QKeyEvent * event) override;
void contextMenuEvent(QContextMenuEvent * event) override;
@@ -138,6 +142,7 @@ Q_SIGNALS:
public Q_SLOTS:
void slotTextChanged();
void slotCompleteText(const QString & completionPrefix);
void adjustCompleterToCursor();
private:
ExpressionCompleter * completer;
bool block;

View File

@@ -618,7 +618,6 @@ void QuantitySpinBox::openFormulaDialog()
QPoint pos = mapToGlobal(QPoint(0,0));
box->move(pos-box->expressionPosition());
box->setExpressionInputSize(width(), height());
Gui::adjustDialogPosition(box);
Q_EMIT showFormulaDialog(true);

View File

@@ -206,7 +206,6 @@ void ExpressionSpinBox::openFormulaDialog()
QPoint pos = spinbox->mapToGlobal(QPoint(0,0));
box->move(pos-box->expressionPosition());
box->setExpressionInputSize(spinbox->width(), spinbox->height());
Gui::adjustDialogPosition(box);
}

View File

@@ -804,7 +804,7 @@ https://doc.qt.io/qt-5/stylesheet-examples.html#customizing-specific-widgets
report view
--------------------------------------------------------------------------- */
QTextEdit {
background-color: @PrimaryColor;
background-color: @TextEditFieldBackgroundColor;
border-top: 1px solid @GeneralBorderColor;
}

View File

@@ -1606,7 +1606,6 @@ void ExpLineEdit::openFormulaDialog()
QPoint pos = mapToGlobal(QPoint(0,0));
box->move(pos-box->expressionPosition());
box->setExpressionInputSize(width(), height());
Gui::adjustDialogPosition(box);
}