diff --git a/src/Gui/Dialogs/DlgExpressionInput.cpp b/src/Gui/Dialogs/DlgExpressionInput.cpp index 78c892f39b..7e4ec4a6dd 100644 --- a/src/Gui/Dialogs/DlgExpressionInput.cpp +++ b/src/Gui/Dialogs/DlgExpressionInput.cpp @@ -40,6 +40,7 @@ #include #include #include +#include #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()) { - 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" diff --git a/src/Gui/Dialogs/DlgExpressionInput.h b/src/Gui/Dialogs/DlgExpressionInput.h index ce62aa2d95..0bc88fdda9 100644 --- a/src/Gui/Dialogs/DlgExpressionInput.h +++ b/src/Gui/Dialogs/DlgExpressionInput.h @@ -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; diff --git a/src/Gui/Dialogs/DlgExpressionInput.ui b/src/Gui/Dialogs/DlgExpressionInput.ui index 8736c83604..83a8f077b2 100644 --- a/src/Gui/Dialogs/DlgExpressionInput.ui +++ b/src/Gui/Dialogs/DlgExpressionInput.ui @@ -18,7 +18,7 @@ - 300 + 350 0 @@ -45,10 +45,30 @@ 0 + + + + + + + 0 + 0 + + + + + 10 + 10 + + + + + + - + 0 0 @@ -71,6 +91,12 @@ + + + 0 + 0 + + Result @@ -78,6 +104,12 @@ + + + 0 + 0 + + @@ -130,6 +162,9 @@ Qt::Orientation::Horizontal + + QSizePolicy::Policy::Fixed + 0 @@ -140,39 +175,6 @@ - - - - - - - 0 - 0 - - - - - 10 - 10 - - - - - - - - Qt::Orientation::Horizontal - - - - 0 - 2 - - - - - - @@ -312,24 +314,41 @@ - - - - 0 - 0 - - - - QDialogButtonBox::StandardButton::Ok|QDialogButtonBox::StandardButton::Reset - - + + + + + Qt::Horizontal + + + + 0 + 20 + + + + + + + + + 0 + 0 + + + + QDialogButtonBox::StandardButton::Ok|QDialogButtonBox::StandardButton::Reset + + + + - Gui::ExpressionLineEdit - QLineEdit + Gui::ExpressionTextEdit + QTextEdit
ExpressionCompleter.h
diff --git a/src/Gui/ExpressionCompleter.cpp b/src/Gui/ExpressionCompleter.cpp index 6f3dc29f50..fc8058f0ef 100644 --- a/src/Gui/ExpressionCompleter.cpp +++ b/src/Gui/ExpressionCompleter.cpp @@ -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 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(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() diff --git a/src/Gui/ExpressionCompleter.h b/src/Gui/ExpressionCompleter.h index cf7f5f20c9..fce6efa163 100644 --- a/src/Gui/ExpressionCompleter.h +++ b/src/Gui/ExpressionCompleter.h @@ -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; diff --git a/src/Gui/QuantitySpinBox.cpp b/src/Gui/QuantitySpinBox.cpp index b7febd34e6..9f380120d8 100644 --- a/src/Gui/QuantitySpinBox.cpp +++ b/src/Gui/QuantitySpinBox.cpp @@ -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); diff --git a/src/Gui/SpinBox.cpp b/src/Gui/SpinBox.cpp index 89287b4f54..2fd840fecc 100644 --- a/src/Gui/SpinBox.cpp +++ b/src/Gui/SpinBox.cpp @@ -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); } diff --git a/src/Gui/Stylesheets/FreeCAD.qss b/src/Gui/Stylesheets/FreeCAD.qss index 8783b77067..05aab36e2c 100644 --- a/src/Gui/Stylesheets/FreeCAD.qss +++ b/src/Gui/Stylesheets/FreeCAD.qss @@ -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; } diff --git a/src/Gui/Widgets.cpp b/src/Gui/Widgets.cpp index 17d1b1a397..a4d1f1505e 100644 --- a/src/Gui/Widgets.cpp +++ b/src/Gui/Widgets.cpp @@ -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); }