/* Copyright 2006-2026 The QElectroTech Team This file is part of QElectroTech. QElectroTech is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. QElectroTech is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with QElectroTech. If not, see . */ #include "diagrameventaddshape.h" #include "../diagram.h" #include "../lastusedstyle.h" #include "../qetapp.h" #include "../qetdiagrameditor.h" #include "../undocommand/addgraphicsobjectcommand.h" #include #include #include #include #include /** @brief DiagramEventAddShape::DiagramEventAddShape Default constructor @param diagram : the diagram where this event must operate @param shape_type : the type of shape to draw */ DiagramEventAddShape::DiagramEventAddShape(Diagram *diagram, QetShapeItem::ShapeType shape_type) : DiagramEventInterface(diagram), m_shape_type (shape_type), m_shape_item (nullptr), m_help_horiz (nullptr), m_help_verti (nullptr) { m_running = true; init(); // Deferred to the next event-loop iteration, not shown immediately: // Diagram::setEventInterface() destroys whatever tool was // previously active *after* this constructor returns, and that // previous tool's own destructor clears the status bar (see // ~DiagramEventAddShape() below) -- an immediate show here would // just get wiped out moments later by that cleanup. Letting the old // tool's teardown finish first, then showing this one's message, is // the same fix already used for the tooltip-flicker issue in // QetShapeItem::refreshInteractionHints(), applied to the same // class of "something later in the same call chain undoes what I // just did" ordering problem. QTimer::singleShot(0, this, [this]() { updateCreationHint(); }); } /** @brief DiagramEventAddShape::~DiagramEventAddShape */ DiagramEventAddShape::~DiagramEventAddShape() { if ((m_running || m_abort) && m_shape_item) { m_diagram->removeItem(m_shape_item); delete m_shape_item; } delete m_help_horiz; delete m_help_verti; delete m_center_marker; if (m_diagram && !m_diagram->views().isEmpty()) { if (auto *editor = QETApp::diagramEditorAncestorOf(m_diagram->views().constFirst())) editor->statusBar()->clearMessage(); } foreach (QGraphicsView *v, m_diagram->views()) v->setContextMenuPolicy(Qt::DefaultContextMenu); } /** @brief DiagramEventAddShape::applyPosition Applies a drag/click position to the in-progress shape, honouring two modifiers that mirror how the very same shape can already be edited afterward, once placed: - Ctrl, for Rectangle/Ellipse only: the first click's point acts as the shape's *center* rather than a corner, growing symmetrically as the cursor moves away from it -- the same meaning Ctrl already has on a Resize handle (anchor at center). Deliberately not offered for Line: unlike the Rectangle/Ellipse case, there's no established convention for "a line grows symmetrically from its middle" to justify it by, so Ctrl for Line means only free positioning (see the plain grid-snap check above), nothing more. - Shift, for Rectangle/Ellipse only: forces the bounding box square (so Ellipse becomes a true circle), using whichever of the two dragged dimensions is currently larger and mirroring that onto the other, preserving the direction the user is actually dragging in. Both can combine (Ctrl+Shift: a centered square/circle). Whether Ctrl currently anchors from the center is re-decided on every call, from the live modifiers passed in here -- not frozen at whatever was held on the first click. Every comparable tool (Illustrator, Photoshop, Figma, Inkscape...) lets you press or release the center-origin modifier at any point mid-drag, with the shape immediately jumping to match; that jump is the expected feedback for changing which point is anchored, not a glitch. Freezing the choice at the first click instead meant holding Ctrl anywhere other than the initial mouse-down did nothing visible -- which, tried the more natural way (drag first, then reach for Ctrl once you decide you want it centered), read as "Ctrl doesn't work" rather than as a deliberate one-shot decision. */ void DiagramEventAddShape::applyPosition(const QPointF &pos, Qt::KeyboardModifiers mods) { if (!m_shape_item) return; if (m_shape_type == QetShapeItem::Polygon) { // setP2() has its own dedicated Polygon handling: it moves the // *last* vertex in place rather than setting a second point, // which is exactly the live "next segment follows the mouse" // preview -- the same idea DiagramEventAddPath's trailing // preview node gives the pen tool. No Ctrl/Shift modifiers apply // here (those are Rectangle/Ellipse/Line-specific below), so // this is a direct, unconditional call. m_shape_item->setP2(pos); return; } m_center_anchored = (mods & Qt::ControlModifier) && (m_shape_type == QetShapeItem::Rectangle || m_shape_type == QetShapeItem::Ellipse); if (m_center_anchored) showCenterMarker(m_anchor_point); else hideCenterMarker(); QPointF target = pos; if ((mods & Qt::ShiftModifier) && (m_shape_type == QetShapeItem::Rectangle || m_shape_type == QetShapeItem::Ellipse)) { const QPointF ref = m_anchor_point; const qreal dx = target.x() - ref.x(); const qreal dy = target.y() - ref.y(); const qreal size = qMax(qAbs(dx), qAbs(dy)); target.setX(ref.x() + (dx < 0 ? -size : size)); target.setY(ref.y() + (dy < 0 ? -size : size)); } if (m_center_anchored) { const QPointF mirrored = 2 * m_anchor_point - target; m_shape_item->setRect(QRectF(mirrored, target)); } else { if (m_shape_type == QetShapeItem::Line) m_shape_item->setLine(QLineF(m_anchor_point, target)); else m_shape_item->setRect(QRectF(m_anchor_point, target)); } } /** @brief DiagramEventAddShape::showCenterMarker Small, filled marker at the anchor point, shown only while Ctrl- anchoring is actually in effect right now (see applyPosition()) -- doubling as live confirmation that it is, rather than leaving the user to infer it purely from how the shape happens to be growing. */ void DiagramEventAddShape::showCenterMarker(const QPointF &scenePos) { if (!m_center_marker) { m_center_marker = new QGraphicsEllipseItem(-4, -4, 8, 8); QPen pen(Qt::red); pen.setCosmetic(true); m_center_marker->setPen(pen); m_center_marker->setBrush(Qt::red); m_diagram->addItem(m_center_marker); } m_center_marker->setPos(scenePos); } void DiagramEventAddShape::hideCenterMarker() { delete m_center_marker; m_center_marker = nullptr; } /** @brief DiagramEventAddShape::mousePressEvent Action when mouse is pressed @param event : event of mouse press */ void DiagramEventAddShape::mousePressEvent(QGraphicsSceneMouseEvent *event) { if (Q_UNLIKELY(m_diagram->isReadOnly())) { return; } QPointF pos = event->scenePos(); // A bitwise flag check, not exact equality: modifiers() == Ctrl // alone fails the moment any other key (Shift for the square/circle // lock, or an incidental platform flag) is also held, silently // falling through to snapToGrid() even though Ctrl is held -- // exactly what made Ctrl+Shift together feel "frozen" (both grid- // snapped *and* square-locked, quantizing to whichever is coarser) // and made "Ctrl = free positioning" not actually hold up. if (!(event->modifiers() & Qt::ControlModifier)) { pos = Diagram::snapToGrid(pos); } //Action for left mouse click if (event->button() == Qt::LeftButton) { //Create shape item if (!m_shape_item) { m_shape_item = new QetShapeItem(pos, pos, m_shape_type); m_anchor_point = pos; // Initial feedback only -- applyPosition() re-decides this // live on every subsequent move, from whatever Ctrl state is // held at the time. m_center_anchored = (event->modifiers() & Qt::ControlModifier) && (m_shape_type == QetShapeItem::Rectangle || m_shape_type == QetShapeItem::Ellipse); if (m_center_anchored) showCenterMarker(m_anchor_point); //Start from whatever pen/brush was last applied this //session, rather than always the hardcoded default. if (LastUsedStyle::hasShapePen()) { m_shape_item->setPen(LastUsedStyle::shapePen()); } if (LastUsedStyle::hasShapeBrush()) { m_shape_item->setBrush(LastUsedStyle::shapeBrush()); } m_diagram->addItem (m_shape_item); updateCreationHint(); event->setAccepted(true); return; } //If current item isn't a polyline, add it with an undo command if (m_shape_type != QetShapeItem::Polygon) { applyPosition(pos, event->modifiers()); if (m_shape_item->shapeType() == QetShapeItem::Rectangle || m_shape_item->shapeType() == QetShapeItem::Ellipse) { m_shape_item->setRect(m_shape_item->rect().normalized()); } m_diagram->undoStack().push (new AddGraphicsObjectCommand(m_shape_item, m_diagram)); m_shape_item = nullptr; //< set to nullptr for create new shape at next left clic hideCenterMarker(); updateCreationHint(); } //Else add a new point to polyline else { m_shape_item->setNextPoint (pos); } event->setAccepted(true); return; } if (event->button() == Qt::RightButton) { event->setAccepted(true); } } /** @brief DiagramEventAddShape::mouseMoveEvent Action when mouse move @param event : event of mouse move */ void DiagramEventAddShape::mouseMoveEvent(QGraphicsSceneMouseEvent *event) { updateHelpCross(event->scenePos()); // Re-asserted on every move, not just once at activation: Qt's own // built-in "show an action's statusTip on hover" has its own // internal "restore whatever was there before" logic for when the // hover ends. Since our message is shown *during* that same hover // session (the user is still over the toolbar icon when the // deferred constructor-time call fires), Qt's hover-tracking has no // idea we changed the status bar in the meantime -- the moment the // mouse leaves the icon for the canvas, it "restores" to whatever it // remembers being there before its own tip started, which is stale // and empty, silently overwriting ours. Re-showing it here, on every // move within the canvas, simply outlasts that one-time restore. updateCreationHint(); if (m_shape_item && event->buttons() == Qt::NoButton) { m_last_mouse_scene_pos = event->scenePos(); // raw, before snapping -- see reapplyLastPosition() QPointF pos = event->scenePos(); if (!(event->modifiers() & Qt::ControlModifier)) { pos = Diagram::snapToGrid(pos); } applyPosition(pos, event->modifiers()); event->setAccepted(true); } } /** @brief DiagramEventAddShape::keyPressEvent / keyReleaseEvent Pressing or releasing Shift (the square/circle lock) does nothing visible on its own -- applyPosition() only ever runs from mouseMoveEvent, so without this, a keyboard-only change just sits there until the next, often incidental, pixel of mouse movement brings the shape in line with it. That's exactly what looked like a freeze: holding Shift while the mouse is genuinely still produces no visible change (correctly -- nothing has moved), and it only "unsticks" once the mouse moves again, which released keys tend to coincide with purely by hand tremor, not because releasing itself did anything. Re-running the last known mouse position through applyPosition() here makes the key press or release itself the trigger, giving immediate feedback instead of waiting on chance. Any other key falls back to standard handling, e.g. pressing ESC aborts the operation. */ void DiagramEventAddShape::keyPressEvent(QKeyEvent *event) { if (event->key() == Qt::Key_Shift || event->key() == Qt::Key_Control) { reapplyLastPosition(event); event->setAccepted(true); } else { DiagramEventInterface::keyPressEvent(event); } } void DiagramEventAddShape::keyReleaseEvent(QKeyEvent *event) { reapplyLastPosition(event); } void DiagramEventAddShape::reapplyLastPosition(QKeyEvent *event) { if (!m_shape_item || (event->key() != Qt::Key_Shift && event->key() != Qt::Key_Control)) return; // A fresh, global query, not event->modifiers(): for a press/release // of a modifier key itself, whether that key is already reflected in // the key event's own modifiers() is ambiguous and platform- // dependent -- the same reason Diagram::snapToGrid() queries this // directly rather than trusting a passed-in modifiers() value. const Qt::KeyboardModifiers mods = QGuiApplication::keyboardModifiers(); QPointF pos = m_last_mouse_scene_pos; if (!(mods & Qt::ControlModifier)) pos = Diagram::snapToGrid(pos); applyPosition(pos, mods); } /** @brief DiagramEventAddShape::mouseReleaseEvent Action when mouse button is released @param event : event of mouse release */ void DiagramEventAddShape::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) { if (event->button() == Qt::RightButton) { //If shape is created, we manage right click if (m_shape_item) { //Shape is a polyline and have three points or more we just remove the last point if (m_shape_type == QetShapeItem::Polygon && (m_shape_item->pointsCount() >= 3) ) { m_shape_item->removePoints(); QPointF pos = event->scenePos(); if (!(event->modifiers() & Qt::ControlModifier)) pos = Diagram::snapToGrid(pos); m_shape_item->setP2(pos); //Set the new last point under the cursor event->setAccepted(true); return; } //For other case, we remove item from scene m_diagram->removeItem(m_shape_item); delete m_shape_item; m_shape_item = nullptr; hideCenterMarker(); updateCreationHint(); event->setAccepted(true); return; } //Else (no shape), we set to false the running status //for indicate to the owner of this event that everything is done m_running = false; emit finish(); event->setAccepted(true); } } /** @brief DiagramEventAddShape::mouseDoubleClickEvent Action when mouse button is double clicked @param event : event of mouse double click */ void DiagramEventAddShape::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) { //If current item is a polyline, add it with an undo command if (m_shape_item && m_shape_type == QetShapeItem::Polygon && event->button() == Qt::LeftButton) { //removePoints(); //If the last is at the same pos of the first point //that mean user want a closed polygon, so we remove the last point and close polygon QPolygonF polygon = m_shape_item->polygon(); if (polygon.first() == polygon.last()) { m_shape_item->removePoints(); m_shape_item->setClosed(true); } m_diagram->undoStack().push (new AddGraphicsObjectCommand(m_shape_item, m_diagram)); m_shape_item = nullptr; //< set to nullptr for create new shape at next left clic hideCenterMarker(); updateCreationHint(); event->setAccepted(true); } } void DiagramEventAddShape::init() { foreach (QGraphicsView *v, m_diagram->views()) v->setContextMenuPolicy(Qt::NoContextMenu); } /** @brief DiagramEventAddShape::updateCreationHint Shows whichever of beforeClickHint()/afterClickHint() matches the current phase -- there was previously either no message at all (Line/Rectangle/Ellipse) or a single static one that never changed regardless of progress (Polygon, set externally in QETDiagramEditor::addItemGroupTriggered()); this replaces both with one phase-aware message per shape type, managed by the tool itself. */ void DiagramEventAddShape::updateCreationHint() const { if (!m_diagram || m_diagram->views().isEmpty()) return; if (auto *editor = QETApp::diagramEditorAncestorOf(m_diagram->views().constFirst())) editor->statusBar()->showMessage(m_shape_item ? afterClickHint() : beforeClickHint()); } QString DiagramEventAddShape::beforeClickHint() const { switch (m_shape_type) { case QetShapeItem::Line: return tr("Clic gauche : positionner le point de départ (Ctrl = position libre)"); case QetShapeItem::Rectangle: case QetShapeItem::Ellipse: return tr("Clic gauche : positionner le premier coin (Ctrl = point central, position libre)"); case QetShapeItem::Polygon: return tr("Clic gauche : positionner le premier point (Ctrl = position libre)"); default: return QString(); } } QString DiagramEventAddShape::afterClickHint() const { switch (m_shape_type) { case QetShapeItem::Line: return tr("Clic gauche : positionner le point final (Ctrl = position libre) ; clic droit : annuler"); case QetShapeItem::Rectangle: return tr("Clic gauche : positionner le coin opposé (Maj = carré, " "Ctrl = depuis le centre + position libre, Ctrl+Maj = carré centré) ; clic droit : annuler"); case QetShapeItem::Ellipse: return tr("Clic gauche : positionner le coin opposé (Maj = cercle, " "Ctrl = depuis le centre + position libre, Ctrl+Maj = cercle centré) ; clic droit : annuler"); case QetShapeItem::Polygon: return tr("Clic gauche : point suivant ; double-clic ou Entrée : terminer ; " "clic droit : annuler le dernier point"); default: return QString(); } } /** @brief DiagramEventAddShape::updateHelpCross Create and update the position of the cross to help user for draw new shape @param p : the center of the cross */ void DiagramEventAddShape::updateHelpCross(const QPointF &p) { //If line isn't created yet, we create it. if (!m_help_horiz || !m_help_verti) { QPen pen; pen.setWidthF(0.4); pen.setCosmetic(true); pen.setColor(Diagram::background_color == Qt::darkGray ? Qt::lightGray : Qt::darkGray); QRectF rect = m_diagram->border_and_titleblock.insideBorderRect(); if (!m_help_horiz) { m_help_horiz = new QGraphicsLineItem(rect.topLeft().x(), 0, rect.topRight().x(), 0); m_help_horiz->setPen(pen); m_diagram->addItem(m_help_horiz); } if (!m_help_verti) { m_help_verti = new QGraphicsLineItem(0, rect.topLeft().y(), 0, rect.bottomLeft().y()); m_help_verti->setPen(pen); m_diagram->addItem(m_help_verti); } } //Update the position of the cross QPointF point = Diagram::snapToGrid(p); m_help_horiz->setY(point.y()); m_help_verti->setX(point.x()); }