diff --git a/src/Gui/Stylesheets/FreeCAD.qss b/src/Gui/Stylesheets/FreeCAD.qss index 3cf0742e3d..9054f55c8b 100644 --- a/src/Gui/Stylesheets/FreeCAD.qss +++ b/src/Gui/Stylesheets/FreeCAD.qss @@ -922,6 +922,12 @@ QToolBar::separator:vertical { background-color: @GeneralBorderColor; } +MRichTextEdit[floating="true"] { + border: 1px solid @GeneralBorderColor; + border-radius: 2px; + padding: 1px; +} + /*The "show more" button (it can also be stylable with "QToolBarExtension" icon is not working Qproperty works but breaks when you move the toolbar*/ diff --git a/src/Mod/AddonManager b/src/Mod/AddonManager index 937b687723..d9c593594a 160000 --- a/src/Mod/AddonManager +++ b/src/Mod/AddonManager @@ -1 +1 @@ -Subproject commit 937b6877239dc78ef59eeefe8099e5f14243eda1 +Subproject commit d9c593594ae4187d09b3ec9c7989db6c3a22d7a2 diff --git a/src/Mod/TechDraw/App/DrawRichAnno.cpp b/src/Mod/TechDraw/App/DrawRichAnno.cpp index 755796f6b3..b9339dcd76 100644 --- a/src/Mod/TechDraw/App/DrawRichAnno.cpp +++ b/src/Mod/TechDraw/App/DrawRichAnno.cpp @@ -20,6 +20,7 @@ * * ***************************************************************************/ +#include #include "DrawRichAnno.h" #include "DrawRichAnnoPy.h" // generated from DrawRichAnnoPy.xml @@ -41,6 +42,8 @@ DrawRichAnno::DrawRichAnno() "Object to which this annontation is attached"); ADD_PROPERTY_TYPE(AnnoText, (""), group, App::Prop_None, "Annotation text"); ADD_PROPERTY_TYPE(ShowFrame, (true), group, App::Prop_None, "Outline rectangle on/off"); + // Necessary to support legacy files made before #24624. + ADD_PROPERTY_TYPE(OriginCentered, (false), group, App::Prop_None, "Center the annotation on it's origin."); ADD_PROPERTY_TYPE(MaxWidth, (-1.0), group, App::Prop_None, "Width limit before auto wrap"); Caption.setStatus(App::Property::Hidden, true); Scale.setStatus(App::Property::Hidden, true); @@ -48,6 +51,40 @@ DrawRichAnno::DrawRichAnno() } +void DrawRichAnno::Restore(Base::XMLReader& reader) +{ + bool originCenteredFound = false; + + // Start parsing the properties block. + reader.readElement("Properties"); + int propCount = reader.getAttribute("Count"); + + for (int i = 0; i < propCount; i++) { + reader.readElement("Property"); + const char* propName = reader.getAttribute("name"); + + // The "checking" part: + if (strcmp(propName, "OriginCentered") == 0) { + originCenteredFound = true; + } + + // The "restoring" part: + App::Property* prop = getPropertyByName(propName); + if (prop) { + prop->Restore(reader); // Restore the value + } + + reader.readEndElement("Property"); + } + + reader.readEndElement("Properties"); + + // Ensure backward compatibility: Old files have their anno centered on origin. + if (!originCenteredFound) { + OriginCentered.setValue(true); + } +} + void DrawRichAnno::onChanged(const App::Property* prop) { if (!isRestoring()) { diff --git a/src/Mod/TechDraw/App/DrawRichAnno.h b/src/Mod/TechDraw/App/DrawRichAnno.h index 199c20c791..b380ecb7e7 100644 --- a/src/Mod/TechDraw/App/DrawRichAnno.h +++ b/src/Mod/TechDraw/App/DrawRichAnno.h @@ -44,7 +44,9 @@ public: App::PropertyString AnnoText; App::PropertyBool ShowFrame; App::PropertyFloat MaxWidth; + App::PropertyBool OriginCentered; + void Restore(Base::XMLReader& reader) override; short mustExecute() const override; App::DocumentObjectExecReturn *execute() override; diff --git a/src/Mod/TechDraw/Gui/CommandAnnotate.cpp b/src/Mod/TechDraw/Gui/CommandAnnotate.cpp index dd99af44c5..bad39a35e9 100644 --- a/src/Mod/TechDraw/Gui/CommandAnnotate.cpp +++ b/src/Mod/TechDraw/Gui/CommandAnnotate.cpp @@ -150,7 +150,7 @@ CmdTechDrawRichTextAnnotation::CmdTechDrawRichTextAnnotation() sToolTipText = QT_TR_NOOP("Inserts a rich text annotation in the current page"); sWhatsThis = "TechDraw_RichTextAnnotation"; sStatusTip = sToolTipText; - sPixmap = "actions/TechDraw_RichTextAnnotation"; + sPixmap = "actions/TechDraw_Annotation"; } void CmdTechDrawRichTextAnnotation::activated(int iMsg) diff --git a/src/Mod/TechDraw/Gui/QGCustomText.cpp b/src/Mod/TechDraw/Gui/QGCustomText.cpp index e1a057d652..814d65fc40 100644 --- a/src/Mod/TechDraw/Gui/QGCustomText.cpp +++ b/src/Mod/TechDraw/Gui/QGCustomText.cpp @@ -26,6 +26,9 @@ # include # include # include +#include +#include +#include #include #include @@ -167,6 +170,8 @@ void QGCustomText::setTightBounding(bool tight) void QGCustomText::paint ( QPainter * painter, const QStyleOptionGraphicsItem * option, QWidget * widget) { QStyleOptionGraphicsItem myOption(*option); + // Remove HasFocus state to prevent the dashed rectangle from being drawn + myOption.state &= ~QStyle::State_HasFocus; myOption.state &= ~QStyle::State_Selected; // painter->setPen(Qt::green); @@ -269,3 +274,78 @@ void QGCustomText::makeMark(Base::Vector3d v) makeMark(v.x, v.y); } +void QGCustomText::focusInEvent(QFocusEvent* event) +{ + // Store the initial cursor state when the item gains focus + m_lastCursor = textCursor(); + QGraphicsTextItem::focusInEvent(event); +} + +void QGCustomText::keyPressEvent(QKeyEvent* event) +{ + // If the user returns more than once, then the format (the font + // size we set as default) is lost. So we need to handle manually + if (event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter) { + QTextCursor cursor = textCursor(); + + // 1. Preserve the all-important character format from the current cursor position. + QTextCharFormat formatToPreserve = cursor.charFormat(); + + // 2. Preserve the block format (for things like alignment, indentation). + QTextBlockFormat blockFormatToPreserve = cursor.blockFormat(); + + // 3. Manually insert a new block (a newline). + // This automatically deletes any selected text, which is the correct behavior. + cursor.insertBlock(blockFormatToPreserve, formatToPreserve); + + // 4. After inserting the block, the cursor is at the start of the new line. + // Its charFormat should already be correct because we passed it to insertBlock. + // We don't need to do anything further with the cursor. + + // 5. Explicitly apply the cursor back to the item to ensure the view updates. + setTextCursor(cursor); + + // 6. Notify that the selection/cursor has changed. + checkCursorChange(); + + // 7. Accept the event to stop it from being processed further. + event->accept(); + return; + } + + // Let the base class handle the key press first (which moves the cursor) + QGraphicsTextItem::keyPressEvent(event); + // Now check if the cursor or selection changed as a result + checkCursorChange(); +} + +void QGCustomText::mousePressEvent(QGraphicsSceneMouseEvent* event) +{ + // Let the base class handle the mouse press first + QGraphicsTextItem::mousePressEvent(event); + checkCursorChange(); +} + +void QGCustomText::mouseReleaseEvent(QGraphicsSceneMouseEvent* event) +{ + // Let the base class handle the mouse release first + QGraphicsTextItem::mouseReleaseEvent(event); + checkCursorChange(); +} + +void QGCustomText::checkCursorChange() +{ + QTextCursor currentCursor = textCursor(); + + // Compare the properties of the cursors that define selection and position. + if (currentCursor.position() != m_lastCursor.position() + || currentCursor.anchor() != m_lastCursor.anchor()) { + // The anchor and position define the selection. If either has changed, + // the selection/cursor has changed. + Q_EMIT selectionChanged(); + } + + m_lastCursor = currentCursor; +} + +#include diff --git a/src/Mod/TechDraw/Gui/QGCustomText.h b/src/Mod/TechDraw/Gui/QGCustomText.h index f0b48ef649..1a2983e6d3 100644 --- a/src/Mod/TechDraw/Gui/QGCustomText.h +++ b/src/Mod/TechDraw/Gui/QGCustomText.h @@ -27,6 +27,7 @@ #include #include #include +#include QT_BEGIN_NAMESPACE class QPainter; @@ -43,6 +44,7 @@ namespace TechDrawGui class TechDrawGuiExport QGCustomText : public QGraphicsTextItem { + Q_OBJECT public: explicit QGCustomText(QGraphicsItem* parent = nullptr); ~QGCustomText() override {} @@ -81,7 +83,14 @@ public: void makeMark(double x, double y); void makeMark(Base::Vector3d v); +Q_SIGNALS: + void selectionChanged(); + protected: + void keyPressEvent(QKeyEvent* event) override; + void mousePressEvent(QGraphicsSceneMouseEvent* event) override; + void mouseReleaseEvent(QGraphicsSceneMouseEvent* event) override; + void focusInEvent(QFocusEvent* event) override; void hoverEnterEvent(QGraphicsSceneHoverEvent *event) override; void hoverLeaveEvent(QGraphicsSceneHoverEvent *event) override; QVariant itemChange(GraphicsItemChange change, const QVariant &value) override; @@ -92,6 +101,8 @@ protected: QColor m_colNormal; private: + void checkCursorChange(); + QTextCursor m_lastCursor; }; diff --git a/src/Mod/TechDraw/Gui/QGIRichAnno.cpp b/src/Mod/TechDraw/Gui/QGIRichAnno.cpp index 49658f1799..c534ec77ed 100644 --- a/src/Mod/TechDraw/Gui/QGIRichAnno.cpp +++ b/src/Mod/TechDraw/Gui/QGIRichAnno.cpp @@ -22,17 +22,27 @@ # include +# include # include # include # include # include # include +# include +# include # include # include # include +# include # include +# include +# include +# include #include +#include +#include + #include #include @@ -51,13 +61,26 @@ using namespace TechDraw; using namespace TechDrawGui; using DU = DrawUtil; +const double QGIRichAnno::HandleInteractionMargin = 30.0; // Scene units for hover/click +const double QGIRichAnno::MinTextWidthDocument = 5.0; // Min width in document units //************************************************************** QGIRichAnno::QGIRichAnno() : - m_isExportingPdf(false), m_isExportingSvg(false), m_hasHover(false) + m_isExportingPdf(false), + m_isExportingSvg(false), + m_currentResizeHandle(ResizeHandle::NoHandle), + m_isResizing(false), + m_isDraggingMidResize(false), + m_transactionOpen(false), + m_dragStartMouseScenePos(), + m_initialItemScenePos(), + m_initialTextWidthScene(0.0), + m_isEditing(false), + m_textScaleFactor(1.0), + m_lastGoodWidthScene(0.0) { setHandlesChildEvents(false); - setAcceptHoverEvents(false); + setAcceptHoverEvents(true); // Enable hover events for cursor changes setFlag(QGraphicsItem::ItemIsSelectable, true); setFlag(QGraphicsItem::ItemIsMovable, true); setFlag(QGraphicsItem::ItemSendsScenePositionChanges, true); @@ -68,15 +91,18 @@ QGIRichAnno::QGIRichAnno() : m_text->setDefaultTextColor(PreferencesGui::normalQColor()); addToGroup(m_text); m_text->setZValue(ZVALUE::DIMENSION); - m_text->centerAt(0.0, 0.0); m_rect = new QGCustomRect(); addToGroup(m_rect); m_rect->setZValue(ZVALUE::DIMENSION - 1); - m_rect->centerAt(0.0, 0.0); setZValue(ZVALUE::DIMENSION); + connect(m_text->document(), + &QTextDocument::contentsChanged, + this, + &QGIRichAnno::onContentsChanged); + connect(m_text, &QGCustomText::selectionChanged, this, &QGIRichAnno::selectionChanged); } void QGIRichAnno::updateView(bool update) @@ -100,24 +126,15 @@ void QGIRichAnno::updateView(bool update) setFlag(QGraphicsItem::ItemIsMovable, true); } - if (annoFeat->X.isTouched() || - annoFeat->Y.isTouched()) { - float x = Rez::guiX(annoFeat->X.getValue()); - float y = Rez::guiX(annoFeat->Y.getValue()); - m_text->centerAt(x, -y); - m_rect->centerAt(x, -y); - } + // Convert the word processing font size spec (in typographic points) to scene units for + // the screen or pdf rendering + constexpr double mmPerPoint {25.4 / 72}; // mm/in / points/inch + m_textScaleFactor = Rez::getRezFactor() * mmPerPoint; // scene units per point: 3.53 + m_text->setScale(m_textScaleFactor); draw(); } -void QGIRichAnno::drawBorder() -{ -////Leaders have no border! -// QGIView::drawBorder(); //good for debugging -} - - void QGIRichAnno::draw() { // Base::Console().log("QGIRA::draw() - %s - parent: %X\n", getFeature()->getNameInDocument(), parentItem()); @@ -146,45 +163,53 @@ void QGIRichAnno::setTextItem() // Base::Console().message("QGIRA::setTextItem() - %s - exportingSvg: %d\n", getViewName(), getExportingSvg()); TechDraw::DrawRichAnno* annoFeat = getFeature(); - // convert the text size - QString inHtml = QString::fromUtf8(annoFeat->AnnoText.getValue()); - QString outHtml = convertTextSizes(inHtml); + updateLayout(); - //position the text - prepareGeometryChange(); - // control auto line break - if (annoFeat->MaxWidth.getValue() > 0.0) { - // we have set a maximum width, so convert it to scene units - m_text->setTextWidth(Rez::guiX(annoFeat->MaxWidth.getValue())); - } else { - // we don't want to break lines - m_text->setTextWidth(annoFeat->MaxWidth.getValue()); + // convert the text size + if (!m_isEditing) { + m_text->setHtml(QString::fromUtf8(annoFeat->AnnoText.getValue())); } - m_text->setHtml(outHtml); + + // 1. Get the bounding rectangle of the text in its own local coordinates. + QRectF textParentRect = m_text->mapRectToParent(m_text->boundingRect()); + + QPointF offset(0.0, 0.0); + if (annoFeat->OriginCentered.getValue()) { + offset = QPointF(-textParentRect.width() / 2.0, -textParentRect.height() / 2.0); + } + m_text->setPos(offset); + textParentRect = m_text->mapRectToParent(m_text->boundingRect()); + + if (annoFeat->OriginCentered.getValue() || !getExportingSvg()) { + m_rect->setRect(textParentRect); + } + + m_rect->setPen(rectPen()); + m_rect->setBrush(Qt::NoBrush); + m_rect->setVisible(annoFeat->ShowFrame.getValue()); + if (getExportingSvg()) { + // Convert the word processing font size spec (in typographic points) to CSS pixels + // for Svg rendering + constexpr double mmPerPoint {25.4 / 72}; + constexpr double cssPxPerPoint {16 / 12}; // CSS says 12 pt text is 16 px high + m_text->setScale(cssPxPerPoint); + + // QSvgRenderer places the text's top edge flush with the item's origin. + // We must manually shift the QGraphicsTextItem down to create padding. + QTextBlock firstBlock = m_text->document()->begin(); + if (firstBlock.isValid()) { + QTextCursor cursor(firstBlock); + double fontSizePx = cursor.charFormat().fontPointSize(); + m_text->setY(m_text->pos().y() + (fontSizePx * mmPerPoint)); + } + // lines are correctly spaced on screen or in pdf, but svg needs this setLineSpacing(100); } - if (!getExportingSvg()) { - // screen or pdf rendering - m_text->centerAt(0.0, 0.0); - } - - // align the frame rectangle to the text - constexpr double frameMargin{10.0}; - QRectF outRect = m_text->boundingRect().adjusted(-frameMargin, -frameMargin, frameMargin, frameMargin); - m_rect->setPen(rectPen()); - m_rect->setBrush(Qt::NoBrush); - if (!getExportingSvg()) { - m_rect->setRect(outRect); - m_rect->setPos(m_text->pos().x() - frameMargin, m_text->pos().y() - frameMargin); - } - - if (annoFeat->ShowFrame.getValue()) { - m_rect->show(); - } else { - m_rect->hide(); + if (m_isEditing) { + Q_EMIT positionChanged(scenePos()); } } @@ -228,53 +253,6 @@ void QGIRichAnno::setLineSpacing(int lineSpacing) } } -//! convert the word processing font size spec (in typographic points) to scene units for the screen or -//! pdf rendering or to CSS pixels for Svg rendering -QString QGIRichAnno::convertTextSizes(const QString& inHtml) const -{ - constexpr double mmPerPoint{0.353}; // 25.4 mm/in / 72 points/inch - constexpr double cssPxPerPoint{1.333333}; // CSS says 12 pt text is 16 px high - double sceneUnitsPerPoint = Rez::getRezFactor() * mmPerPoint; // scene units per point: 3.53 - - QRegularExpression rxFontSize(QStringLiteral("font-size:([0-9]*)pt;")); - QRegularExpressionMatch match; - QStringList findList; - QStringList replList; - - // find each occurrence of "font-size:..." and calculate the equivalent size in scene units - // or CSS pixels - int pos = 0; - while ((pos = inHtml.indexOf(rxFontSize, pos, &match)) != -1) { - QString found = match.captured(0); - findList << found; - QString qsOldSize = match.captured(1); - - QString repl = found; - double newSize = qsOldSize.toDouble(); // in points - // The font size in the QGraphicsTextItem html is interpreted differently - // in QSvgRenderer rendering compared to painting the screen or pdf - if (getExportingSvg()) { - // scale point size to CSS pixels - newSize = newSize * cssPxPerPoint; - } else { - // scale point size to scene units - newSize = newSize * sceneUnitsPerPoint; - } - QString qsNewSize = QString::number(newSize, 'f', 2); - repl.replace(qsOldSize, qsNewSize); - replList << repl; - pos += match.capturedLength(); - } - QString outHtml = inHtml; - int iRepl = 0; - //TODO: check list for duplicates? - for ( ; iRepl < findList.size(); iRepl++) { - outHtml = outHtml.replace(findList[iRepl], replList[iRepl]); - } - - return outHtml; -} - TechDraw::DrawRichAnno* QGIRichAnno::getFeature() { return static_cast(getViewObject()); @@ -284,10 +262,7 @@ TechDraw::DrawRichAnno* QGIRichAnno::getFeature() // TODO: this rect is the right size, but not in the right place QRectF QGIRichAnno::boundingRect() const { - QRectF roughRect = m_text->boundingRect() | m_rect->boundingRect(); - double halfWidth = roughRect.width() / 2.0; - double halfHeight = roughRect.height() / 2.0; - return { -halfWidth, - halfHeight, halfWidth * 2.0, halfHeight * 2.0 }; + return childrenBoundingRect(); } void QGIRichAnno::paint ( QPainter * painter, const QStyleOptionGraphicsItem * option, QWidget * widget) { @@ -327,35 +302,368 @@ QFont QGIRichAnno::prefFont() return PreferencesGui::labelFontQFont(); } -void QGIRichAnno::mouseDoubleClickEvent(QGraphicsSceneMouseEvent* event) { - Q_UNUSED(event); - - TechDraw::DrawRichAnno *annotation = dynamic_cast(getViewObject()); - if (!annotation) +void QGIRichAnno::hoverMoveEvent(QGraphicsSceneHoverEvent* event) +{ + TechDraw::DrawRichAnno* annoFeat = getFeature(); + if (!annoFeat || annoFeat->isLocked()) { + setCursor(Qt::ArrowCursor); + QGIView::hoverMoveEvent(event); return; + } + // Mouse position in QGIRichAnno's local coordinates + QPointF localPos = mapFromScene(event->scenePos()); - QString text = QString::fromUtf8(annotation->AnnoText.getValue()); + // Calculate visual edges of m_rect in QGIRichAnno's local coordinates + double visualRectLeftEdgeX = m_rect->x() + m_rect->rect().left(); + double visualRectRightEdgeX = m_rect->x() + m_rect->rect().right(); - QDialog dialog(nullptr); - dialog.setWindowTitle(QObject::tr("Rich text editor")); - dialog.setMinimumWidth(400); - dialog.setMinimumHeight(400); + bool onLeftEdge = qAbs(localPos.x() - visualRectLeftEdgeX) < HandleInteractionMargin; + bool onRightEdge = qAbs(localPos.x() - visualRectRightEdgeX) < HandleInteractionMargin; - MRichTextEdit richEdit(&dialog, text); - QGridLayout gridLayout(&dialog); - gridLayout.addWidget(&richEdit, 0, 0, 1, 1); + if (onLeftEdge || onRightEdge) { + setCursor(Qt::SizeHorCursor); + } + else { + setCursor(Qt::ArrowCursor); + } + QGIView::hoverMoveEvent(event); +} - connect(&richEdit, &MRichTextEdit::saveText, &dialog, &QDialog::accept); - connect(&richEdit, &MRichTextEdit::editorFinished, &dialog, &QDialog::reject); +void QGIRichAnno::mousePressEvent(QGraphicsSceneMouseEvent* event) +{ + TechDraw::DrawRichAnno* annoFeat = getFeature(); + // Allow resizing even if MaxWidth is initially -1 or 0, as long as frame is shown + if (event->button() != Qt::LeftButton || !annoFeat || annoFeat->isLocked()) { + QGIView::mousePressEvent(event); + return; + } - if (dialog.exec()) { - QString newText = richEdit.toHtml(); - if (newText != text) { - App::GetApplication().setActiveTransaction("Set Rich Annotation Text"); - annotation->AnnoText.setValue(newText.toStdString()); - App::GetApplication().closeActiveTransaction(); + // Mouse position in QGIRichAnno's local coordinates + QPointF localPos = mapFromScene(event->scenePos()); + m_currentResizeHandle = ResizeHandle::NoHandle; + + // Calculate visual edges of m_rect in QGIRichAnno's local coordinates + // m_rect's geometry should be up-to-date from the last draw/updateView + double visualRectLeftEdgeX = m_rect->x() + m_rect->rect().left(); + double visualRectRightEdgeX = m_rect->x() + m_rect->rect().right(); + + if (qAbs(localPos.x() - visualRectLeftEdgeX) < HandleInteractionMargin) { + m_currentResizeHandle = ResizeHandle::LeftHandle; + } + else if (qAbs(localPos.x() - visualRectRightEdgeX) < HandleInteractionMargin) { + m_currentResizeHandle = ResizeHandle::RightHandle; + } + + if (m_currentResizeHandle != ResizeHandle::NoHandle) { + m_isResizing = true; + m_isDraggingMidResize = false; + m_transactionOpen = false; + m_dragStartMouseScenePos = event->scenePos(); + m_initialItemScenePos = this->scenePos(); + + // Determine initial text width for resizing + if (annoFeat->MaxWidth.getValue() > 0.0) { + m_initialTextWidthScene = Rez::guiX(annoFeat->MaxWidth.getValue()); } + else { + if (m_rect) { + m_initialTextWidthScene = m_rect->rect().width(); + } + else { + // Fallback, should not happen + m_initialTextWidthScene = Rez::guiX(MinTextWidthDocument * 2); + } + } + + m_lastGoodWidthScene = m_initialTextWidthScene; + + event->accept(); + } + else { + m_isResizing = false; + QGIView::mousePressEvent(event); } } +void QGIRichAnno::mouseMoveEvent(QGraphicsSceneMouseEvent* event) +{ + if (m_isResizing && m_currentResizeHandle != ResizeHandle::NoHandle) { + TechDraw::DrawRichAnno* annoFeat = getFeature(); + if (!annoFeat) { + return; + } + + if (!m_isDraggingMidResize) { // First actual move during this resize op + if (!Gui::Control().activeDialog()) { + Gui::Command::openCommand( + QObject::tr("Resize Rich Annotation").toStdString().c_str()); + } + m_transactionOpen = true; + m_isDraggingMidResize = true; + } + + QPointF currentMouseScenePos = event->scenePos(); + double mouseDeltaSceneX = currentMouseScenePos.x() - m_dragStartMouseScenePos.x(); + + // 1. Calculate the raw target width based on mouse movement + double targetVisualWidthScene = (m_currentResizeHandle == ResizeHandle::RightHandle) + ? m_initialTextWidthScene + mouseDeltaSceneX + : m_initialTextWidthScene - mouseDeltaSceneX; + + // Clamp against the geometric minimum + double geometricMinWidthScene = Rez::guiX(MinTextWidthDocument); + if (targetVisualWidthScene < geometricMinWidthScene) { + targetVisualWidthScene = geometricMinWidthScene; + } + + // --- 2. Perform Synchronous "What-If" Analysis --- + double finalVisualWidthScene = 0; + const double originalDocWidth = m_text->document()->textWidth(); + const double targetWidthLocal = targetVisualWidthScene / m_textScaleFactor; + m_text->document()->setTextWidth(targetWidthLocal); + QSizeF actualSizeLocal = m_text->document()->documentLayout()->documentSize(); + m_text->document()->setTextWidth(originalDocWidth); + + constexpr double tolerance = 1e-5; + if (actualSizeLocal.width() > targetWidthLocal + tolerance) { + finalVisualWidthScene = m_lastGoodWidthScene; + } + else { + finalVisualWidthScene = targetVisualWidthScene; + m_lastGoodWidthScene = finalVisualWidthScene; + } + + if (annoFeat->OriginCentered.getValue()) { + // --- 3. Calculate Final CENTER Position based on Validated Width --- + // Calculate the *actual* change in width that resulted from the what-if analysis. + const double actualWidthChange = finalVisualWidthScene - m_initialTextWidthScene; + + // The center of the item moves by exactly half of this actual change. + double positionDeltaX = 0; + if (m_currentResizeHandle == ResizeHandle::RightHandle) { + // Width was added to the right side, so center moves right. + positionDeltaX = actualWidthChange / 2.0; + } + else { // LeftHandle + // Width was added to the left side, so center moves left. + positionDeltaX = -actualWidthChange / 2.0; + } + + const double newItemScenePosX = m_initialItemScenePos.x() + positionDeltaX; + + annoFeat->MaxWidth.setValue(Rez::appX(finalVisualWidthScene)); + annoFeat->X.setValue(Rez::appX(newItemScenePosX)); + + updateView(true); + } + else { + // --- 3. Calculate Final Position based on Validated Width --- + double newItemScenePosX = 0; + if (m_currentResizeHandle == ResizeHandle::RightHandle) { + newItemScenePosX = m_initialItemScenePos.x(); + } + else { // LeftHandle + newItemScenePosX = + m_initialItemScenePos.x() + (m_initialTextWidthScene - finalVisualWidthScene); + } + + // --- 4. Commit Final, Validated State to the Feature --- + annoFeat->MaxWidth.setValue(Rez::appX(finalVisualWidthScene)); + annoFeat->X.setValue(Rez::appX(newItemScenePosX)); + } + + // The property changes will trigger QGIRichAnno::updateView, which handles the visual + // update. + + QTimer::singleShot(0, this, [this]() { + if (this && scene()) { + Q_EMIT positionChanged(scenePos()); + } + }); + + event->accept(); + } + else { + QGIView::mouseMoveEvent(event); + } +} + +void QGIRichAnno::mouseReleaseEvent(QGraphicsSceneMouseEvent* event) +{ + if (m_isResizing) { + if (m_transactionOpen) { + // Only commit if actual dragging (and thus property changes) occurred. + // m_isDraggingMidResize flag indicates if mouseMoveEvent was processed. + if (!Gui::Control().activeDialog()) { + Gui::Command::commitCommand(); + } + m_transactionOpen = false; + widthChanged(); + } + m_isResizing = false; + m_isDraggingMidResize = false; + m_currentResizeHandle = ResizeHandle::NoHandle; + setCursor(Qt::ArrowCursor); // Reset cursor + + // Ensure focus returns to the text item after the resize handle is released + refocusAnnotation(); + + Q_EMIT positionChanged(scenePos()); + + if (!isUnderMouse()) { + QGraphicsSceneHoverEvent leaveEvent(QEvent::GraphicsSceneHoverLeave); + hoverLeaveEvent(&leaveEvent); // Manually trigger leave event + } + + event->accept(); + } + else { + QGIView::mouseReleaseEvent(event); + } +} + +void QGIRichAnno::mouseDoubleClickEvent(QGraphicsSceneMouseEvent* event) { + Q_UNUSED(event); + + // If resizing was in progress, cancel it to avoid conflict with dialog + if (m_isResizing) { + if (m_transactionOpen) { + // To avoid partial changes, might need to revert or just abort. + // For simplicity, we commit if open. A better way would be to store original values and revert. + if (!Gui::Control().activeDialog()) { + Gui::Command::commitCommand(); + } + m_transactionOpen = false; + } + m_isResizing = false; + m_isDraggingMidResize = false; + m_currentResizeHandle = ResizeHandle::NoHandle; + setCursor(Qt::ArrowCursor); + } + + auto vp = static_cast(getViewProvider(getViewObject())); + if (!vp) { + return; + } + vp->doubleClicked(); +} + +void QGIRichAnno::setEditMode(bool enable) +{ + m_isEditing = enable; + if (enable) { + m_text->setTextInteractionFlags(Qt::TextEditorInteraction); + + QTextCursor cursor = m_text->textCursor(); + + // Check if the document is empty. If so, create a default format. + if (m_text->document()->isEmpty()) { + // Document is empty, so we need to create a default style from scratch. + // Let's use the default label font from preferences. + QFont font = PreferencesGui::labelFontQFont(); + + QTextCharFormat defaultFormat; + defaultFormat.setFontPointSize(font.pointSizeF()); + cursor.setCharFormat(defaultFormat); + } + else { + // Document has content. Let's use the format of the first character + // as the default for any new text. + cursor.setPosition(0); + QTextCharFormat formatAtStart = cursor.charFormat(); + + // Move the cursor back to its original position (or end of document) + cursor.movePosition(QTextCursor::End); + + // Apply the format from the start of the document to the current cursor position. + // This sets the "default" format for subsequent typing. + cursor.setCharFormat(formatAtStart); + } + + // IMPORTANT: Apply the modified cursor back to the text item. + m_text->setTextCursor(cursor); + + refocusAnnotation(); + + Q_EMIT positionChanged(scenePos()); + } + else { + m_text->setTextInteractionFlags(Qt::NoTextInteraction); + m_text->clearFocus(); + clearFocus(); + } + update(); +} + +void QGIRichAnno::refocusAnnotation() +{ + if (scene()) { + if (!scene()->views().isEmpty()) { + scene()->views().first()->setFocus(); + } + + scene()->setFocusItem(m_text, Qt::OtherFocusReason); + } +} + +QTextDocument* QGIRichAnno::document() const +{ + return m_text->document(); +} + +QTextCursor QGIRichAnno::textCursor() const +{ + return m_text->textCursor(); +} + +void QGIRichAnno::setTextCursor(const QTextCursor& cursor) +{ + m_text->setTextCursor(cursor); +} + +void QGIRichAnno::onContentsChanged() +{ + // Only process changes when in edit mode to avoid loops during setup + if (m_isEditing) { + // Update the feature property in real-time + getFeature()->AnnoText.setValue(m_text->toHtml().toUtf8()); + // Emit signal for the task panel + Q_EMIT textChanged(); + drawBorder(); // Make sure view frame is updated. + } +} + +QVariant QGIRichAnno::itemChange(GraphicsItemChange change, const QVariant& value) +{ + if (change == QGraphicsItem::ItemScenePositionHasChanged + && scene()) { + Q_EMIT positionChanged(scenePos()); + } + return QGIView::itemChange(change, value); +} + +void QGIRichAnno::updateLayout() +{ + TechDraw::DrawRichAnno* annoFeat = getFeature(); + if (!annoFeat || !m_text || m_textScaleFactor <= 0.0) { + return; + } + prepareGeometryChange(); + + double maxWidthDoc = annoFeat->MaxWidth.getValue(); + if (maxWidthDoc > 0.0) { + m_text->setTextWidth(Rez::guiX(maxWidthDoc) / m_textScaleFactor); + } + else { + m_text->setTextWidth(-1.0); + } + + update(); + + if (scene()) { + Q_EMIT positionChanged(scenePos()); + } + drawBorder(); +} + #include diff --git a/src/Mod/TechDraw/Gui/QGIRichAnno.h b/src/Mod/TechDraw/Gui/QGIRichAnno.h index 7cd8762a5e..c818e0078d 100644 --- a/src/Mod/TechDraw/Gui/QGIRichAnno.h +++ b/src/Mod/TechDraw/Gui/QGIRichAnno.h @@ -19,7 +19,6 @@ * Suite 330, Boston, MA 02111-1307, USA * * * ***************************************************************************/ - #pragma once #include @@ -33,10 +32,11 @@ #include "QGIUserTypes.h" -namespace TechDraw { +namespace TechDraw +{ class DrawRichAnno; class DrawLeaderLine; -} +} // namespace TechDraw namespace TechDrawGui { @@ -50,23 +50,33 @@ class QGCustomRect; //******************************************************************* -class TechDrawGuiExport QGIRichAnno : public QGIView +class TechDrawGuiExport QGIRichAnno: public QGIView { Q_OBJECT public: enum {Type = UserType::QGIRichAnno}; + enum class ResizeHandle + { + NoHandle, + LeftHandle, + RightHandle + // Future: TopHandle, BottomHandle, CornerHandles + }; + explicit QGIRichAnno(); ~QGIRichAnno() override = default; - int type() const override { return Type;} - void paint( QPainter * painter, - const QStyleOptionGraphicsItem * option, - QWidget * widget = nullptr ) override; + int type() const override + { + return Type; + } + void paint(QPainter* painter, + const QStyleOptionGraphicsItem* option, + QWidget* widget = nullptr) override; QRectF boundingRect() const override; - void drawBorder() override; void updateView(bool update = false) override; void setTextItem(); @@ -74,26 +84,74 @@ public: virtual TechDraw::DrawRichAnno* getFeature(); QPen rectPen() const; - void setExportingPdf(bool b) { m_isExportingPdf = b; } - bool getExportingPdf() const { return m_isExportingPdf; } - void setExportingSvg(bool b) { m_isExportingSvg = b; } - bool getExportingSvg() const { return m_isExportingSvg; } + void setExportingPdf(bool b) + { + m_isExportingPdf = b; + } + bool getExportingPdf() const + { + return m_isExportingPdf; + } + void setExportingSvg(bool b) + { + m_isExportingSvg = b; + } + bool getExportingSvg() const + { + return m_isExportingSvg; + } + + void setEditMode(bool enable); + QTextDocument* document() const; + QTextCursor textCursor() const; + void setTextCursor(const QTextCursor& cursor); + void updateLayout(); + + void refocusAnnotation(); + + Q_SIGNALS: + void widthChanged(); + void textChanged(); + void selectionChanged(); + void positionChanged(const QPointF& scenePos); protected: void draw() override; + void hoverMoveEvent(QGraphicsSceneHoverEvent* event) override; + void mousePressEvent(QGraphicsSceneMouseEvent* event) override; + void mouseMoveEvent(QGraphicsSceneMouseEvent* event) override; + void mouseReleaseEvent(QGraphicsSceneMouseEvent* event) override; + void setLineSpacing(int lineSpacing); QFont prefFont(void); - void mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) override; + void mouseDoubleClickEvent(QGraphicsSceneMouseEvent* event) override; - QString convertTextSizes(const QString& inHtml) const; + QVariant itemChange(GraphicsItemChange change, const QVariant& value) override; bool m_isExportingPdf; bool m_isExportingSvg; QGCustomText* m_text; - bool m_hasHover; QGCustomRect* m_rect; + // For resizing + ResizeHandle m_currentResizeHandle; + bool m_isResizing; + bool m_isDraggingMidResize; // True if mouse has moved significantly after press during resize + bool m_transactionOpen; // True if a Gui::Command transaction is open + QPointF m_dragStartMouseScenePos; + QPointF m_initialItemScenePos; // Scene pos of QGIRichAnno item (center) + double m_initialTextWidthScene; // Scene units, from MaxWidth property + + static const double HandleInteractionMargin; // Margin for grabbing handles (scene units) + static const double MinTextWidthDocument; // Minimum resizable width (document units) + + bool m_isEditing; + double m_textScaleFactor; + double m_lastGoodWidthScene; + +private Q_SLOTS: + void onContentsChanged(); }; } \ No newline at end of file diff --git a/src/Mod/TechDraw/Gui/QGIView.cpp b/src/Mod/TechDraw/Gui/QGIView.cpp index 4e2c2b5228..1b48678b7f 100644 --- a/src/Mod/TechDraw/Gui/QGIView.cpp +++ b/src/Mod/TechDraw/Gui/QGIView.cpp @@ -696,7 +696,11 @@ void QGIView::layoutDecorations(const QRectF& contentArea, constexpr double padding{10}; QRectF paddedContentArea = contentArea.adjusted(-padding, -padding, padding, padding); - double frameWidth = qMax(paddedContentArea.width(), labelRect.width()); + double frameWidth = paddedContentArea.width(); + // For standard views, expand frame to fit label. For RichAnno, keep frame tight to text. + if (type() != UserType::QGIRichAnno) { + frameWidth = qMax(frameWidth, labelRect.width()); + } double frameHeight = paddedContentArea.height(); outFrameRect = QRectF(paddedContentArea.center().x() - (frameWidth / 2), diff --git a/src/Mod/TechDraw/Gui/QGVPage.cpp b/src/Mod/TechDraw/Gui/QGVPage.cpp index 7188fd7179..2428b8aea7 100644 --- a/src/Mod/TechDraw/Gui/QGVPage.cpp +++ b/src/Mod/TechDraw/Gui/QGVPage.cpp @@ -393,6 +393,15 @@ void QGVPage::keyPressEvent(QKeyEvent* event) toolHandler->keyPressEvent(event); } else { + if (scene() && scene()->focusItem() != nullptr) { + // The event belongs to the focused item. The base QGraphicsView implementation + // will handle forwarding it correctly. + QGraphicsView::keyPressEvent(event); + + // We MUST return here to prevent the navigation style from also + // processing (and likely consuming) the event. + return; + } m_navStyle->handleKeyPressEvent(event); } if (!event->isAccepted()) { diff --git a/src/Mod/TechDraw/Gui/TaskRichAnno.cpp b/src/Mod/TechDraw/Gui/TaskRichAnno.cpp index c3ab0af3f3..aa073e129e 100644 --- a/src/Mod/TechDraw/Gui/TaskRichAnno.cpp +++ b/src/Mod/TechDraw/Gui/TaskRichAnno.cpp @@ -20,9 +20,12 @@ * * ***************************************************************************/ -# include -# include - +#include +#include +#include +#include +#include +#include #include #include @@ -30,18 +33,21 @@ #include #include #include +#include #include #include #include #include #include -#include "ui_TaskRichAnno.h" +#include "ui_TaskRichAnno.h" //This will include mrichtextedit.h if the .ui file uses MRichTextEdit #include "TaskRichAnno.h" -#include "mrichtextedit.h" +#include "MDIViewPage.h" #include "PreferencesGui.h" #include "QGIView.h" +#include "QGIRichAnno.h" #include "QGMText.h" +#include "QGVPage.h" #include "QGSPage.h" #include "Rez.h" #include "ViewProviderPage.h" @@ -61,11 +67,14 @@ TaskRichAnno::TaskRichAnno(TechDrawGui::ViewProviderRichAnno* annoVP) : m_annoFeat(nullptr), m_qgParent(nullptr), m_createMode(false), - m_inProgressLock(false), + m_placementMode(false), + m_inProgressLock(true), // Lock during setup m_btnOK(nullptr), m_btnCancel(nullptr), - m_textDialog(nullptr), - m_rte(nullptr) + m_qgiAnno(nullptr), + m_syncLock(false), + m_view(nullptr), + m_toolbar(nullptr) { //existence of annoVP is guaranteed by caller being ViewProviderRichAnno.setEdit @@ -74,6 +83,7 @@ TaskRichAnno::TaskRichAnno(TechDrawGui::ViewProviderRichAnno* annoVP) : m_basePage = m_annoFeat->findParentPage(); if (!m_basePage) { Base::Console().error("TaskRichAnno - bad parameters (2). Cannot proceed.\n"); + m_inProgressLock = false; return; } @@ -88,23 +98,25 @@ TaskRichAnno::TaskRichAnno(TechDrawGui::ViewProviderRichAnno* annoVP) : Gui::Document* activeGui = Gui::Application::Instance->getDocument(m_basePage->getDocument()); Gui::ViewProvider* vp = activeGui->getViewProvider(m_basePage); m_vpp = static_cast(vp); + m_view = m_vpp->getMDIViewPage(); m_qgParent = nullptr; if (m_baseFeat) { m_qgParent = m_vpp->getQGSPage()->findQViewForDocObj(m_baseFeat); } + QGVPage* graphicsView = nullptr; + graphicsView = m_vpp->getQGVPage(); + m_toolbar = new MRichTextEdit(graphicsView->viewport()); + + Gui::Command::openCommand(QT_TRANSLATE_NOOP("Command", "Edit Annotation")); + ui->setupUi(this); m_title = QObject::tr("Rich Text Editor"); setUiEdit(); - m_attachPoint = Rez::guiX(Base::Vector3d(m_annoFeat->X.getValue(), - -m_annoFeat->Y.getValue(), - 0.0)); - - connect(ui->pbEditor, &QPushButton::clicked, - this, &TaskRichAnno::onEditorClicked); + finishSetup(); } //ctor for creation @@ -117,87 +129,285 @@ TaskRichAnno::TaskRichAnno(TechDraw::DrawView* baseFeat, m_annoFeat(nullptr), m_qgParent(nullptr), m_createMode(true), - m_inProgressLock(false), + m_placementMode(true), + m_inProgressLock(true), // Lock during setup m_btnOK(nullptr), m_btnCancel(nullptr), - m_textDialog(nullptr), - m_rte(nullptr) + m_qgiAnno(nullptr), + m_syncLock(false), + m_view(nullptr), + m_toolbar(nullptr) { //existence of baseFeat and page guaranteed by CmdTechDrawRichTextAnnotation (CommandAnnotate.cpp) Gui::Document* activeGui = Gui::Application::Instance->getDocument(m_basePage->getDocument()); Gui::ViewProvider* vp = activeGui->getViewProvider(m_basePage); m_vpp = static_cast(vp); + m_view = m_vpp->getMDIViewPage(); m_qgParent = nullptr; if (m_vpp->getQGSPage()) { m_qgParent = m_vpp->getQGSPage()->findQViewForDocObj(baseFeat); } + Gui::Command::openCommand(QT_TRANSLATE_NOOP("Command", "Create Annotation")); + ui->setupUi(this); m_title = QObject::tr("Rich Text Creator"); - setUiPrimary(); + setUiPrimary(); // Sets initial UI values, might trigger signals if connected - connect(ui->pbEditor, &QPushButton::clicked, - this, &TaskRichAnno::onEditorClicked); + // Don't create the feature or the toolbar yet. Enter placement mode instead. + QGVPage* graphicsView = m_vpp->getQGVPage(); + if (graphicsView) { + // Avoid double installation + if (m_viewport) { + m_viewport->removeEventFilter(this); + } + m_viewport = graphicsView->viewport(); + m_viewport->installEventFilter(this); + } + + enterPlacementMode(); + + m_inProgressLock = false; +} + +TaskRichAnno::~TaskRichAnno() +{ + removeViewFilter(); + + if (m_toolbar) { + m_toolbar->close(); // This will delete + m_toolbar = nullptr; + } +} + +void TaskRichAnno::finishSetup() +{ + m_inProgressLock = true; // Lock during setup + + // --- Step 1: Get pointer to the QGIRichAnno object --- + if (!m_annoVP || !m_view) { + Base::Console().error( + "TaskRichAnno::finishSetup - Critical m_annoVP are missing. Aborting setup.\n"); + return; + } + m_qgiAnno = static_cast(m_annoVP->getQView()); + QGVPage* graphicsView = m_view->getViewProviderPage()->getQGVPage(); + + if (!m_qgiAnno || !graphicsView) { + Base::Console().error( + "TaskRichAnno::finishSetup - Critical m_qgiAnno is missing. Aborting setup.\n"); + return; + } + + m_toolbar->setWindowFlags(Qt::Tool | Qt::FramelessWindowHint | Qt::X11BypassWindowManagerHint); + m_toolbar->setAttribute(Qt::WA_DeleteOnClose); + m_toolbar->setProperty("floating", true); + + // --- Step 2: Perform the "hide text area" trick --- + QTextEdit* textEditChild = m_toolbar->findChild(); + if (!textEditChild) { + delete m_toolbar; + m_toolbar = nullptr; + m_inProgressLock = false; + return; + } + + textEditChild->setVisible(false); + textEditChild->setMinimumHeight(0); + textEditChild->setMaximumHeight(0); + + m_toolbar->setMinimalMode(true); + m_toolbar->adjustSize(); + m_toolbar->setFixedSize(m_toolbar->sizeHint()); + m_toolbar->show(); + + // --- Step 3: Connect signals --- + // Get the internal document of the toolbar and link it to the QGIRichAnno + textEditChild->setDocument(m_qgiAnno->document()); + + // Connect signals to keep things in sync + connect(m_qgiAnno, + &QGIRichAnno::selectionChanged, + this, + &TaskRichAnno::onViewSelectionChanged); + connect(m_qgiAnno, + &QGIRichAnno::positionChanged, + this, + &TaskRichAnno::onViewPositionChanged); + + // Also connect the width changed signal for resize handles + connect(m_qgiAnno, &QGIRichAnno::widthChanged, this, &TaskRichAnno::onViewWidthChanged); + + connect(ui->dsbMaxWidth, + qOverload(&Gui::QuantitySpinBox::valueChanged), + this, + &TaskRichAnno::onMaxWidthChanged); + connect(ui->gbFrame, &QGroupBox::toggled, this, &TaskRichAnno::onShowFrameToggled); + connect(ui->cpFrameColor, &Gui::ColorButton::changed, this, &TaskRichAnno::onFrameColorChanged); + connect(ui->dsbWidth, + qOverload(&Gui::QuantitySpinBox::valueChanged), + this, + &TaskRichAnno::onFrameWidthChanged); + connect(ui->cFrameStyle, + qOverload(&QComboBox::currentIndexChanged), + this, + &TaskRichAnno::onFrameStyleChanged); + + // Panning is detected by scroll bar value changes. + connect(graphicsView->horizontalScrollBar(), + &QScrollBar::valueChanged, + this, + &TaskRichAnno::onViewTransformed); + connect(graphicsView->verticalScrollBar(), + &QScrollBar::valueChanged, + this, + &TaskRichAnno::onViewTransformed); + + + connect(ui->gbFrame, &QGroupBox::toggled, this, &TaskRichAnno::refocusAnnotation); + connect(ui->cpFrameColor, &Gui::ColorButton::changed, this, &TaskRichAnno::refocusAnnotation); + connect(ui->cFrameStyle, + QOverload::of(&QComboBox::activated), + this, + &TaskRichAnno::refocusAnnotation); + connect(ui->dsbMaxWidth, + &Gui::QuantitySpinBox::editingFinished, + this, + &TaskRichAnno::refocusAnnotation); + connect(ui->dsbWidth, + &Gui::QuantitySpinBox::editingFinished, + this, + &TaskRichAnno::refocusAnnotation); + + onViewSelectionChanged(); // Sync initial cursor to hidden editor + m_qgiAnno->setEditMode(true); + + if (graphicsView) { + // Install the event filter on the viewport, which receives the mouse events. + if (m_viewport) { + m_viewport->removeEventFilter(this); + } + m_viewport = graphicsView->viewport(); + m_viewport->installEventFilter(this); + } + + QTimer::singleShot(0, m_qgiAnno, &QGIRichAnno::updateLayout); + + m_inProgressLock = false; +} + +void TaskRichAnno::onViewTransformed() +{ + // When the view pans, the item's scene position hasn't changed. + // We just need to re-run the position calculation with its current scenePos. + if (m_qgiAnno) { + onViewPositionChanged(m_qgiAnno->scenePos()); + } +} + +void TaskRichAnno::onViewSelectionChanged() +{ + if (m_syncLock) { + return; + } + + // When the selection in the view changes, we need to update the + // toolbar's internal state so the buttons (Bold, etc.) reflect the selection. + if (m_toolbar && m_qgiAnno) { + QTextEdit* textEditChild = m_toolbar->findChild(); + if (textEditChild) { + m_syncLock = true; + textEditChild->setTextCursor(m_qgiAnno->textCursor()); + m_syncLock = false; + } + } + +} + +void TaskRichAnno::onViewPositionChanged(const QPointF& scenePos) +{ + // Make sure you have a local variable for the QGVPage to make the code cleaner + QGVPage* graphicsView = nullptr; + if (m_view) { + graphicsView = m_view->getViewProviderPage()->getQGVPage(); + } + + if (m_toolbar && graphicsView && m_qgiAnno) { + // Get the item's bounding rectangle in Scene coordinates + QRectF itemRect = m_qgiAnno->mapToScene(m_qgiAnno->boundingRect()).boundingRect(); + + // Calculate the top-center point of the item in Scene coordinates + QPointF topCenterScenePos(itemRect.center().x(), itemRect.top()); + + // Map from scene using the correct QGVPage object + QPoint viewPos = graphicsView->mapFromScene(topCenterScenePos); + + // Map the QGraphicsView point to global screen coordinates + QPoint globalPos = graphicsView->mapToGlobal(viewPos); + + // Position the toolbar above this point, centered horizontally + int yOffset = 10; + QPoint toolbarPos(globalPos.x() - m_toolbar->width() / 2, + globalPos.y() - m_toolbar->height() - yOffset); + + m_toolbar->move(toolbarPos); + + // Ensure the toolbar is raised to the top + m_toolbar->raise(); + } } void TaskRichAnno::updateTask() { // blockUpdate = true; - -// blockUpdate = false; } void TaskRichAnno::changeEvent(QEvent *event) { if (event->type() == QEvent::LanguageChange) { ui->retranslateUi(this); + // Update titles if they are translatable and set directly + if (m_createMode) { + m_title = QObject::tr("Rich text creator"); + } else { + m_title = QObject::tr("Rich text editor"); + } + if (m_annoFeat || m_createMode) { // Only set window title if UI is relevant + setWindowTitle(m_title); + } } + QWidget::changeEvent(event); } void TaskRichAnno::setUiPrimary() { -// Base::Console().message("TRA::setUiPrimary()\n"); - enableVPUi(false); setWindowTitle(m_title); if (m_baseFeat) { std::string baseName = m_baseFeat->getNameInDocument(); ui->leBaseView->setText(QString::fromStdString(baseName)); } + ui->dsbMaxWidth->setUnit(Base::Unit::Length); + ui->dsbMaxWidth->setValue(-1.0); + ui->dsbWidth->setUnit(Base::Unit::Length); ui->dsbWidth->setMinimum(0); ui->dsbWidth->setValue(prefWeight()); + ui->cpFrameColor->setColor(prefLineColor().asValue()); - // set a default font size, use for this the preferences setting - MRichTextEdit mre; - ui->teAnnoText->setFontPointSize(mre.getDefFontSizeNum()); - // set a placeholder text to inform the user - ui->teAnnoText->setPlaceholderText(tr("Input the annotation text directly or start the rich text editor")); + ui->gbFrame->setChecked(false); } void TaskRichAnno::enableTextUi(bool enable) { - ui->pbEditor->setEnabled(enable); - ui->teAnnoText->setEnabled(enable); -} - -//switch widgets related to ViewProvider on/off -//there is no ViewProvider until some time after feature is created. -void TaskRichAnno::enableVPUi(bool enable) -{ - Q_UNUSED(enable); -// ui->cpLineColor->setEnabled(b); -// ui->dsbWeight->setEnabled(b); -// ui->cboxStyle->setEnabled(b); + m_toolbar->setEnabled(enable); } void TaskRichAnno::setUiEdit() { -// Base::Console().message("TRA::setUiEdit()); - enableVPUi(true); setWindowTitle(m_title); enableTextUi(true); @@ -208,9 +418,9 @@ void TaskRichAnno::setUiEdit() baseName = docObj->getNameInDocument(); } ui->leBaseView->setText(QString::fromStdString(baseName)); - ui->teAnnoText->setHtml(QString::fromUtf8(m_annoFeat->AnnoText.getValue())); + m_toolbar->setText(QString::fromUtf8(m_annoFeat->AnnoText.getValue())); ui->dsbMaxWidth->setValue(m_annoFeat->MaxWidth.getValue()); - ui->cbShowFrame->setChecked(m_annoFeat->ShowFrame.getValue()); + ui->gbFrame->setChecked(m_annoFeat->ShowFrame.getValue()); } if (m_annoVP) { @@ -220,46 +430,73 @@ void TaskRichAnno::setUiEdit() } } -void TaskRichAnno::onEditorClicked(bool clicked) + +void TaskRichAnno::onMaxWidthChanged(double value) { -// Base::Console().message("TL::onEditorClicked(%d)\n", b); - Q_UNUSED(clicked); - m_textDialog = new QDialog(nullptr); - QString leadText = ui->teAnnoText->toHtml(); - QString plainText = ui->teAnnoText->toPlainText(); - if (plainText.isEmpty()) { - m_rte = new MRichTextEdit(m_textDialog); - } else { - m_rte = new MRichTextEdit(m_textDialog, leadText); + createAnnoIfNotAlready(); + + if (m_inProgressLock || !m_annoFeat) return; + m_annoFeat->MaxWidth.setValue(value); + m_annoFeat->requestPaint(); +} + +void TaskRichAnno::onViewWidthChanged() +{ + ui->dsbMaxWidth->blockSignals(true); + ui->dsbMaxWidth->setValue(m_annoFeat->MaxWidth.getValue()); + ui->dsbMaxWidth->blockSignals(false); +} + +void TaskRichAnno::onShowFrameToggled(bool checked) +{ + createAnnoIfNotAlready(); + + if (m_inProgressLock || !m_annoFeat) return; + m_annoFeat->ShowFrame.setValue(checked); + // Update VP editable status based on ShowFrame + if (m_annoVP) { + bool editable = checked; + m_annoVP->LineWidth.setStatus(App::Property::ReadOnly, !editable); + m_annoVP->LineStyle.setStatus(App::Property::ReadOnly, !editable); + m_annoVP->LineColor.setStatus(App::Property::ReadOnly, !editable); } - QGridLayout* gl = new QGridLayout(m_textDialog); - gl->addWidget(m_rte, 0,0, 1,1); - m_textDialog->setWindowTitle(QObject::tr("Rich text editor")); - m_textDialog->setMinimumWidth (400); - m_textDialog->setMinimumHeight(400); - - connect(m_rte, &MRichTextEdit::saveText, - this, &TaskRichAnno::onSaveAndExit); - connect(m_rte, &MRichTextEdit::editorFinished, - this, &TaskRichAnno::onEditorExit); - - m_textDialog->show(); + m_annoFeat->requestPaint(); } -void TaskRichAnno::onSaveAndExit(QString qs) +void TaskRichAnno::onFrameColorChanged() { - ui->teAnnoText->setHtml(qs); - //dialog clean up should be handled by accept() call in dialog - m_textDialog->accept(); - m_textDialog = nullptr; - m_rte = nullptr; + createAnnoIfNotAlready(); + + if (m_inProgressLock || !m_annoVP) return; + Base::Color ac; + ac.setValue(ui->cpFrameColor->color()); + m_annoVP->LineColor.setValue(ac); + if (m_annoFeat) m_annoFeat->requestPaint(); } -void TaskRichAnno::onEditorExit() +void TaskRichAnno::onFrameWidthChanged(double value) { - m_textDialog->reject(); - m_textDialog = nullptr; - m_rte = nullptr; + createAnnoIfNotAlready(); + + if (m_inProgressLock || !m_annoVP) return; + m_annoVP->LineWidth.setValue(value); + if (m_annoFeat) m_annoFeat->requestPaint(); +} + +void TaskRichAnno::onFrameStyleChanged(int index) +{ + createAnnoIfNotAlready(); + + if (m_inProgressLock || !m_annoVP) return; + m_annoVP->LineStyle.setValue(index); + if (m_annoFeat) m_annoFeat->requestPaint(); +} + +void TaskRichAnno::createAnnoIfNotAlready() +{ + if (m_createMode && m_placementMode) { + createAndSetupAnnotation(nullptr); + } } double TaskRichAnno::prefWeight() const @@ -272,9 +509,152 @@ Base::Color TaskRichAnno::prefLineColor() return PreferencesGui::leaderColor(); } +void TaskRichAnno::refocusAnnotation() +{ + // Use a zero-delay timer to schedule the focus change. + // This allows the current widget interaction (e.g., the checkbox toggling) + // to complete fully before we shift focus. + QTimer::singleShot(0, [this]() { + if (m_qgiAnno) { + m_qgiAnno->refocusAnnotation(); + } + }); +} + +void TaskRichAnno::focusOutEvent(QFocusEvent* event) +{ + // Let the base class do its thing first + QWidget::focusOutEvent(event); + + // If the focus is leaving our task panel for something else, + // ensure our annotation is the active element. + refocusAnnotation(); +} + +bool TaskRichAnno::eventFilter(QObject* watched, QEvent* event) +{ + if (!m_view || !m_view->getViewProviderPage() || !m_viewport) { + return QWidget::eventFilter(watched, event); + } + + QGVPage* graphicsView = m_view->getViewProviderPage()->getQGVPage(); + if (!graphicsView) { + return QWidget::eventFilter(watched, event); + } + + if (watched == m_viewport) { + if (event->type() == QEvent::Enter) { + if (!m_placementMode && m_qgiAnno) { + refocusAnnotation(); + } + } + + if (event->type() == QEvent::MouseButtonPress) { + if (m_createMode && m_placementMode) { + auto mouseEvent = static_cast(event); + QPointF scenePos = graphicsView->mapToScene(mouseEvent->pos()); + createAndSetupAnnotation(&scenePos); + return QWidget::eventFilter(watched, event); + } + + // Cast the event to get the mouse position + auto mouseEvent = static_cast(event); + QGraphicsItem* item = graphicsView->itemAt(mouseEvent->pos()); + + // Walk up the parent chain to see if the click was on our annotation or one of its + // children. + QGIRichAnno* clickedAnno = nullptr; + while (item) { + clickedAnno = dynamic_cast(item); + if (clickedAnno) { + break; // Found an annotation + } + item = item->parentItem(); // Check the parent + } + + // If we didn't find our specific annotation, the click was "outside". + if (clickedAnno != m_qgiAnno) { + // Simulate clicking the "OK" button to accept the changes. + if (m_btnOK && m_btnOK->isEnabled()) { + m_btnOK->click(); + return true; // We've handled this event, so stop further processing. + } + } + else if (!m_placementMode && m_qgiAnno) { + refocusAnnotation(); + } + } + } + + // For all other events, pass them on to the default handler. + return QWidget::eventFilter(watched, event); +} + +void TaskRichAnno::removeViewFilter() +{ + if (m_viewport) { + m_viewport->removeEventFilter(this); + m_viewport = nullptr; + } +} //****************************************************************************** -void TaskRichAnno::createAnnoFeature() +void TaskRichAnno::enterPlacementMode() +{ + if (m_view) { + if (auto* gv = m_view->getViewProviderPage()->getQGVPage()) { + gv->viewport()->setCursor(Qt::CrossCursor); + } + } + // Disable UI elements that require an annotation to exist + ui->gbFrame->setEnabled(false); + ui->dsbMaxWidth->setEnabled(false); + setFocus(); // Set focus to the panel to capture key presses +} + +void TaskRichAnno::createAndSetupAnnotation(const QPointF* scenePos) +{ + if (!m_placementMode) { + return; // Already created + } + m_inProgressLock = true; + m_placementMode = false; + + // Restore cursor + if (m_view) { + if (auto* gv = m_view->getViewProviderPage()->getQGVPage()) { + gv->viewport()->setCursor(Qt::ArrowCursor); + } + } + + // Now that the feature exists, create the toolbar and finish setup + QGVPage* graphicsView = m_vpp->getQGVPage(); + m_toolbar = new MRichTextEdit(graphicsView->viewport()); + + createAnnoFeature(scenePos); // Create the feature at the specified position + + if (!m_annoFeat) { // Safety check if creation failed + Base::Console().error("TaskRichAnno - Failed to create annotation feature.\n"); + m_inProgressLock = false; + reject(); // Abort the task + return; + } + + finishSetup(); // This will connect all signals and show the toolbar + + // Re-enable the UI + ui->gbFrame->setEnabled(true); + ui->dsbMaxWidth->setEnabled(true); + + // Select the new annotation object so that the frame is shown + Gui::Selection().addSelection(m_annoFeat->getDocument()->getName(), m_annoFeat->getNameInDocument()); + + refocusAnnotation(); // Give focus to the new annotation + + m_inProgressLock = false; +} + +void TaskRichAnno::createAnnoFeature(const QPointF* scenePos) { // Base::Console().message("TRA::createAnnoFeature()"); const std::string objectName{QT_TR_NOOP("RichTextAnnotation")}; @@ -284,7 +664,6 @@ void TaskRichAnno::createAnnoFeature() std::string PageName = m_basePage->getNameInDocument(); - Gui::Command::openCommand(QT_TRANSLATE_NOOP("Command", "Create Anno")); Command::doCommand(Command::Doc, "App.activeDocument().addObject('%s', '%s')", annoType.c_str(), annoName.c_str()); Command::doCommand(Command::Doc, "App.activeDocument().%s.addView(App.activeDocument().%s)", @@ -296,12 +675,18 @@ void TaskRichAnno::createAnnoFeature() } App::DocumentObject* obj = m_basePage->getDocument()->getObject(annoName.c_str()); if (!obj) { + Gui::Command::abortCommand(); throw Base::RuntimeError("TaskRichAnno - new RichAnno object not found"); } if (obj->isDerivedFrom()) { m_annoFeat = static_cast(obj); - commonFeatureUpdate(); - if (m_baseFeat) { + commonFeatureUpdate(); // Set text, MaxWidth, ShowFrame from UI + if (scenePos) { + // New: Use the clicked position + m_annoFeat->X.setValue(Rez::appX(scenePos->x())); + m_annoFeat->Y.setValue(-Rez::appX(scenePos->y())); + } + else if (m_baseFeat) { QPointF qTemp = calcTextStartPos(m_annoFeat->getScale()); Base::Vector3d vTemp(qTemp.x(), qTemp.y()); m_annoFeat->X.setValue(Rez::appX(vTemp.x)); @@ -315,22 +700,25 @@ void TaskRichAnno::createAnnoFeature() if (m_annoFeat) { Gui::ViewProvider* vp = QGIView::getViewProvider(m_annoFeat); - auto annoVP = freecad_cast(vp); - if (annoVP) { + m_annoVP = freecad_cast(vp); // Store m_annoVP + if (m_annoVP) { Base::Color ac; ac.setValue(ui->cpFrameColor->color()); - annoVP->LineColor.setValue(ac); - annoVP->LineWidth.setValue(ui->dsbWidth->rawValue()); - annoVP->LineStyle.setValue(ui->cFrameStyle->currentIndex()); + m_annoVP->LineColor.setValue(ac); + m_annoVP->LineWidth.setValue(ui->dsbWidth->rawValue()); + m_annoVP->LineStyle.setValue(ui->cFrameStyle->currentIndex()); + + // Set initial VP property readonly state based on ShowFrame + bool editable = m_annoFeat->ShowFrame.getValue(); + m_annoVP->LineWidth.setStatus(App::Property::ReadOnly, !editable); + m_annoVP->LineStyle.setStatus(App::Property::ReadOnly, !editable); + m_annoVP->LineColor.setStatus(App::Property::ReadOnly, !editable); } } std::string translatedObjectName{tr(objectName.c_str()).toStdString()}; obj->Label.setValue(translatedObjectName + generatedSuffix); - Gui::Command::commitCommand(); - Gui::Command::updateActive(); - //trigger claimChildren in tree if (m_baseFeat) { m_baseFeat->touch(); @@ -343,55 +731,14 @@ void TaskRichAnno::createAnnoFeature() } } -void TaskRichAnno::updateAnnoFeature() -{ -// Base::Console().message("TRA::updateAnnoFeature()\n"); - Gui::Command::openCommand(QT_TRANSLATE_NOOP("Command", "Edit Anno")); - commonFeatureUpdate(); - Base::Color ac; - ac.setValue(ui->cpFrameColor->color()); - m_annoVP->LineColor.setValue(ac); - m_annoVP->LineWidth.setValue(ui->dsbWidth->rawValue()); - m_annoVP->LineStyle.setValue(ui->cFrameStyle->currentIndex()); - - Gui::Command::commitCommand(); - Gui::Command::updateActive(); -} - void TaskRichAnno::commonFeatureUpdate() { // Base::Console().message("TRA::commonFeatureUpdate()\n"); - m_annoFeat->setPosition(Rez::appX(m_attachPoint.x), Rez::appX(- m_attachPoint.y), true); - m_annoFeat->AnnoText.setValue(ui->teAnnoText->toHtml().toUtf8()); + if (!m_annoFeat) return; + + m_annoFeat->AnnoText.setValue(m_toolbar->toHtml().toUtf8()); m_annoFeat->MaxWidth.setValue(ui->dsbMaxWidth->value().getValue()); - m_annoFeat->ShowFrame.setValue(ui->cbShowFrame->isChecked()); -} - -void TaskRichAnno::removeFeature() -{ -// Base::Console().message("TRA::removeFeature()\n"); - if (!m_annoFeat) - return; - - if (m_createMode) { - try { - // this doesn't remove the QGMText item?? - std::string PageName = m_basePage->getNameInDocument(); - Gui::Command::doCommand(Gui::Command::Gui, "App.activeDocument().%s.removeView(App.activeDocument().%s)", - PageName.c_str(), m_annoFeat->getNameInDocument()); - Gui::Command::doCommand(Gui::Command::Gui, "App.activeDocument().removeObject('%s')", - m_annoFeat->getNameInDocument()); - } - catch (...) { - Base::Console().warning("TRA::removeFeature - failed to delete feature\n"); - return; - } - } else { - if (Gui::Command::hasPendingCommand()) { - std::vector undos = Gui::Application::Instance->activeDocument()->getUndoVector(); - Gui::Application::Instance->activeDocument()->undo(1); - } - } + m_annoFeat->ShowFrame.setValue(ui->gbFrame->isChecked()); } //we don't know the bounding rect of the text, so we have to calculate a reasonable @@ -400,51 +747,63 @@ QPointF TaskRichAnno::calcTextStartPos(double scale) { Q_UNUSED(scale) // Base::Console().message("TRA::calcTextStartPos(%.3f)\n", scale); - double textWidth = 100.0; - double textHeight = 20.0; - double horizGap(20.0); - double tPosX(0.0); - double tPosY(0.0); + double textWidth = 100.0; // Default guess for text width in document units + double textHeight = 20.0; // Default guess for text height + double horizGap(Rez::appX(5.0)); // 5mm gap from leader end point in document units - double width = m_annoFeat->MaxWidth.getValue(); - if (width > 0 ) { - textWidth = width; + double annoMaxWidth = ui->dsbMaxWidth->value().getValue(); // MaxWidth from UI + if (annoMaxWidth > 0 ) { + textWidth = annoMaxWidth; // Use user-defined MaxWidth if available } + // Note: textHeight is harder to guess accurately without rendering. - std::vector points; if (m_baseFeat) { if (m_baseFeat->isDerivedFrom()) { TechDraw::DrawLeaderLine* dll = static_cast(m_baseFeat); - points = dll->WayPoints.getValues(); - } else { -// Base::Console().message("TRA::calcTextPos - m_baseFeat is not Leader\n"); - return QPointF(0.0, 0.0); - } - } else { -// Base::Console().message("TRA::calcStartPos - no m_baseFeat\n"); - if (m_basePage) { - double w = Rez::guiX(m_basePage->getPageWidth() / 2.0); - double h = Rez::guiX(m_basePage->getPageHeight() / 2.0); - return QPointF(w, h); - } else { - Base::Console().message("TRA::calcStartPos - no m_basePage\n"); //shouldn't happen. caught elsewhere + const auto& wayPoints = dll->WayPoints.getValues(); + if (!wayPoints.empty()) { + Base::Vector3d leaderEndDoc = wayPoints.back(); // Last point of leader in document units + Base::Vector3d leaderPrevDoc = (wayPoints.size() > 1) ? wayPoints[wayPoints.size()-2] : leaderEndDoc; + + // Position relative to leader end point (these are document units) + double tPosX_doc, tPosY_doc; + + // Heuristic: place annotation to the right or left of the leader's last segment + // Anchor point of RichAnno is its center. We want text block (top-left) relative to leader. + // This calculation is for the *center* of the RichAnno. + if (leaderEndDoc.x < leaderPrevDoc.x) { // Leader pointing left-ish + tPosX_doc = leaderEndDoc.x - horizGap - (textWidth / 2.0); + } else { // Leader pointing right-ish or vertical + tPosX_doc = leaderEndDoc.x + horizGap + (textWidth / 2.0); + } + // Vertically, align center of text block slightly below leader end for now + tPosY_doc = leaderEndDoc.y - (textHeight / 2.0) ; + + return QPointF(Rez::guiX(tPosX_doc), Rez::guiX(tPosY_doc)); // Convert to GUI coords for return + } } + // If baseFeat is not a leader or has no points, fall through to page center or view center. + // For a generic DrawView, position near its center. + double baseX_doc = m_baseFeat->X.getValue(); + double baseY_doc = m_baseFeat->Y.getValue(); + + // Position annotation center to the right of baseX, + // and vertically such that top of annotation is near baseY. + double tPosX_doc = baseX_doc + (textWidth / 2.0) + horizGap; + // RichAnno Y is its center. baseY_doc is top of view. + // So, center of anno = baseY_doc (top of view) + half of anno's estimated height. + double tPosY_doc = baseY_doc + (textHeight / 2.0); + return QPointF(Rez::guiX(tPosX_doc), Rez::guiX(tPosY_doc)); // Convert to GUI coords for } - if (!points.empty()) { - QPointF lastPoint(points.back().x, points.back().y); - QPointF firstPoint(points.front().x, points.front().y); - QPointF lastOffset = lastPoint - firstPoint; - - if (lastPoint.x() < firstPoint.x()) { //last is left of first - tPosX = lastOffset.x() - horizGap - textWidth; //left of last - tPosY = lastOffset.y() - textHeight; - } else { //last is right of first - tPosX = lastOffset.x() + horizGap; //right of last - tPosY = lastOffset.y() - textHeight; - } + // Default to page center if no baseFeat + if (m_basePage) { + double w_doc = m_basePage->getPageWidth() / 2.0; + double h_doc = m_basePage->getPageHeight() / 2.0; + return QPointF(Rez::guiX(w_doc), Rez::guiX(h_doc)); } - return QPointF(tPosX, -tPosY); + + return QPointF(0.0, 0.0); // Absolute fallback } void TaskRichAnno::saveButtons(QPushButton* btnOK, @@ -456,61 +815,52 @@ void TaskRichAnno::saveButtons(QPushButton* btnOK, void TaskRichAnno::enableTaskButtons(bool enable) { - m_btnOK->setEnabled(enable); - m_btnCancel->setEnabled(enable); + if (m_btnOK) m_btnOK->setEnabled(enable); + if (m_btnCancel) m_btnCancel->setEnabled(enable); } //****************************************************************************** bool TaskRichAnno::accept() { -// Base::Console().message("TRA::accept()\n"); - if (m_inProgressLock) { -// Base::Console().message("TRA::accept - edit in progress!!\n"); - //TODO: kill MRTE dialog? - return true; - } - - Gui::Document* doc = Gui::Application::Instance->getDocument(m_basePage->getDocument()); - if (!doc) + if (m_inProgressLock || !m_annoFeat) { // Should not happen if UI is responsive return false; - - if (getCreateMode()) { - createAnnoFeature(); - } else { - updateAnnoFeature(); } - m_annoFeat->requestPaint(); + removeViewFilter(); + if (m_qgiAnno) { + m_qgiAnno->setEditMode(false); + } + + Gui::Command::commitCommand(); Gui::Command::doCommand(Gui::Command::Gui, "Gui.ActiveDocument.resetEdit()"); + m_annoFeat->getDocument()->recompute(); + return true; } bool TaskRichAnno::reject() { -// Base::Console().message("TRA::reject()\n"); if (m_inProgressLock) { -// Base::Console().message("TRA::reject - edit in progress!!\n"); return false; } - if (m_basePage) { - Gui::Document* doc = Gui::Application::Instance->getDocument(m_basePage->getDocument()); - if (!doc) { - return false; - } - if (getCreateMode() && m_annoFeat) { - removeFeature(); - } + if (m_qgiAnno) { + m_qgiAnno->setEditMode(false); } + + removeViewFilter(); - //make sure any dangling objects are cleaned up - Gui::Command::doCommand(Gui::Command::Gui, "App.activeDocument().recompute()"); + Gui::Command::abortCommand(); Gui::Command::doCommand(Gui::Command::Gui, "Gui.ActiveDocument.resetEdit()"); - return false; + if (!m_createMode) { // Feature gone and m_annoFeat dangling if we are creating! + m_annoFeat->getDocument()->recompute(); + } + + return true; } ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -519,31 +869,32 @@ TaskDlgRichAnno::TaskDlgRichAnno(TechDraw::DrawView* baseFeat, : TaskDialog() { widget = new TaskRichAnno(baseFeat, page); - taskbox = new Gui::TaskView::TaskBox(Gui::BitmapFactory().pixmap("actions/TechDraw_RichTextAnnotation"), + taskbox = new Gui::TaskView::TaskBox(Gui::BitmapFactory().pixmap("actions/TechDraw_Annotation"), widget->windowTitle(), true, nullptr); taskbox->groupLayout()->addWidget(widget); Content.push_back(taskbox); + setAutoCloseOnTransactionChange(true); + if (page) { + setDocumentName(page->getDocument()->getFullName()); + } } TaskDlgRichAnno::TaskDlgRichAnno(TechDrawGui::ViewProviderRichAnno* annoVP) : TaskDialog() { widget = new TaskRichAnno(annoVP); - taskbox = new Gui::TaskView::TaskBox(Gui::BitmapFactory().pixmap("actions/TechDraw_RichTextAnnotation"), + taskbox = new Gui::TaskView::TaskBox(Gui::BitmapFactory().pixmap("actions/TechDraw_Annotation"), widget->windowTitle(), true, nullptr); taskbox->groupLayout()->addWidget(widget); Content.push_back(taskbox); + setAutoCloseOnTransactionChange(true); + setDocumentName(annoVP->getDocument()->getDocument()->getFullName()); } TaskDlgRichAnno::~TaskDlgRichAnno() { } -void TaskDlgRichAnno::update() -{ -// widget->updateTask(); -} - void TaskDlgRichAnno::modifyStandardButtons(QDialogButtonBox* box) { QPushButton* btnOK = box->button(QDialogButtonBox::Ok); @@ -552,24 +903,20 @@ void TaskDlgRichAnno::modifyStandardButtons(QDialogButtonBox* box) } //==== calls from the TaskView =============================================================== -void TaskDlgRichAnno::open() -{ -} - -void TaskDlgRichAnno::clicked(int) -{ -} bool TaskDlgRichAnno::accept() { - widget->accept(); - return true; + return widget->accept(); // Delegate to the widget's accept logic } bool TaskDlgRichAnno::reject() { - widget->reject(); - return true; + return widget->reject(); // Delegate to the widget's reject logic +} + +void TaskDlgRichAnno::autoClosedOnTransactionChange() +{ + reject(); } #include diff --git a/src/Mod/TechDraw/Gui/TaskRichAnno.h b/src/Mod/TechDraw/Gui/TaskRichAnno.h index ff1eadf0f7..c80544fae6 100644 --- a/src/Mod/TechDraw/Gui/TaskRichAnno.h +++ b/src/Mod/TechDraw/Gui/TaskRichAnno.h @@ -26,9 +26,8 @@ #include #include #include - - -class MRichTextEdit; +#include "mrichtextedit.h" +#include "MDIViewPage.h" class Ui_TaskRichAnno; @@ -43,9 +42,9 @@ namespace TechDrawGui { class QGIView; class QGIPrimPath; +class QGIRichAnno; class MDIViewPage; class QGMText; -class QGIRichAnno; class ViewProviderRichAnno; class ViewProviderPage; @@ -54,45 +53,60 @@ class TaskRichAnno : public QWidget Q_OBJECT public: - TaskRichAnno(TechDraw::DrawView* baseFeat, - TechDraw::DrawPage* page); + TaskRichAnno(TechDraw::DrawView* baseFeat, TechDraw::DrawPage* page); explicit TaskRichAnno(TechDrawGui::ViewProviderRichAnno* annoVP); - ~TaskRichAnno() override = default; + ~TaskRichAnno() override; + + void finishSetup(); virtual bool accept(); virtual bool reject(); virtual void setCreateMode(bool mode) { m_createMode = mode; } virtual bool getCreateMode() { return m_createMode; } void updateTask(); - void saveButtons(QPushButton* btnOK, - QPushButton* btnCancel); + void saveButtons(QPushButton* btnOK, QPushButton* btnCancel); void enableTaskButtons(bool enable); -public Q_SLOTS: - void onEditorClicked(bool clicked); + ViewProviderRichAnno* getAnnoVP() const + { + return m_annoVP; + } protected: - void changeEvent(QEvent *event) override; + void changeEvent(QEvent* event) override; + void focusOutEvent(QFocusEvent* event) override; + bool eventFilter(QObject* watched, QEvent* event) override; - void createAnnoFeature(); - void updateAnnoFeature(); + void createAnnoFeature(const QPointF* scenePos = nullptr); void commonFeatureUpdate(); - void removeFeature(); QPointF calcTextStartPos(double scale); void setUiPrimary(); void setUiEdit(); void enableTextUi(bool enable); - void enableVPUi(bool enable); double prefWeight() const; Base::Color prefLineColor(); protected Q_SLOTS: - void onSaveAndExit(QString); - void onEditorExit(); + void onMaxWidthChanged(double value); + void onViewWidthChanged(); + void onShowFrameToggled(bool checked); + void onFrameColorChanged(); + void onFrameWidthChanged(double value); + void onFrameStyleChanged(int index); + void onViewTransformed(); + void refocusAnnotation(); + + void onViewSelectionChanged(); + void onViewPositionChanged(const QPointF& scenePos); private: + void removeViewFilter(); + void enterPlacementMode(); + void createAndSetupAnnotation(const QPointF* scenePos = nullptr); + void createAnnoIfNotAlready(); + std::unique_ptr ui; ViewProviderPage* m_vpp; @@ -103,18 +117,22 @@ private: QGIView* m_qgParent; std::string m_qgParentName; - Base::Vector3d m_attachPoint; - bool m_createMode; + bool m_placementMode; bool m_inProgressLock; QPushButton* m_btnOK; QPushButton* m_btnCancel; - QDialog* m_textDialog; - MRichTextEdit* m_rte; QString m_title; + + QGIRichAnno* m_qgiAnno; + bool m_syncLock; + MDIViewPage* m_view; + + QPointer m_toolbar {nullptr}; + QPointer m_viewport {nullptr}; }; class TaskDlgRichAnno : public Gui::TaskView::TaskDialog @@ -122,28 +140,28 @@ class TaskDlgRichAnno : public Gui::TaskView::TaskDialog Q_OBJECT public: - TaskDlgRichAnno(TechDraw::DrawView* baseFeat, - TechDraw::DrawPage* page); + TaskDlgRichAnno(TechDraw::DrawView* baseFeat, TechDraw::DrawPage* page); explicit TaskDlgRichAnno(TechDrawGui::ViewProviderRichAnno* annoVP); ~TaskDlgRichAnno() override; public: - /// is called the TaskView when the dialog is opened - void open() override; - /// is called by the framework if an button is clicked which has no accept or reject role - void clicked(int) override; /// is called by the framework if the dialog is accepted (Ok) bool accept() override; /// is called by the framework if the dialog is rejected (Cancel) bool reject() override; + void autoClosedOnTransactionChange() override; /// is called by the framework if the user presses the help button bool isAllowedAlterDocument() const override - { return false; } - void update(); + { + return false; + } void modifyStandardButtons(QDialogButtonBox* box) override; -protected: + bool isFor(ViewProviderRichAnno* vp) const + { + return (widget->getAnnoVP() == vp); + } private: TaskRichAnno * widget; diff --git a/src/Mod/TechDraw/Gui/TaskRichAnno.ui b/src/Mod/TechDraw/Gui/TaskRichAnno.ui index ff1233af22..6002e590de 100644 --- a/src/Mod/TechDraw/Gui/TaskRichAnno.ui +++ b/src/Mod/TechDraw/Gui/TaskRichAnno.ui @@ -6,22 +6,10 @@ 0 0 - 409 - 419 + 150 + 80 - - - 0 - 0 - - - - - 250 - 0 - - Rich Text Annotation Block @@ -31,209 +19,157 @@ - - - - 0 - 0 - + + + Base feature - - QFrame::Box + + + + + + false - - QFrame::Raised + + + + + + Width - + + + + + + Maximal width of the annotation block, if -1 then automatic width + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + -1.000000000000000 + + + -1.000000000000000 + + + + + + + Frame + + + true + + + false + + + + 6 + + + 6 + + + 6 + + + 4 + - - - - - Base feature - - - - - - - false - - - - - - - Max width - - - - - - - Maximal width, if -1 then automatic width - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - -1.000000000000000 - - - -1.000000000000000 - - - - + + + Thickness + + + + + + + Line width + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + 0.100000000000000 + + + 0.500000000000000 + + - - - Qt::Horizontal + + + Style + + + + Line style + + + 1 + + + + NoLine + + + + + Continuous + + + + + Dash + + + + + Dot + + + + + DashDot + + + + + DashDotDot + + + + - - - true - + - Start Rich Text Editor + Color - - - - - 0 - 0 - + + + + Line color + + + + 0 + 0 + 0 + - - - - - - Show frame - - - - - - - Qt::LeftToRight - - - - - - - - - - Color - - - - - - - false - - - Line color - - - - 0 - 0 - 0 - - - - - - - - Width - - - - - - - false - - - Line width - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - 0.100000000000000 - - - 0.500000000000000 - - - - - - - Style - - - - - - - false - - - Line style - - - 1 - - - - NoLine - - - - - Continuous - - - - - Dash - - - - - Dot - - - - - DashDot - - - - - DashDotDot - - - - - - @@ -256,52 +192,52 @@ - cbShowFrame + gbFrame toggled(bool) cpFrameColor setEnabled(bool) - 183 - 270 + 250 + 380 - 183 - 294 + 380 + 380 - cbShowFrame + gbFrame toggled(bool) cFrameStyle setEnabled(bool) - 236 - 277 + 250 + 380 - 236 - 354 + 330 + 380 - cbShowFrame + gbFrame toggled(bool) dsbWidth setEnabled(bool) - 236 - 277 + 250 + 380 - 236 - 328 + 280 + 380 - + \ No newline at end of file diff --git a/src/Mod/TechDraw/Gui/ViewProviderRichAnno.cpp b/src/Mod/TechDraw/Gui/ViewProviderRichAnno.cpp index f9e8eb47c4..431a571526 100644 --- a/src/Mod/TechDraw/Gui/ViewProviderRichAnno.cpp +++ b/src/Mod/TechDraw/Gui/ViewProviderRichAnno.cpp @@ -57,7 +57,7 @@ const char* ViewProviderRichAnno::LineStyleEnums[] = { "NoLine", ViewProviderRichAnno::ViewProviderRichAnno() { - sPixmap = "actions/TechDraw_RichTextAnnotation"; + sPixmap = "actions/TechDraw_Annotation"; static const char *group = "Frame Format"; @@ -70,6 +70,21 @@ ViewProviderRichAnno::ViewProviderRichAnno() } +bool ViewProviderRichAnno::setEdit(int ModNum) +{ + if (ModNum != Gui::ViewProvider::Default) { + return Gui::ViewProviderDocumentObject::setEdit(ModNum); + } + if (Gui::Control().activeDialog()) { + return false; // TaskPanel already open! + } + + // clear the selection (convenience) + Gui::Selection().clearSelection(); + Gui::Control().showDialog(new TaskDlgRichAnno(this)); + return true; +} + bool ViewProviderRichAnno::doubleClicked() { // Base::Console().message("VPRA::doubleClicked()\n"); @@ -201,3 +216,20 @@ std::vector ViewProviderRichAnno::claimChildren() const } return temp; } + +bool ViewProviderRichAnno::onDelete(const std::vector& subs) +{ + Q_UNUSED(subs); + + // Check if there is an active dialog + if (Gui::TaskView::TaskDialog* dlg = Gui::Control().activeDialog()) { + // Check if the active dialog is our RichAnno dialog + if (auto* richAnnoDlg = dynamic_cast(dlg)) { + // Check if the dialog is for THIS specific view provider + if (richAnnoDlg->isFor(this)) { + Gui::Control().closeDialog(); // Close the dialog gracefully + } + } + } + return true; // Allow deletion to proceed +} \ No newline at end of file diff --git a/src/Mod/TechDraw/Gui/ViewProviderRichAnno.h b/src/Mod/TechDraw/Gui/ViewProviderRichAnno.h index d1da2db8c5..cbd80b2b13 100644 --- a/src/Mod/TechDraw/Gui/ViewProviderRichAnno.h +++ b/src/Mod/TechDraw/Gui/ViewProviderRichAnno.h @@ -54,8 +54,10 @@ public: bool useNewSelectionModel() const override {return false;} void updateData(const App::Property* prop) override; void onChanged(const App::Property* prop) override; + bool onDelete(const std::vector&) override; bool doubleClicked() override; bool canDelete(App::DocumentObject* obj) const override; + bool setEdit(int ModNum) override; static const char* LineStyleEnums[]; diff --git a/src/Mod/TechDraw/Gui/Workbench.cpp b/src/Mod/TechDraw/Gui/Workbench.cpp index 46c1fa0c1c..c2880c365d 100644 --- a/src/Mod/TechDraw/Gui/Workbench.cpp +++ b/src/Mod/TechDraw/Gui/Workbench.cpp @@ -387,9 +387,8 @@ Gui::ToolBarItem* Workbench::setupToolBars() const Gui::ToolBarItem* anno = new Gui::ToolBarItem(root); anno->setCommand("TechDraw Annotation"); - *anno << "TechDraw_Annotation"; - *anno << "TechDraw_LeaderLine"; *anno << "TechDraw_RichTextAnnotation"; + *anno << "TechDraw_LeaderLine"; *anno << "TechDraw_CosmeticVertexGroup"; *anno << "TechDraw_CenterLineGroup"; *anno << "TechDraw_2PointCosmeticLine"; diff --git a/src/Mod/TechDraw/Gui/mrichtextedit.cpp b/src/Mod/TechDraw/Gui/mrichtextedit.cpp index b7fe3f1d61..8fb480ad1f 100644 --- a/src/Mod/TechDraw/Gui/mrichtextedit.cpp +++ b/src/Mod/TechDraw/Gui/mrichtextedit.cpp @@ -58,8 +58,12 @@ using namespace TechDrawGui; using namespace TechDraw; -MRichTextEdit::MRichTextEdit(QWidget *parent, QString textIn) : QWidget(parent) { +MRichTextEdit::MRichTextEdit(QWidget *parent, QString textIn) : QFrame(parent) { setupUi(this); + + f_fontsize->setEditable(true); + f_fontsize->setMinimumContentsLength(3); + m_lastBlockList = nullptr; f_textedit->setTabStopDistance(40); setDefFontSize(TechDrawGui::PreferencesGui::labelFontSizePX()); @@ -228,8 +232,6 @@ MRichTextEdit::MRichTextEdit(QWidget *parent, QString textIn) : QWidget(parent) int intSize = round(currSize); QString qsSize = QString::number(intSize); addFontSize(qsSize); - int fSize = f_fontsize->findText(qsSize); - f_fontsize ->setCurrentIndex(fSize); } else { QTextCursor cursor = f_textedit->textCursor(); cursor.movePosition(QTextCursor::Start); @@ -239,10 +241,9 @@ MRichTextEdit::MRichTextEdit(QWidget *parent, QString textIn) : QWidget(parent) fmt.setFontPointSize(getDefFontSizeNum()); addFontSize(getDefFontSize()); - - f_fontsize->setCurrentIndex(f_fontsize->findText(getDefFontSize())); } + updateFontSizeDisplay(); } @@ -486,14 +487,15 @@ void MRichTextEdit::list(bool checked, QTextListFormat::Style style) { QTextBlockFormat bfmt; bfmt.setIndent(obfmt.indent()); cursor.setBlockFormat(bfmt); - } else { + } + else { QTextListFormat listFmt; if (cursor.currentList()) { listFmt = cursor.currentList()->format(); } listFmt.setStyle(style); cursor.createList(listFmt); - } + } cursor.endEditBlock(); } @@ -507,17 +509,15 @@ void MRichTextEdit::mergeFormatOnWordOrSelection(const QTextCharFormat &format) f_textedit->setFocus(Qt::TabFocusReason); } -void MRichTextEdit::slotCursorPositionChanged() { +void MRichTextEdit::slotCursorPositionChanged() +{ + updateFontSizeDisplay(); + //why do we change text style when selecting text? QTextCursor cursor = f_textedit->textCursor(); if (cursor.hasSelection()) { //let selection logic handle this return; } - //set font size widget to match curr char size - QTextCharFormat fmt = cursor.charFormat(); - double currSize = fmt.fontPointSize(); - int fSize = f_fontsize->findText(QString::number(currSize)); - f_fontsize ->setCurrentIndex(fSize); QTextList *l = f_textedit->textCursor().currentList(); @@ -704,20 +704,25 @@ void MRichTextEdit::onExit() Q_EMIT editorFinished(); } -void MRichTextEdit::onSelectionChanged() +void MRichTextEdit::updateFontSizeDisplay() { if (hasMultipleSizes()) { f_fontsize->setEditText(QString()); f_fontsize->setCurrentIndex(-1); - } else { + } + else { QTextCursor cursor = f_textedit->textCursor(); QTextCharFormat fmt = cursor.charFormat(); double currSize = fmt.fontPointSize(); - int fSize = f_fontsize->findText(QString::number(currSize)); - f_fontsize ->setCurrentIndex(fSize); + f_fontsize->setEditText(QString::number(currSize, 'g', 4)); } } +void MRichTextEdit::onSelectionChanged() +{ + updateFontSizeDisplay(); +} + //does selection have multiple sizes? bool MRichTextEdit::hasMultipleSizes() { @@ -801,33 +806,55 @@ QFont MRichTextEdit::getDefFont() // if the conversion factor is float, will generate non-standard sizes void MRichTextEdit::addFontSize(QString fontSize) { + bool ok; + const double newSize = fontSize.toDouble(&ok); + if (!ok) { + return; // Ignore invalid numbers + } + + // 1. Collect all existing sizes as doubles + QList sizes; + for (int i = 0; i < f_fontsize->count(); ++i) { + sizes.append(f_fontsize->itemText(i).toDouble()); + } + + // 2. Check if the new size is already in the list (using fuzzy comparison for doubles) + for (double existingSize : qAsConst(sizes)) { + if (qFuzzyCompare(existingSize, newSize)) { + // Already exists, just make sure it's the current text + f_fontsize->setCurrentText(QString::number(newSize, 'g', 4)); + return; + } + } + + // 3. Add the new size and re-sort + sizes.append(newSize); + std::sort(sizes.begin(), sizes.end()); + + // 4. Repopulate the combobox with the sorted, correctly formatted list QStringList newList; - int fsVal = fontSize.toInt(); - int size = f_fontsize->count(); - bool added = false; - for (int i = 0; i < size; i++) { - QString item = f_fontsize->itemText(i); - int itemVal = item.toInt(); - if (added) { - newList << item; - continue; - } - if (itemVal < fsVal) { - newList << item; - } else if (itemVal == fsVal) { // already in list - newList << item; - added = true; - } else { //item > fs, add both - newList << fontSize; - newList << item; - added = true; - } - } - if (!added) { //bigger than all choices, add to end of list - newList << fontSize; + for (double size : qAsConst(sizes)) { + newList << QString::number(size, 'g', 4); } + + const QString currentText = f_fontsize->currentText(); // Save current text + f_fontsize->blockSignals(true); f_fontsize->clear(); f_fontsize->addItems(newList); + f_fontsize->setCurrentText(currentText); // Restore current text + f_fontsize->blockSignals(false); + + // 5. Set the new size as the current item + f_fontsize->setCurrentText(fontSize); +} + +void MRichTextEdit::setMinimalMode(bool on) +{ + f_save->setVisible(!on); + f_exit->setVisible(!on); + f_cut->setVisible(!on); + f_copy->setVisible(!on); + f_paste->setVisible(!on); } #include diff --git a/src/Mod/TechDraw/Gui/mrichtextedit.h b/src/Mod/TechDraw/Gui/mrichtextedit.h index 741242aaab..2a5104f2d5 100644 --- a/src/Mod/TechDraw/Gui/mrichtextedit.h +++ b/src/Mod/TechDraw/Gui/mrichtextedit.h @@ -37,7 +37,7 @@ /** * @brief A simple rich-text editor */ -class MRichTextEdit : public QWidget, protected Ui::MRichTextEdit { +class MRichTextEdit : public QFrame, protected Ui::MRichTextEdit { Q_OBJECT public: @@ -55,6 +55,8 @@ public: int getDefFontSizeNum(); QFont getDefFont(); + void setMinimalMode(bool on); + public Q_SLOTS: void setText(const QString &text); @@ -72,6 +74,7 @@ protected: void focusInEvent(QFocusEvent *event) override; void keyPressEvent(QKeyEvent *event) override; bool hasMultipleSizes(); + void updateFontSizeDisplay(); void addFontSize(QString fontSize);