From 2bed00fd2508ef1ce402347ff2a214dbb6161da7 Mon Sep 17 00:00:00 2001 From: xavier Date: Fri, 8 Feb 2013 22:05:15 +0000 Subject: [PATCH] Implemented a primitive decorator, allowing groups of primitives to be easily resized. git-svn-id: svn+ssh://svn.tuxfamily.org/svnroot/qet/qet/trunk@2027 bfdf4180-ca20-0410-9c96-a3a8aa849046 --- sources/editor/customelementpart.cpp | 55 ++ sources/editor/customelementpart.h | 12 +- sources/editor/editorcommands.cpp | 101 +++ sources/editor/editorcommands.h | 38 ++ sources/editor/elementprimitivedecorator.cpp | 614 +++++++++++++++++++ sources/editor/elementprimitivedecorator.h | 90 +++ sources/editor/elementscene.cpp | 150 +++-- sources/editor/elementscene.h | 13 +- sources/editor/partarc.cpp | 2 +- sources/editor/partcircle.cpp | 2 +- sources/editor/partellipse.cpp | 2 +- sources/editor/partline.cpp | 2 +- sources/editor/partpolygon.cpp | 2 +- sources/editor/partrectangle.cpp | 2 +- sources/editor/partterminal.cpp | 2 +- sources/editor/parttext.cpp | 250 ++++++-- sources/editor/parttext.h | 18 +- sources/editor/parttextfield.cpp | 243 +++++++- sources/editor/parttextfield.h | 18 +- sources/qet.cpp | 57 ++ sources/qet.h | 17 + 21 files changed, 1566 insertions(+), 124 deletions(-) create mode 100644 sources/editor/elementprimitivedecorator.cpp create mode 100644 sources/editor/elementprimitivedecorator.h diff --git a/sources/editor/customelementpart.cpp b/sources/editor/customelementpart.cpp index 4a9722884..b357d967d 100644 --- a/sources/editor/customelementpart.cpp +++ b/sources/editor/customelementpart.cpp @@ -44,6 +44,61 @@ QUndoStack &CustomElementPart::undoStack() const { return(elementScene() -> undoStack()); } +/// @return this primitive as a QGraphicsItem +QGraphicsItem *CustomElementPart::toItem() { + return(dynamic_cast(this)); +} + +/** + This method is called by the decorator when it manages only a single + primitive. This brings the possibility to implement custom behaviour, such + as text edition, points edition or specific resizing. + The default implementation does nothing. +*/ +void CustomElementPart::setDecorator(ElementPrimitiveDecorator *decorator) { + Q_UNUSED(decorator) +} + +/** + This method is called by the decorator when it manages only a single + primitive and it received a mouse press event. + The implementation should return true if the primitive accepts the event, false otherwise. + The default implementation returns false. +*/ +bool CustomElementPart::singleItemPressEvent(ElementPrimitiveDecorator *, QGraphicsSceneMouseEvent *) { + return(false); +} + +/** + This method is called by the decorator when it manages only a single + primitive and it received a mouse move event. + The implementation should return true if the primitive accepts the event, false otherwise. + The default implementation returns false. +*/ +bool CustomElementPart::singleItemMoveEvent(ElementPrimitiveDecorator *, QGraphicsSceneMouseEvent *) { + return(false); +} + +/** + This method is called by the decorator when it manages only a single + primitive and it received a mouse release event. + The implementation should return true if the primitive accepts the event, false otherwise. + The default implementation returns false. +*/ +bool CustomElementPart::singleItemReleaseEvent(ElementPrimitiveDecorator *, QGraphicsSceneMouseEvent *) { + return(false); +} + +/** + This method is called by the decorator when it manages only a single + primitive and it received a mouse double click event. + The implementation should return true if the primitive accepts the event, false otherwise. + The default implementation returns false. +*/ +bool CustomElementPart::singleItemDoubleClickEvent(ElementPrimitiveDecorator *, QGraphicsSceneMouseEvent *) { + return(false); +} + /** Helper method to map points in CustomElementPart::handleUserTransformation() @param initial_selection_rect Selection rectangle when the movement started, in scene coordinates diff --git a/sources/editor/customelementpart.h b/sources/editor/customelementpart.h index b2f1c85ad..c1c24d713 100644 --- a/sources/editor/customelementpart.h +++ b/sources/editor/customelementpart.h @@ -21,8 +21,10 @@ #include #include class CustomElement; -class QETElementEditor; +class ElementPrimitiveDecorator; class ElementScene; +class QETElementEditor; + /** This abstract class represents a primitive of the visual representation of an electrical element. The Element, FixedElement and CustomElement classes do not @@ -95,6 +97,14 @@ class CustomElementPart { /// @return the name that will be used as XML tag when exporting the primitive virtual QString xmlName() const = 0; + virtual QGraphicsItem *toItem(); + + virtual void setDecorator(ElementPrimitiveDecorator *); + virtual bool singleItemPressEvent(ElementPrimitiveDecorator *, QGraphicsSceneMouseEvent *); + virtual bool singleItemMoveEvent(ElementPrimitiveDecorator *, QGraphicsSceneMouseEvent *); + virtual bool singleItemReleaseEvent(ElementPrimitiveDecorator *, QGraphicsSceneMouseEvent *); + virtual bool singleItemDoubleClickEvent(ElementPrimitiveDecorator *, QGraphicsSceneMouseEvent *); + protected: QList mapPoints(const QRectF &, const QRectF &, const QList &); }; diff --git a/sources/editor/editorcommands.cpp b/sources/editor/editorcommands.cpp index e9f65638e..ab573df5b 100644 --- a/sources/editor/editorcommands.cpp +++ b/sources/editor/editorcommands.cpp @@ -684,3 +684,104 @@ void ChangeInformationsCommand::undo() { void ChangeInformationsCommand::redo() { editor_scene_ -> setInformations(new_informations_); } + +/** + Constructor + @param scene Modified ElementScene + @param parent Parent QUndoCommand +*/ +ScalePartsCommand::ScalePartsCommand(ElementScene *scene, QUndoCommand * parent) : + ElementEditionCommand(scene, 0, parent), + first_redo(true) +{ +} + +/** + Destructor +*/ +ScalePartsCommand::~ScalePartsCommand() { +} + +/** + Undo the scaling operation +*/ +void ScalePartsCommand::undo() { + scale(new_rect_, original_rect_); +} + +/** + Redo the scaling operation +*/ +void ScalePartsCommand::redo() { + if (first_redo) { + first_redo = false; + return; + } + scale(original_rect_, new_rect_); +} + +/** + @return the element editor/scene the command should take place on +*/ +ElementScene *ScalePartsCommand::elementScene() const { + return(editor_scene_); +} + +/** + Set \a primitives as the list of primitives to be scaled by this command +*/ +void ScalePartsCommand::setScaledPrimitives(const QList &primitives) { + scaled_primitives_ = primitives; + adjustText(); +} + +/** + @return the list of primitives to be scaled by this command +*/ +QList ScalePartsCommand::scaledPrimitives() const { + return(scaled_primitives_); +} + +/** + Define the transformation applied by this command + @param original_rect Bounding rectangle for all scaled primitives before the operation + @param original_rect Bounding rectangle for all scaled primitives after the operation +*/ +void ScalePartsCommand::setTransformation(const QRectF &original_rect, const QRectF &new_rect) { + original_rect_ = original_rect; + new_rect_ = new_rect; +} + +/** + @return the transformation applied by this command. The returned rectangles + are the bounding rectangles for all scaled primitives respectively before + and after the operation. +*/ +QPair ScalePartsCommand::transformation() { + return(QPair(original_rect_, new_rect_)); +} + +/** + Apply the scaling operation from \a before to \a after. +*/ +void ScalePartsCommand::scale(const QRectF &before, const QRectF &after) { + if (!scaled_primitives_.count()) return; + if (before == after) return; + if (!before.width() || !before.height()) return; // cowardly flee division by zero FIXME? + + foreach (CustomElementPart *part_item, scaled_primitives_) { + part_item -> startUserTransformation(before); + part_item -> handleUserTransformation(before, after); + } +} + +/** + Generate the text describing what this command does exactly. +*/ +void ScalePartsCommand::adjustText() { + if (scaled_primitives_.count() == 1) { + setText(QObject::tr("redimensionnement %1", "undo caption -- %1 is the resized primitive type name").arg(scaled_primitives_.first() -> name())); + } else { + setText(QObject::tr("redimensionnement de %1 primitives", "undo caption -- %1 always > 1").arg(scaled_primitives_.count())); + } +} diff --git a/sources/editor/editorcommands.h b/sources/editor/editorcommands.h index 35582b21b..ab077f847 100644 --- a/sources/editor/editorcommands.h +++ b/sources/editor/editorcommands.h @@ -391,4 +391,42 @@ class ChangeInformationsCommand : public ElementEditionCommand { /// New information QString new_informations_; }; + +/** + This command scales primitives when editing an electrical element. +*/ +class ScalePartsCommand : public ElementEditionCommand { + // constructors, destructor + public: + ScalePartsCommand(ElementScene * = 0, QUndoCommand * = 0); + virtual ~ScalePartsCommand(); + private: + ScalePartsCommand(const ScalePartsCommand &); + + // methods + public: + virtual void undo(); + virtual void redo(); + ElementScene *elementScene() const; + void setScaledPrimitives(const QList &); + QList scaledPrimitives() const; + void setTransformation(const QRectF &, const QRectF &); + QPair transformation(); + + protected: + void scale(const QRectF &before, const QRectF &after); + void adjustText(); + + // attributes + private: + /// List of moved primitives + QList scaled_primitives_; + /// original rect items fit in + QRectF original_rect_; + /// new rect items should fit in + QRectF new_rect_; + /// Prevent the first call to redo() + bool first_redo; +}; + #endif diff --git a/sources/editor/elementprimitivedecorator.cpp b/sources/editor/elementprimitivedecorator.cpp new file mode 100644 index 000000000..4ead8e817 --- /dev/null +++ b/sources/editor/elementprimitivedecorator.cpp @@ -0,0 +1,614 @@ +#include "elementprimitivedecorator.h" +#include "elementscene.h" +#include "customelementpart.h" +#include "editorcommands.h" +#include "qet.h" +#include +#include +#include +#include +#include +#include + +/** + Constructor + @param parent Parent QGraphicsItem +*/ +ElementPrimitiveDecorator::ElementPrimitiveDecorator(QGraphicsItem *parent): + QGraphicsObject(parent) +{ + init(); +} + +/** + Destructor +*/ +ElementPrimitiveDecorator::~ElementPrimitiveDecorator() { +} + +/** + @return the internal bouding rect, i.e. the smallest rectangle containing + the bounding rectangle of every selected item. +*/ +QRectF ElementPrimitiveDecorator::internalBoundingRect() const { + if (!decorated_items_.count() || !scene()) return(QRectF()); + + QRectF rect = getSceneBoundingRect(decorated_items_.first() -> toItem()); + foreach (CustomElementPart *item, decorated_items_) { + rect = rect.united(getSceneBoundingRect(item -> toItem())); + } + return(rect); +} + +/** + @return the outer bounds of the decorator as a rectangle. +*/ +QRectF ElementPrimitiveDecorator::boundingRect() const { + const qreal additional_margin = 2.5; + + QRectF rect = effective_bounding_rect_; + rect.adjust(-additional_margin, -additional_margin, additional_margin, additional_margin); + return(rect); +} + +/** + Paint the contents of an item in local coordinates, using \a painter, with + respect to \a option and + @param option The option parameter provides style options for the item, such + as its state, exposed area and its level-of-detail hints. + @param The widget argument is optional. If provided, it points to the + widget that is being painted on; otherwise, it is 0. For cached painting, + widget is always 0. +*/ +void ElementPrimitiveDecorator::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) { + Q_UNUSED(option) + Q_UNUSED(widget) + painter -> save(); + + // paint the original bounding rect + painter -> setPen(Qt::DashLine); + //QGraphicsItemGroup::paint(painter, option, widget); + painter -> drawRect(modified_bounding_rect_); + drawSquares(painter, option, widget); + + // uncomment to draw the real bouding rect (=adjusted internal bounding rect) + // painter -> setBrush(QBrush(QColor(240, 0, 0, 127))); + // painter -> drawRect(boundingRect()); + painter -> restore(); +} + +/** + @param items the new list of items this decorator is suposed to manipulate. +*/ +void ElementPrimitiveDecorator::setItems(const QList &items) { + if (CustomElementPart *single_item = singleItem()) { + if (items.count() == 1 && items.first() == single_item) { + // no actual change + goto end_setItems; + } + + // break any connection between the former single selected item (if any) and + // the decorator + single_item -> setDecorator(0); + if (QGraphicsObject *single_object = dynamic_cast(single_item)) { + disconnect(single_object, 0, this, 0); + } + } + + decorated_items_ = items; + + // when only a single primitive is selected, the decorator behaves specially + // to enable extra features, such as text edition, internal points movements, + // etc. + if (CustomElementPart *single_item = singleItem()) { + single_item -> setDecorator(this); + } + + end_setItems: + adjust(); + show(); + grabKeyboard(); +} + +/** + @param items the new list of items this decorator is suposed to manipulate. +*/ +void ElementPrimitiveDecorator::setItems(const QList &items) { + QList primitives; + foreach (QGraphicsItem *item, items) { + if (CustomElementPart *part_item = dynamic_cast(item)) { + primitives << part_item; + } + } + setItems(primitives); +} + +/** + @return the list of items this decorator is supposed to manipulate +*/ +QList ElementPrimitiveDecorator::items() const { + return(decorated_items_); +} + +/** + @return the list of items this decorator is supposed to manipulate +*/ +QList ElementPrimitiveDecorator::graphicsItems() const { + QList list; + foreach (CustomElementPart *part_item, decorated_items_) { + if (QGraphicsItem *item = dynamic_cast(part_item)) { + list << item; + } + } + return(list); +} + +/** + Adjust the visual decorator according to the currently assigned items. + It is notably called by setItems(). +*/ +void ElementPrimitiveDecorator::adjust() { + saveOriginalBoundingRect(); + modified_bounding_rect_ = original_bounding_rect_; + adjustEffectiveBoundingRect(); +} + +/** + Handle events generated when the mouse hovers over the decorator. + @param event Object describing the hover event. +*/ +void ElementPrimitiveDecorator::hoverMoveEvent(QGraphicsSceneHoverEvent *event) { + QList rects = getResizingSquares(); + QPointF pos = event -> pos(); + + if (rects.at(QET::ResizeFromTopLeftCorner).contains(pos) || rects.at(QET::ResizeFromBottomRightCorner).contains(pos)) { + setCursor(Qt::SizeFDiagCursor); + } else if (rects.at(QET::ResizeFromTopRightCorner).contains(pos) || rects.at(QET::ResizeFromBottomLeftCorner).contains(pos)) { + setCursor(Qt::SizeBDiagCursor); + } else if (rects.at(QET::ResizeFromTopCenterCorner).contains(pos) || rects.at(QET::ResizeFromBottomCenterCorner).contains(pos)) { + setCursor(Qt::SizeVerCursor); + } else if (rects.at(QET::ResizeFromMiddleLeftCorner).contains(pos) || rects.at(QET::ResizeFromMiddleRightCorner).contains(pos)) { + setCursor(Qt::SizeHorCursor); + } else if (internalBoundingRect().contains(pos)) { + setCursor(Qt::SizeAllCursor); + } else { + setCursor(Qt::ArrowCursor); + } +} + +/** + Handle event generated when mouse buttons are pressed. + @param event Object describing the mouse event +*/ +void ElementPrimitiveDecorator::mousePressEvent(QGraphicsSceneMouseEvent *event) { + qDebug() << Q_FUNC_INFO << event << zValue(); + QList rects = getResizingSquares(); + QPointF pos = event -> pos(); + + current_operation_square_ = resizingSquareAtPos(pos); + bool accept = false; + if (current_operation_square_ != QET::NoOperation) { + accept = true; + } else { + if (internalBoundingRect().contains(pos)) { + if (CustomElementPart *single_item = singleItem()) { + bool event_accepted = single_item -> singleItemPressEvent(this, event); + if (event_accepted) { + event -> ignore(); + return; + } + } + current_operation_square_ = QET::MoveArea; + accept = true; + } + } + + if (accept) { + if (current_operation_square_ > QET::NoOperation) { + first_pos_ = latest_pos_ = mapToScene(rects.at(current_operation_square_).center()); + } else { + first_pos_ = decorated_items_.at(0) -> toItem() -> scenePos(); + latest_pos_ = event -> scenePos(); + mouse_offset_ = event -> scenePos() - first_pos_; + } + startMovement(); + event -> accept(); + } else { + event -> ignore(); + } +} + +/** + Handle events generated when mouse buttons are double clicked. + @param event Object describing the mouse event +*/ +void ElementPrimitiveDecorator::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) { + //QGraphicsObject::mouseDoubleClickEvent(event); + if (CustomElementPart *single_item = singleItem()) { + bool event_accepted = single_item -> singleItemDoubleClickEvent(this, event); + if (event_accepted) { + event -> ignore(); + return; + } + } +} + +/** + Handle event generated when the mouse is moved and the decorator is the mouse grabber item. + @param event Object describing the mouse event + @see QGraphicsScene::mouseGrabberItem() +*/ +void ElementPrimitiveDecorator::mouseMoveEvent(QGraphicsSceneMouseEvent *event) { + QList rects = getResizingSquares(); + + QPointF scene_pos = event -> scenePos(); + QPointF movement = scene_pos - latest_pos_; + + // snap the movement to a non-visible grid when scaling selection + if (mustSnapToGrid(event)) { + // We now want some point belonging to the selection to be snapped to this rounded position + if (current_operation_square_ > QET::NoOperation) { + // real, non-rounded movement from the mouse press event + QPointF global_movement = scene_pos - first_pos_; + + // real, rounded movement from the mouse press event + QPointF rounded_global_movement = snapConstPointToGrid(global_movement); + + // rounded position of the current mouse move event + QPointF rounded_scene_pos = first_pos_ + rounded_global_movement; + + // when scaling the selection, consider the center of the currently dragged resizing rectangle + QPointF current_position = mapToScene(rects.at(current_operation_square_).center()); + // determine the final, effective movement + movement = rounded_scene_pos - current_position; + } else if (current_operation_square_ == QET::MoveArea) { + // when moving the selection, consider the position of the first selected item + QPointF current_position = scene_pos - mouse_offset_; + QPointF rounded_current_position = snapConstPointToGrid(current_position); + movement = rounded_current_position - decorated_items_.at(0) -> toItem() -> scenePos(); + } else { + if (CustomElementPart *single_item = singleItem()) { + bool event_accepted = single_item -> singleItemMoveEvent(this, event); + if (event_accepted) { + event -> ignore(); + return; + } + } + } + } + + QRectF bounding_rect = modified_bounding_rect_; + applyMovementToRect(current_operation_square_, movement, modified_bounding_rect_); + if (modified_bounding_rect_ != bounding_rect) { + adjustEffectiveBoundingRect(); + } + latest_pos_ = event -> scenePos(); + + if (current_operation_square_ == QET::MoveArea) { + translateItems(movement); + } else { + scaleItems(original_bounding_rect_, modified_bounding_rect_); + } +} + +/** + Handle event generated when a mouse buttons are releaseis moved and the + decorator is the mouse grabber item. + @param event Object describing the mouse event + @see QGraphicsScene::mouseGrabberItem() +*/ +void ElementPrimitiveDecorator::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) { + Q_UNUSED(event) + + ElementEditionCommand *command = 0; + if (current_operation_square_ > QET::NoOperation) { + ScalePartsCommand *scale_command = new ScalePartsCommand(); + scale_command -> setScaledPrimitives(items()); + scale_command -> setTransformation( + mapToScene(original_bounding_rect_).boundingRect(), + mapToScene(modified_bounding_rect_).boundingRect() + ); + command = scale_command; + } else if (current_operation_square_ == QET::MoveArea) { + QPointF movement = mapToScene(modified_bounding_rect_.topLeft()) - mapToScene(original_bounding_rect_.topLeft()); + if (!movement.isNull()) { + MovePartsCommand *move_command = new MovePartsCommand(movement, 0, graphicsItems()); + command = move_command; + } + } else { + if (CustomElementPart *single_item = singleItem()) { + bool event_accepted = single_item -> singleItemReleaseEvent(this, event); + if (event_accepted) { + event -> ignore(); + return; + } + } + } + + if (command) { + emit(actionFinished(command)); + } + + if (current_operation_square_ != QET::NoOperation) { + adjust(); + } + + current_operation_square_ = QET::NoOperation; +} + +/** + Initialize an ElementPrimitiveDecorator +*/ +void ElementPrimitiveDecorator::init() { + //setAcceptedMouseButtons(Qt::LeftButton); + grid_step_x_ = grid_step_y_ = 1; + setAcceptHoverEvents(true); +} + +/** + Save the original bounding rectangle. +*/ +void ElementPrimitiveDecorator::saveOriginalBoundingRect() { + original_bounding_rect_ = internalBoundingRect(); +} + +/** + Adjust the effective bounding rect. This method should be called after the + modified_bouding_rect_ attribute was modified. +*/ +void ElementPrimitiveDecorator::adjustEffectiveBoundingRect() { + prepareGeometryChange(); + effective_bounding_rect_ = modified_bounding_rect_ | effective_bounding_rect_; + update(); +} + +/** + Start a movement (i.e. either a move or scaling operation) +*/ +void ElementPrimitiveDecorator::startMovement() { + adjust(); + + foreach(CustomElementPart *item, decorated_items_) { + qDebug() << Q_FUNC_INFO << "starting movement with rect" << original_bounding_rect_ << "i.e. in scene coordinates " << mapToScene(original_bounding_rect_).boundingRect(); + item -> startUserTransformation(mapToScene(original_bounding_rect_).boundingRect()); + } +} + +/** + Apply the movement described by \a movement_type and \a movement to \a rect. +*/ +void ElementPrimitiveDecorator::applyMovementToRect(int movement_type, const QPointF &movement, QRectF &rect) { + qreal new_value; + QPointF new_point; + + switch (movement_type) { + case QET::MoveArea: + rect.translate(movement.x(), movement.y()); + break; + case QET::ResizeFromTopLeftCorner: + new_point = rect.topLeft() + movement; + rect.setTopLeft(new_point); + break; + case QET::ResizeFromTopCenterCorner: + new_value = rect.top() + movement.y(); + rect.setTop(new_value); + break; + case QET::ResizeFromTopRightCorner: + new_point = rect.topRight() + movement; + rect.setTopRight(new_point); + break; + case QET::ResizeFromMiddleLeftCorner: + new_value = rect.left() + movement.x(); + rect.setLeft(new_value); + break; + case QET::ResizeFromMiddleRightCorner: + new_value = rect.right() + movement.x(); + rect.setRight(new_value); + break; + case QET::ResizeFromBottomLeftCorner: + new_point = rect.bottomLeft() + movement; + rect.setBottomLeft(new_point); + break; + case QET::ResizeFromBottomCenterCorner: + new_value = rect.bottom() + movement.y(); + rect.setBottom(new_value); + break; + case QET::ResizeFromBottomRightCorner: + new_point = rect.bottomRight() + movement; + rect.setBottomRight(new_point); + break; + } +} + +CustomElementPart *ElementPrimitiveDecorator::singleItem() const { + if (decorated_items_.count() == 1) { + return(decorated_items_.first()); + } + return(0); +} + +/** + Translated the managed items by the \a movement +*/ +void ElementPrimitiveDecorator::translateItems(const QPointF &movement) { + if (!decorated_items_.count()) return; + + foreach(QGraphicsItem *qgi, graphicsItems()) { + // this is a naive, proof-of-concept implementation; we actually need to take + // the grid into account and create a command object in mouseReleaseEvent() + qgi -> moveBy(movement.x(), movement.y()); + } +} + + +/** + Scale the managed items, provided they originally fit within \a + original_rect and they should now fit \a new_rect +*/ +void ElementPrimitiveDecorator::scaleItems(const QRectF &original_rect, const QRectF &new_rect) { + if (!decorated_items_.count()) return; + if (original_rect == new_rect) return; + if (!original_rect.width() || !original_rect.height()) return; // cowardly flee division by zero FIXME? + + QRectF scene_original_rect = mapToScene(original_rect).boundingRect(); + QRectF scene_new_rect = mapToScene(new_rect).boundingRect(); + qDebug() << Q_FUNC_INFO << "from " << original_rect << " to " << new_rect; + qDebug() << Q_FUNC_INFO << "from " << scene_original_rect << " to " << scene_new_rect << "(scene coordinates)"; + + foreach(CustomElementPart *item, decorated_items_) { + item -> handleUserTransformation(scene_original_rect, scene_new_rect); + } +} + +/** + @return the bounding rectangle of \a item, in scene coordinates +*/ +QRectF ElementPrimitiveDecorator::getSceneBoundingRect(QGraphicsItem *item) const { + if (!item) return(QRectF()); + return(item -> mapRectToScene(item -> boundingRect())); +} + +/** + Draw all known resizing squares using \a painter. + @param option The option parameter provides style options for the item, such + as its state, exposed area and its level-of-detail hints. + @param The widget argument is optional. If provided, it points to the + widget that is being painted on; otherwise, it is 0. For cached painting, + widget is always 0. +*/ +void ElementPrimitiveDecorator::drawSquares(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) { + foreach (QRectF rect, getResizingSquares()) { + drawResizeSquare(painter, option, widget, rect); + } +} + +/** + Draw the provided resizing square \a rect using \a painter. + @param option The option parameter provides style options for the item, such + as its state, exposed area and its level-of-detail hints. + @param The widget argument is optional. If provided, it points to the + widget that is being painted on; otherwise, it is 0. For cached painting, + widget is always 0. +*/ +void ElementPrimitiveDecorator::drawResizeSquare(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget, const QRectF &rect) { + QColor inner(0xFF, 0xFF, 0xFF); + QColor outer(0x00, 0x61, 0xFF); + if (decorated_items_.count() > 1) { + outer = QColor(0x1A, 0x5C, 0x14); + } + drawGenericSquare(painter, option, widget, rect, inner, outer); +} + +/** + Draw a generic square \a rect using \a painter. + @param inner Color used to fill the square + @param outer Color usd to draw the outline + @param option The option parameter provides style options for the item, such + as its state, exposed area and its level-of-detail hints. + @param The widget argument is optional. If provided, it points to the + widget that is being painted on; otherwise, it is 0. For cached painting, + widget is always 0. +*/ +void ElementPrimitiveDecorator::drawGenericSquare(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget, const QRectF &rect, const QColor &inner, const QColor &outer) { + Q_UNUSED(option) + Q_UNUSED(widget) + // 1.0px will end up to level_of_details px once rendered + // qreal level_of_details = option->levelOfDetailFromTransform(painter -> transform()); + + painter -> save(); + painter -> setBrush(QBrush(inner)); + QPen square_pen(QBrush(outer), 2.0, Qt::SolidLine, Qt::SquareCap, Qt::MiterJoin); + square_pen.setCosmetic(true); + painter -> setPen(square_pen); + painter -> drawRect(rect); + painter -> restore(); +} + +/** + @return A list containing all known resizing squares, based on the + modified_bounding_rect_ attribute. They are ordered following + QET::OperationAreas, so it is possible to use positive values from this enum + to fetch the corresponding square in the list. + @see QET::OperationAreas +*/ +QList ElementPrimitiveDecorator::getResizingSquares() { + QRectF primitive_rect = modified_bounding_rect_; + QRectF half_primitive_rect1 = primitive_rect; + half_primitive_rect1.setHeight(half_primitive_rect1.height() / 2.0); + QRectF half_primitive_rect2 = primitive_rect; + half_primitive_rect2.setWidth(half_primitive_rect2.width() / 2.0); + + QList rects; + + rects << getGenericSquare(primitive_rect.topLeft()); + rects << getGenericSquare(half_primitive_rect2.topRight()); + rects << getGenericSquare(primitive_rect.topRight()); + rects << getGenericSquare(half_primitive_rect1.bottomLeft()); + rects << getGenericSquare(half_primitive_rect1.bottomRight()); + rects << getGenericSquare(primitive_rect.bottomLeft()); + rects << getGenericSquare(half_primitive_rect2.bottomRight()); + rects << getGenericSquare(primitive_rect.bottomRight()); + + /// TODO cache the rects instead of calculating them again and again? + return(rects); +} + +/** + @return the square to be drawn to represent \a position +*/ +QRectF ElementPrimitiveDecorator::getGenericSquare(const QPointF &position) { + const qreal square_half_size = 0.5; + return( + QRectF( + position.x() - square_half_size, + position.y() - square_half_size, + square_half_size * 2.0, + square_half_size * 2.0 + ) + ); +} + +/** + @return the index of the square containing the \a position point or -1 if + none matches. +*/ +int ElementPrimitiveDecorator::resizingSquareAtPos(const QPointF &position) { + QList rects = getResizingSquares(); + int current_square = QET::NoOperation; + for (int i = 0 ; i < rects.count() ; ++ i) { + if (rects.at(i).contains(position)) { + current_square = i; + } + } + return(current_square); +} + +/** + Round the coordinates of \a point so it is snapped to the grid defined by the + grid_step_x_ and grid_step_y_ attributes. +*/ +QPointF ElementPrimitiveDecorator::snapConstPointToGrid(const QPointF &point) const { + return( + QPointF( + qRound(point.x() / grid_step_x_) * grid_step_x_, + qRound(point.y() / grid_step_y_) * grid_step_y_ + ) + ); +} + +/** + Round the coordinates of \a point so it is snapped to the grid defined by the + grid_step_x_ and grid_step_y_ attributes. +*/ +void ElementPrimitiveDecorator::snapPointToGrid(QPointF &point) const { + point.rx() = qRound(point.x() / grid_step_x_) * grid_step_x_; + point.ry() = qRound(point.y() / grid_step_y_) * grid_step_y_; +} + +/** + @return whether the current operation should take the grid into account + according to the state of the provided \a event +*/ +bool ElementPrimitiveDecorator::mustSnapToGrid(QGraphicsSceneMouseEvent *event) { + return(!(event -> modifiers() & Qt::ControlModifier)); +} diff --git a/sources/editor/elementprimitivedecorator.h b/sources/editor/elementprimitivedecorator.h new file mode 100644 index 000000000..17f5a342c --- /dev/null +++ b/sources/editor/elementprimitivedecorator.h @@ -0,0 +1,90 @@ +#ifndef ELEMENTPRIMITIVEDECORATOR_H +#define ELEMENTPRIMITIVEDECORATOR_H +#include +class ElementEditionCommand; +class ElementScene; +class CustomElementPart; + +/** + This class represents a decorator rendered above selected items so users + can manipulate (move, resize, ...) them. + + The implementation considers four kinds of bounding rects: + - the actual, effective bounding rect as returned by the boundingRect() method + - the original bounding rect, i.e. the rect containing all selected items at + the beginning of operations (or after a command object was generated) + - the new bounding rect, after the user moved or resized items + - the former bounding rect, due to implementation details +*/ +class ElementPrimitiveDecorator : public QGraphicsObject { + Q_OBJECT + + public: + ElementPrimitiveDecorator(QGraphicsItem * = 0); + virtual ~ElementPrimitiveDecorator(); + + enum { Type = UserType + 2200 }; + + // methods + QRectF internalBoundingRect() const; + virtual QRectF boundingRect () const; + virtual void paint(QPainter *, const QStyleOptionGraphicsItem *, QWidget * = 0); + virtual int type() const { return Type; } + void setItems(const QList &); + void setItems(const QList &); + QList items() const; + QList graphicsItems() const; + + public slots: + void adjust(); + + signals: + void actionFinished(ElementEditionCommand *); + + protected: + void hoverMoveEvent(QGraphicsSceneHoverEvent *); + void mousePressEvent(QGraphicsSceneMouseEvent *); + void mouseDoubleClickEvent(QGraphicsSceneMouseEvent *); + void mouseMoveEvent(QGraphicsSceneMouseEvent *); + void mouseReleaseEvent(QGraphicsSceneMouseEvent *); + QPointF snapConstPointToGrid(const QPointF &) const; + void snapPointToGrid(QPointF &) const; + bool mustSnapToGrid(QGraphicsSceneMouseEvent *); + + private: + void init(); + void saveOriginalBoundingRect(); + void adjustEffectiveBoundingRect(); + void startMovement(); + void applyMovementToRect(int, const QPointF &, QRectF &); + CustomElementPart *singleItem() const; + void translateItems(const QPointF &); + void scaleItems(const QRectF &, const QRectF &); + QRectF getSceneBoundingRect(QGraphicsItem *) const; + void drawSquares(QPainter *, const QStyleOptionGraphicsItem *, QWidget *); + void drawResizeSquare(QPainter *, const QStyleOptionGraphicsItem *, QWidget *, const QRectF &); + void drawGenericSquare(QPainter *, const QStyleOptionGraphicsItem *, QWidget *, const QRectF &, const QColor &, const QColor &); + QList getResizingSquares(); + QRectF getGenericSquare(const QPointF &); + int resizingSquareAtPos(const QPointF &); + + // attributes + private: + QList decorated_items_; + QRectF effective_bounding_rect_; ///< actual, effective bounding rect -- never shrinks + QRectF original_bounding_rect_; ///< original bounding rect + QRectF modified_bounding_rect_; ///< new bounding rect, after the user moved or resized items + + /** + Index of the square leading the current operation (resizing, etc.) or -1 if no + operation is occurring, -2 for a move operation. + */ + int current_operation_square_; + int grid_step_x_; ///< Grid horizontal step + int grid_step_y_; ///< Grid horizontal step + QPointF first_pos_; ///< First point involved within the current resizing operation + QPointF latest_pos_; ///< Latest point involved within the current resizing operation + QPointF mouse_offset_; ///< Offset between the mouse position and the point to be snapped to grid when moving selection +}; + +#endif diff --git a/sources/editor/elementscene.cpp b/sources/editor/elementscene.cpp index 6104fab3f..44bec8961 100644 --- a/sources/editor/elementscene.cpp +++ b/sources/editor/elementscene.cpp @@ -17,6 +17,7 @@ */ #include "elementscene.h" #include "qetelementeditor.h" +#include "elementprimitivedecorator.h" #include #include "partline.h" #include "partrectangle.h" @@ -44,17 +45,22 @@ ElementScene::ElementScene(QETElementEditor *editor, QObject *parent) : _hotspot(15, 35), internal_connections(false), qgi_manager(this), - element_editor(editor) + element_editor(editor), + decorator_(0) { setItemIndexMethod(NoIndex); current_polygon = NULL; setGrid(1, 1); initPasteArea(); undo_stack.setClean(); + decorator_lock_ = new QMutex(QMutex::NonRecursive); + connect(&undo_stack, SIGNAL(indexChanged(int)), this, SLOT(managePrimitivesGroups())); + connect(this, SIGNAL(selectionChanged()), this, SLOT(managePrimitivesGroups())); } /// Destructeur ElementScene::~ElementScene() { + delete decorator_lock_; } /** @@ -187,25 +193,7 @@ void ElementScene::mouseMoveEvent(QGraphicsSceneMouseEvent *e) { break; case Normal: default: - QList selected_items = selectedItems(); - if (!selected_items.isEmpty()) { - // mouvement de souris realise depuis le dernier press event - QPointF mouse_movement = e -> scenePos() - moving_press_pos; - - // application de ce mouvement a la fsi_pos enregistre dans le dernier press event - QPointF new_fsi_pos = fsi_pos + mouse_movement; - - // snap eventuel de la nouvelle fsi_pos - if (mustSnapToGrid(e)) snapToGrid(new_fsi_pos); - - // difference entre la fsi_pos finale et la fsi_pos courante = mouvement a appliquer - - QPointF current_fsi_pos = selected_items.first() -> scenePos(); - QPointF final_movement = new_fsi_pos - current_fsi_pos; - foreach(QGraphicsItem *qgi, selected_items) { - qgi -> moveBy(final_movement.x(), final_movement.y()); - } - } + QGraphicsScene::mouseMoveEvent(e); } } else if (behavior == Polygon && current_polygon != NULL) { temp_polygon = current_polygon -> polygon(); @@ -263,16 +251,6 @@ void ElementScene::mousePressEvent(QGraphicsSceneMouseEvent *e) { case Normal: default: QGraphicsScene::mousePressEvent(e); - // gestion des deplacements de parties - if (!selectedItems().isEmpty()) { - fsi_pos = selectedItems().first() -> scenePos(); - moving_press_pos = e -> scenePos(); - moving_parts_ = true; - } else { - fsi_pos = QPoint(); - moving_press_pos = QPoint(); - moving_parts_ = false; - } } } else QGraphicsScene::mousePressEvent(e); } @@ -358,12 +336,6 @@ void ElementScene::mouseReleaseEvent(QGraphicsSceneMouseEvent *e) { case Normal: default: // detecte les deplacements de parties - if (!selectedItems().isEmpty() && moving_parts_) { - QPointF movement = selectedItems().first() -> scenePos() - fsi_pos; - if (!movement.isNull()) { - undo_stack.push(new MovePartsCommand(movement, this, selectedItems())); - } - } QGraphicsScene::mouseReleaseEvent(e); moving_parts_ = false; } @@ -657,7 +629,7 @@ QRectF ElementScene::borderRect() const { */ QRectF ElementScene::sceneContent() const { qreal adjustment = 5.0; - return(itemsBoundingRect().unite(borderRect()).adjusted(-adjustment, -adjustment, adjustment, adjustment)); + return(elementContentBoundingRect(items()).unite(borderRect()).adjusted(-adjustment, -adjustment, adjustment, adjustment)); } /** @@ -666,7 +638,7 @@ QRectF ElementScene::sceneContent() const { l'element. */ bool ElementScene::borderContainsEveryParts() const { - return(borderRect().contains(itemsBoundingRect())); + return(borderRect().contains(elementContentBoundingRect(items()))); } /** @@ -800,7 +772,10 @@ void ElementScene::slot_delete() { // efface tout ce qui est selectionne undo_stack.push(new DeletePartsCommand(this, selected_items)); + + // removing items does not trigger QGraphicsScene::selectionChanged() emit(partsRemoved()); + emit(selectionChanged()); } /** @@ -825,7 +800,7 @@ void ElementScene::slot_editSizeHotSpot() { hotspot_editor -> setElementHeight(static_cast(height() / 10)); hotspot_editor -> setHotspot(hotspot()); hotspot_editor -> setOldHotspot(hotspot()); - hotspot_editor -> setPartsRect(itemsBoundingRect()); + hotspot_editor -> setPartsRect(elementContentBoundingRect(items())); hotspot_editor -> setPartsRectEnabled(true); hotspot_editor -> setReadOnly(is_read_only); dialog_layout -> addWidget(hotspot_editor); @@ -1021,6 +996,19 @@ void ElementScene::slot_sendBackward() { emit(partsZValueChanged()); } +/** + @return the list of primitives currently present on the scene. +*/ +QList ElementScene::primitives() const { + QList primitives_list; + foreach (QGraphicsItem *item, items()) { + if (CustomElementPart *primitive = dynamic_cast(item)) { + primitives_list << primitive; + } + } + return(primitives_list); +} + /** @param include_terminals true pour inclure les bornes, false sinon @return les parties de l'element ordonnes par zValue croissante @@ -1032,7 +1020,13 @@ QList ElementScene::zItems(bool include_terminals) const { // enleve les bornes QList terminals; foreach(QGraphicsItem *qgi, all_items_list) { - if (qgraphicsitem_cast(qgi)) { + if ( + qgi -> type() == ElementPrimitiveDecorator::Type || + qgi -> type() == QGraphicsRectItem::Type + ) { + all_items_list.removeAt(all_items_list.indexOf(qgi)); + } + else if (qgraphicsitem_cast(qgi)) { all_items_list.removeAt(all_items_list.indexOf(qgi)); terminals << qgi; } @@ -1089,9 +1083,12 @@ void ElementScene::reset() { @return le boundingRect de ces parties, exprime dans les coordonnes de la scene */ -QRectF ElementScene::elementContentBoundingRect(const ElementContent &content) { +QRectF ElementScene::elementContentBoundingRect(const ElementContent &content) const { QRectF bounding_rect; foreach(QGraphicsItem *qgi, content) { + // skip non-primitives QGraphicsItems (paste area, selection decorator) + if (qgi -> type() == ElementPrimitiveDecorator::Type) continue; + if (qgi -> type() == QGraphicsRectItem::Type) continue; bounding_rect |= qgi -> sceneBoundingRect(); } return(bounding_rect); @@ -1225,7 +1222,7 @@ ElementContent ElementScene::loadContent(const QDomDocument &xml_document, QStri ElementContent ElementScene::addContent(const ElementContent &content, QString *error_message) { Q_UNUSED(error_message); foreach(QGraphicsItem *part, content) { - addItem(part); + addPrimitive(part); } return(content); } @@ -1249,11 +1246,20 @@ ElementContent ElementScene::addContentAtPos(const ElementContent &content, cons // ajoute les parties avec le decalage adequat foreach(QGraphicsItem *part, content) { part -> setPos(part -> pos() + offset); - addItem(part); + addPrimitive(part); } return(content); } +/** + Add a primitive to the scene by wrapping it within an + ElementPrimitiveDecorator group. +*/ +void ElementScene::addPrimitive(QGraphicsItem *primitive) { + if (!primitive) return; + addItem(primitive); +} + /** Initialise la zone de collage */ @@ -1300,3 +1306,61 @@ bool ElementScene::mustSnapToGrid(QGraphicsSceneMouseEvent *e) { bool ElementScene::zValueLessThan(QGraphicsItem *item1, QGraphicsItem *item2) { return(item1-> zValue() < item2 -> zValue()); } + +/** + Ensure the decorator is adequately shown, hidden or updated so it always + represents the current selection. +*/ +void ElementScene::managePrimitivesGroups() { + if (!decorator_lock_ -> tryLock()) return; + + if (!decorator_) { + decorator_ = new ElementPrimitiveDecorator(); + connect(decorator_, SIGNAL(actionFinished(ElementEditionCommand*)), this, SLOT(stackAction(ElementEditionCommand *))); + addItem(decorator_); + decorator_ -> hide(); + } + + QList selected_items; + foreach (QGraphicsItem *item, items()) { + if (item -> type() == ElementPrimitiveDecorator::Type) continue; + if (item -> type() == QGraphicsRectItem::Type) continue; + if (item -> isSelected()) { + selected_items << item; + } + } + /// TODO export the above code to a proper method + + + // should we hide the decorator? + if (!selected_items.count()) { + decorator_ -> hide(); + } else { + decorator_ -> setZValue(1000000); + decorator_ -> setPos(0, 0); + decorator_ -> setItems(selected_items); + } + decorator_lock_ -> unlock(); +} + +/** + Push the provided \a command on the undo stack. +*/ +void ElementScene::stackAction(ElementEditionCommand *command) { + if (command -> elementScene()) { + if (command -> elementScene() != this) return; + } else { + command -> setElementScene(this); + } + + if (!command -> elementView()) { + foreach (QGraphicsView *view, views()) { + if (ElementView *element_view = dynamic_cast(view)) { + command -> setElementView(element_view); + break; + } + } + } + + undoStack().push(command); +} diff --git a/sources/editor/elementscene.h b/sources/editor/elementscene.h index 300acd3ec..f13b187c9 100644 --- a/sources/editor/elementscene.h +++ b/sources/editor/elementscene.h @@ -23,6 +23,9 @@ #include "orientationsetwidget.h" #include "qgimanager.h" #include "elementcontent.h" +class CustomElementPart; +class ElementEditionCommand; +class ElementPrimitiveDecorator; class QETElementEditor; class PartLine; class PartRectangle; @@ -95,6 +98,9 @@ class ElementScene : public QGraphicsScene { /// Variables to handle copy/paste with offset QString last_copied_; + /// Decorator item displayed when at least one item is selected + ElementPrimitiveDecorator *decorator_; + ///< Size of the horizontal grid step int x_grid; ///< Size of the vertical grid step @@ -123,6 +129,7 @@ class ElementScene : public QGraphicsScene { virtual QRectF boundingRectFromXml(const QDomDocument &); virtual void fromXml(const QDomDocument &, const QPointF & = QPointF(), bool = true, ElementContent * = 0); virtual void reset(); + virtual QList primitives() const; virtual QList zItems(bool = false) const; virtual ElementContent selectedContent() const; virtual void getPasteArea(const QRectF &); @@ -149,15 +156,17 @@ class ElementScene : public QGraphicsScene { virtual void endCurrentBehavior(const QGraphicsSceneMouseEvent *); private: - QRectF elementContentBoundingRect(const ElementContent &); + QRectF elementContentBoundingRect(const ElementContent &) const; bool applyInformations(const QDomDocument &, QString * = 0); ElementContent loadContent(const QDomDocument &, QString * = 0); ElementContent addContent(const ElementContent &, QString * = 0); ElementContent addContentAtPos(const ElementContent &, const QPointF &, QString * = 0); + void addPrimitive(QGraphicsItem *); void initPasteArea(); void snapToGrid(QPointF &); bool mustSnapToGrid(QGraphicsSceneMouseEvent *); static bool zValueLessThan(QGraphicsItem *, QGraphicsItem *); + QMutex *decorator_lock_; public slots: void slot_move(); @@ -183,6 +192,8 @@ class ElementScene : public QGraphicsScene { void slot_raise(); void slot_lower(); void slot_sendBackward(); + void managePrimitivesGroups(); + void stackAction(ElementEditionCommand *); signals: /** diff --git a/sources/editor/partarc.cpp b/sources/editor/partarc.cpp index 35a92aabf..aa07cb3e5 100644 --- a/sources/editor/partarc.cpp +++ b/sources/editor/partarc.cpp @@ -29,7 +29,7 @@ PartArc::PartArc(QETElementEditor *editor, QGraphicsItem *parent, QGraphicsScene _angle(-90), start_angle(0) { - setFlags(QGraphicsItem::ItemIsMovable | QGraphicsItem::ItemIsSelectable); + setFlags(QGraphicsItem::ItemIsSelectable); #if QT_VERSION >= 0x040600 setFlag(QGraphicsItem::ItemSendsGeometryChanges, true); #endif diff --git a/sources/editor/partcircle.cpp b/sources/editor/partcircle.cpp index 2fa93f107..acd1ce80d 100644 --- a/sources/editor/partcircle.cpp +++ b/sources/editor/partcircle.cpp @@ -24,7 +24,7 @@ @param scene La scene sur laquelle figure ce cercle */ PartCircle::PartCircle(QETElementEditor *editor, QGraphicsItem *parent, QGraphicsScene *scene) : QGraphicsEllipseItem(parent, scene), CustomElementGraphicPart(editor) { - setFlags(QGraphicsItem::ItemIsMovable | QGraphicsItem::ItemIsSelectable); + setFlags(QGraphicsItem::ItemIsSelectable); #if QT_VERSION >= 0x040600 setFlag(QGraphicsItem::ItemSendsGeometryChanges, true); #endif diff --git a/sources/editor/partellipse.cpp b/sources/editor/partellipse.cpp index fd7fda21b..e913ab0f5 100644 --- a/sources/editor/partellipse.cpp +++ b/sources/editor/partellipse.cpp @@ -24,7 +24,7 @@ @param scene La scene sur laquelle figure cette ellipse */ PartEllipse::PartEllipse(QETElementEditor *editor, QGraphicsItem *parent, QGraphicsScene *scene) : QGraphicsEllipseItem(parent, scene), CustomElementGraphicPart(editor) { - setFlags(QGraphicsItem::ItemIsMovable | QGraphicsItem::ItemIsSelectable); + setFlags(QGraphicsItem::ItemIsSelectable); #if QT_VERSION >= 0x040600 setFlag(QGraphicsItem::ItemSendsGeometryChanges, true); #endif diff --git a/sources/editor/partline.cpp b/sources/editor/partline.cpp index 9c471876a..ab4576f4e 100644 --- a/sources/editor/partline.cpp +++ b/sources/editor/partline.cpp @@ -32,7 +32,7 @@ PartLine::PartLine(QETElementEditor *editor, QGraphicsItem *parent, QGraphicsSce second_end(QET::None), second_length(1.5) { - setFlags(QGraphicsItem::ItemIsMovable | QGraphicsItem::ItemIsSelectable); + setFlags(QGraphicsItem::ItemIsSelectable); #if QT_VERSION >= 0x040600 setFlag(QGraphicsItem::ItemSendsGeometryChanges, true); #endif diff --git a/sources/editor/partpolygon.cpp b/sources/editor/partpolygon.cpp index 2c2d4817a..1ac84839f 100644 --- a/sources/editor/partpolygon.cpp +++ b/sources/editor/partpolygon.cpp @@ -29,7 +29,7 @@ PartPolygon::PartPolygon(QETElementEditor *editor, QGraphicsItem *parent, QGraph CustomElementGraphicPart(editor), closed(false) { - setFlags(QGraphicsItem::ItemIsMovable | QGraphicsItem::ItemIsSelectable); + setFlags(QGraphicsItem::ItemIsSelectable); #if QT_VERSION >= 0x040600 setFlag(QGraphicsItem::ItemSendsGeometryChanges, true); #endif diff --git a/sources/editor/partrectangle.cpp b/sources/editor/partrectangle.cpp index 25ebf7955..e5b2126d6 100644 --- a/sources/editor/partrectangle.cpp +++ b/sources/editor/partrectangle.cpp @@ -24,7 +24,7 @@ @param scene La scene sur laquelle figure ce rectangle */ PartRectangle::PartRectangle(QETElementEditor *editor, QGraphicsItem *parent, QGraphicsScene *scene) : QGraphicsRectItem(parent, scene), CustomElementGraphicPart(editor) { - setFlags(QGraphicsItem::ItemIsMovable | QGraphicsItem::ItemIsSelectable); + setFlags(QGraphicsItem::ItemIsSelectable); #if QT_VERSION >= 0x040600 setFlag(QGraphicsItem::ItemSendsGeometryChanges, true); #endif diff --git a/sources/editor/partterminal.cpp b/sources/editor/partterminal.cpp index 3ffdca7a6..f39cd79c5 100644 --- a/sources/editor/partterminal.cpp +++ b/sources/editor/partterminal.cpp @@ -30,7 +30,7 @@ PartTerminal::PartTerminal(QETElementEditor *editor, QGraphicsItem *parent, QGra _orientation(QET::North) { updateSecondPoint(); - setFlags(QGraphicsItem::ItemIsMovable | QGraphicsItem::ItemIsSelectable); + setFlags(QGraphicsItem::ItemIsSelectable); #if QT_VERSION >= 0x040600 setFlag(QGraphicsItem::ItemSendsGeometryChanges, true); #endif diff --git a/sources/editor/parttext.cpp b/sources/editor/parttext.cpp index b17951ac7..3e89b0c3e 100644 --- a/sources/editor/parttext.cpp +++ b/sources/editor/parttext.cpp @@ -18,6 +18,7 @@ #include "parttext.h" #include "texteditor.h" #include "editorcommands.h" +#include "elementprimitivedecorator.h" #include "elementscene.h" #include "qetapp.h" @@ -29,17 +30,21 @@ */ PartText::PartText(QETElementEditor *editor, QGraphicsItem *parent, ElementScene *scene) : QGraphicsTextItem(parent, scene), - CustomElementPart(editor) + CustomElementPart(editor), + previous_text(), + decorator_(0) { #if QT_VERSION >= 0x040500 document() -> setDocumentMargin(1.0); #endif setDefaultTextColor(Qt::black); setFont(QETApp::diagramTextsFont()); - setFlags(QGraphicsItem::ItemIsMovable | QGraphicsItem::ItemIsSelectable); + real_font_size_ = font().pointSize(); + setFlags(QGraphicsItem::ItemIsSelectable); #if QT_VERSION >= 0x040600 setFlag(QGraphicsItem::ItemSendsGeometryChanges, true); #endif + setAcceptHoverEvents(true); setDefaultTextColor(Qt::black); setPlainText(QObject::tr("T", "default text when adding a text in the element editor")); @@ -63,7 +68,7 @@ void PartText::fromXml(const QDomElement &xml_element) { if (!ok || font_size < 1) font_size = 20; setBlack(xml_element.attribute("color") != "white"); - setFont(QETApp::diagramTextsFont(font_size)); + setProperty("size" , font_size); setPlainText(xml_element.attribute("text")); qreal default_rotation_angle = 0.0; @@ -153,31 +158,24 @@ QPointF PartText::margin() const { } /** - Permet a l'element texte de redevenir deplacable a la fin de l'edition de texte - @param e Le QFocusEvent decrivant la perte de focus + @reimp QGraphicsItem::focusInEvent(QFocusEvent *) + @param e The QFocusEvent object describing the focus gain. + Start text edition when the item gains focus. +*/ +void PartText::focusInEvent(QFocusEvent *e) { + startEdition(); + QGraphicsTextItem::focusInEvent(e); +} + + +/** + @reimp QGraphicsItem::focusOutEvent(QFocusEvent *) + @param e The QFocusEvent object describing the focus loss. + End text edition when the item loses focus. */ void PartText::focusOutEvent(QFocusEvent *e) { QGraphicsTextItem::focusOutEvent(e); - if (previous_text != toPlainText()) { - undoStack().push( - new ChangePartCommand( - TextEditor::tr("contenu") + " " + name(), - this, - "text", - previous_text, - toPlainText() - ) - ); - previous_text = toPlainText(); - } - - // deselectionne le texte - QTextCursor qtc = textCursor(); - qtc.clearSelection(); - setTextCursor(qtc); - - setTextInteractionFlags(Qt::NoTextInteraction); - setFlag(QGraphicsItem::ItemIsFocusable, false); + endEdition(); } /** @@ -185,11 +183,10 @@ void PartText::focusOutEvent(QFocusEvent *e) { @param e Le QGraphicsSceneMouseEvent qui decrit le double-clic */ void PartText::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *e) { - setFlag(QGraphicsItem::ItemIsFocusable, true); - setTextInteractionFlags(Qt::TextEditorInteraction); - previous_text = toPlainText(); QGraphicsTextItem::mouseDoubleClickEvent(e); - setFocus(Qt::MouseFocusReason); + if (e -> button() == Qt::LeftButton) { + setEditable(true); + } } /** @@ -212,6 +209,11 @@ void PartText::setProperty(const QString &property, const QVariant &value) { } else if (property == "size") { if (!value.canConvert(QVariant::Int)) return; setFont(QETApp::diagramTextsFont(value.toInt())); + real_font_size_ = value.toInt(); + } else if (property == "real_size") { + if (!value.canConvert(QVariant::Double)) return; + setFont(QETApp::diagramTextsFont(value.toInt())); + real_font_size_ = value.toDouble(); } else if (property == "text") { setPlainText(value.toString()); } else if (property == "rotation angle") { @@ -241,6 +243,8 @@ QVariant PartText::property(const QString &property) { return(pos().y()); } else if (property == "size") { return(font().pointSize()); + } else if (property == "real_size") { + return(real_font_size_); } else if (property == "text") { return(toPlainText()); } else if (property == "rotation angle") { @@ -292,7 +296,7 @@ bool PartText::isUseless() const { void PartText::startUserTransformation(const QRectF &rect) { Q_UNUSED(rect) saved_point_ = pos(); // scene coordinates, no need to mapFromScene() - saved_font_size_ = font().pointSize(); + saved_font_size_ = real_font_size_; } /** @@ -303,12 +307,10 @@ void PartText::handleUserTransformation(const QRectF &initial_selection_rect, co QPointF new_pos = mapPoints(initial_selection_rect, new_selection_rect, QList() << saved_point_).first(); setPos(new_pos); - // adjust the font size following the smallest scale factor - qreal sx = new_selection_rect.width() / initial_selection_rect.width(); + // adjust the font size following the vertical scale factor qreal sy = new_selection_rect.height() / initial_selection_rect.height(); - qreal smallest_scale_factor = sx > sy ? sy : sx; - qreal new_font_size = saved_font_size_ * smallest_scale_factor; - setProperty("size", qMax(1, qRound(new_font_size))); + qreal new_font_size = saved_font_size_ * sy; + setProperty("real_size", qMax(1, qRound(new_font_size))); } /** @@ -318,7 +320,12 @@ void PartText::handleUserTransformation(const QRectF &initial_selection_rect, co @param widget Widget sur lequel on dessine (facultatif) */ void PartText::paint(QPainter *painter, const QStyleOptionGraphicsItem *qsogi, QWidget *widget) { - QGraphicsTextItem::paint(painter, qsogi, widget); + // According to the source code of QGraphicsTextItem::paint(), this should + // avoid the drawing of the dashed rectangle around the text. + QStyleOptionGraphicsItem our_qsogi(*qsogi); + our_qsogi.state = QStyle::State_None; + + QGraphicsTextItem::paint(painter, &our_qsogi, widget); #ifdef QET_DEBUG_EDITOR_TEXTS painter -> setPen(Qt::blue); @@ -332,6 +339,124 @@ void PartText::paint(QPainter *painter, const QStyleOptionGraphicsItem *qsogi, Q #endif } +/** + Handle context menu events. + @param event Object describing the context menu event to handle. +*/ +void PartText::contextMenuEvent(QGraphicsSceneContextMenuEvent *event) { + Q_UNUSED(event); +} + +/** + Handle events generated when the mouse hovers over the decorator. + @param event Object describing the hover event. +*/ +void PartText::hoverMoveEvent(QGraphicsSceneHoverEvent *event) { + // force the cursor when the text is being edited + if (hasFocus() && decorator_) { + decorator_ -> setCursor(Qt::IBeamCursor); + } + QGraphicsTextItem::hoverMoveEvent(event); +} + +/** + @reimp CustomElementPart::setDecorator(ElementPrimitiveDecorator *) + Install or remove a sceneEventFilter on the decorator and ensure it will + adjust itself while the text is being edited. +*/ +void PartText::setDecorator(ElementPrimitiveDecorator *decorator) { + if (decorator) { + decorator -> installSceneEventFilter(this); + // ensure the decorator will adjust itself when the text area expands or shrinks + connect(document(), SIGNAL(contentsChanged()), decorator, SLOT(adjust())); + } + else { + decorator_ -> removeSceneEventFilter(this); + endEdition(); + } + decorator_ = decorator; +} + +/** + @reimp QGraphicsItem::sceneEventFilter(QGraphicsItem *, QEvent *). + Intercepts events before they reach the watched target, i.e. typically the + primitives decorator. + This method mainly works with key strokes (F2, escape) and double clicks to + begin or end text edition. +*/ +bool PartText::sceneEventFilter(QGraphicsItem *watched, QEvent *event) { + if (watched != decorator_) return(false); + + QPointF event_scene_pos = QET::graphicsSceneEventPos(event); + if (!event_scene_pos.isNull()) { + if (contains(mapFromScene(event_scene_pos))) { + if (hasFocus()) { + return sceneEvent(event); // manually deliver the event to this item + return(true); // prevent this event from being delivered to any item + } else { + if (event -> type() == QEvent::GraphicsSceneMouseDoubleClick) { + mouseDoubleClickEvent(static_cast(event)); + } + } + } + } + else if (event -> type() == QEvent::KeyRelease || event -> type() == QEvent::KeyPress) { + // Intercept F2 and escape keystrokes to focus in and out + QKeyEvent *key_event = static_cast(event); + if (key_event -> key() == Qt::Key_F2) { + setEditable(true); + QTextCursor qtc = textCursor(); + qtc.setPosition(qMax(0, document()->characterCount() - 1)); + setTextCursor(qtc); + } else if (hasFocus() && key_event -> key() == Qt::Key_Escape) { + endEdition(); + } + sceneEvent(event); // manually deliver the event to this item + return(true); // prevent this event from being delivered to any item + } + return(false); +} + +/** + Accept the mouse \a event relayed by \a decorator if this text item has focus. +*/ +bool PartText::singleItemPressEvent(ElementPrimitiveDecorator *decorator, QGraphicsSceneMouseEvent *event) { + Q_UNUSED(decorator) + Q_UNUSED(event) + return(hasFocus()); +} + +/** + Accept the mouse \a event relayed by \a decorator if this text item has focus. +*/ +bool PartText::singleItemMoveEvent(ElementPrimitiveDecorator *decorator, QGraphicsSceneMouseEvent *event) { + Q_UNUSED(decorator) + Q_UNUSED(event) + return(hasFocus()); +} + +/** + Accept the mouse \a event relayed by \a decorator if this text item has focus. +*/ +bool PartText::singleItemReleaseEvent(ElementPrimitiveDecorator *decorator, QGraphicsSceneMouseEvent *event) { + Q_UNUSED(decorator) + Q_UNUSED(event) + return(hasFocus()); +} + +/** + Accept the mouse \a event relayed by \a decorator if this text item has focus. +*/ +bool PartText::singleItemDoubleClickEvent(ElementPrimitiveDecorator *decorator, QGraphicsSceneMouseEvent *event) { + Q_UNUSED(decorator) + // calling mouseDoubleClickEvent() will set this text item editable and grab keyboard focus + if (event -> button() == Qt::LeftButton) { + mouseDoubleClickEvent(event); + return(true); + } + return(false); +} + /** Cette methode s'assure que la position du champ de texte est coherente en repositionnant son origine (c-a-d le milieu du bord gauche du champ de @@ -349,6 +474,59 @@ void PartText::adjustItemPosition(int new_block_count) { setTransformOriginPoint(origin_offset); } +/** + @param editable Whether this text item should be interactively editable. +*/ +void PartText::setEditable(bool editable) { + if (editable) { + setFlag(QGraphicsItem::ItemIsFocusable, true); + setTextInteractionFlags(Qt::TextEditorInteraction); + setFocus(Qt::MouseFocusReason); + } + else { + setTextInteractionFlags(Qt::NoTextInteraction); + setFlag(QGraphicsItem::ItemIsFocusable, false); + } +} + +/** + Start text edition by storing the former value of the text. +*/ +void PartText::startEdition() { + // !previous_text.isNull() means the text is being edited + previous_text = toPlainText(); +} + +/** + End text edition, potentially generating a ChangePartCommand if the text + has changed. +*/ +void PartText::endEdition() { + if (!previous_text.isNull()) { + // the text was being edited + QString new_text = toPlainText(); + if (previous_text != new_text) { + // the text was changed + ChangePartCommand *text_change = new ChangePartCommand( + TextEditor::tr("contenu") + " " + name(), + this, + "text", + previous_text, + new_text + ); + previous_text = QString(); + undoStack().push(text_change); + } + } + + // deselectionne le texte + QTextCursor qtc = textCursor(); + qtc.clearSelection(); + setTextCursor(qtc); + + setEditable(false); +} + #ifdef QET_DEBUG_EDITOR_TEXTS /** Dessine deux petites fleches pour mettre un point en valeur diff --git a/sources/editor/parttext.h b/sources/editor/parttext.h index 801c49c60..caf095a8f 100644 --- a/sources/editor/parttext.h +++ b/sources/editor/parttext.h @@ -20,6 +20,7 @@ #include #include "customelementpart.h" class TextEditor; +class ElementPrimitiveDecorator; /** This class represents an static text primitive which may be used to compose the drawing of an electrical element within the element editor. @@ -59,10 +60,23 @@ class PartText : public QGraphicsTextItem, public CustomElementPart { virtual void handleUserTransformation(const QRectF &, const QRectF &); virtual void paint(QPainter *, const QStyleOptionGraphicsItem *, QWidget * = 0 ); + virtual void setDecorator(ElementPrimitiveDecorator *); + virtual bool singleItemPressEvent(ElementPrimitiveDecorator *, QGraphicsSceneMouseEvent *); + virtual bool singleItemMoveEvent(ElementPrimitiveDecorator *, QGraphicsSceneMouseEvent *); + virtual bool singleItemReleaseEvent(ElementPrimitiveDecorator *, QGraphicsSceneMouseEvent *); + virtual bool singleItemDoubleClickEvent(ElementPrimitiveDecorator *, QGraphicsSceneMouseEvent *); + public slots: void adjustItemPosition(int = 0); + void setEditable(bool); + void startEdition(); + void endEdition(); protected: + virtual void contextMenuEvent(QGraphicsSceneContextMenuEvent *); + virtual void hoverMoveEvent(QGraphicsSceneHoverEvent *); + virtual bool sceneEventFilter(QGraphicsItem *, QEvent *); + virtual void focusInEvent(QFocusEvent *); virtual void focusOutEvent(QFocusEvent *); virtual void mouseDoubleClickEvent(QGraphicsSceneMouseEvent *); virtual QVariant itemChange(GraphicsItemChange, const QVariant &); @@ -74,7 +88,9 @@ class PartText : public QGraphicsTextItem, public CustomElementPart { void drawPoint(QPainter *, const QPointF &); #endif QString previous_text; + qreal real_font_size_; QPointF saved_point_; - int saved_font_size_; + qreal saved_font_size_; + QGraphicsItem *decorator_; }; #endif diff --git a/sources/editor/parttextfield.cpp b/sources/editor/parttextfield.cpp index 208a8de80..7e8306244 100644 --- a/sources/editor/parttextfield.cpp +++ b/sources/editor/parttextfield.cpp @@ -18,6 +18,7 @@ #include "parttextfield.h" #include "textfieldeditor.h" #include "editorcommands.h" +#include "elementprimitivedecorator.h" #include "qetapp.h" /** @@ -29,14 +30,18 @@ PartTextField::PartTextField(QETElementEditor *editor, QGraphicsItem *parent, QGraphicsScene *scene) : QGraphicsTextItem(parent, scene), CustomElementPart(editor), - follow_parent_rotations(true) + follow_parent_rotations(true), + previous_text(), + decorator_(0) { setDefaultTextColor(Qt::black); setFont(QETApp::diagramTextsFont()); - setFlags(QGraphicsItem::ItemIsMovable | QGraphicsItem::ItemIsSelectable); + real_font_size_ = font().pointSize(); + setFlags(QGraphicsItem::ItemIsSelectable); #if QT_VERSION >= 0x040600 setFlag(QGraphicsItem::ItemSendsGeometryChanges, true); #endif + setAcceptHoverEvents(true); setPlainText(QObject::tr("_", "default text when adding a textfield in the element editor")); adjustItemPosition(1); @@ -58,7 +63,7 @@ void PartTextField::fromXml(const QDomElement &xml_element) { int font_size = xml_element.attribute("size").toInt(&ok); if (!ok || font_size < 1) font_size = 20; - setFont(QETApp::diagramTextsFont(font_size)); + setProperty("size", font_size); setPlainText(xml_element.attribute("text")); qreal default_rotation_angle = 0.0; @@ -134,32 +139,83 @@ QPointF PartTextField::margin() const { return(QPointF(0.0, boundingRect().bottom() / 2.0)); } +/** + Handle context menu events. + @param event Object describing the context menu event to handle. +*/ +void PartTextField::contextMenuEvent(QGraphicsSceneContextMenuEvent *event) { + Q_UNUSED(event); +} + +/** + Handle events generated when the mouse hovers over the decorator. + @param event Object describing the hover event. +*/ +void PartTextField::hoverMoveEvent(QGraphicsSceneHoverEvent *event) { + // force the cursor when the text is being edited + if (hasFocus() && decorator_) { + decorator_ -> setCursor(Qt::IBeamCursor); + } + QGraphicsTextItem::hoverMoveEvent(event); +} + +/** + @reimp QGraphicsItem::sceneEventFilter(QGraphicsItem *, QEvent *). + Intercepts events before they reach the watched target, i.e. typically the + primitives decorator. + This method mainly works with key strokes (F2, escape) and double clicks to + begin or end text edition. +*/ +bool PartTextField::sceneEventFilter(QGraphicsItem *watched, QEvent *event) { + if (watched != decorator_) return(false); + + QPointF event_scene_pos = QET::graphicsSceneEventPos(event); + if (!event_scene_pos.isNull()) { + if (contains(mapFromScene(event_scene_pos))) { + if (hasFocus()) { + return sceneEvent(event); // manually deliver the event to this item + return(true); // prevent this event from being delivered to any item + } else { + if (event -> type() == QEvent::GraphicsSceneMouseDoubleClick) { + mouseDoubleClickEvent(static_cast(event)); + } + } + } + } + else if (event -> type() == QEvent::KeyRelease || event -> type() == QEvent::KeyPress) { + // Intercept F2 and escape keystrokes to focus in and out + QKeyEvent *key_event = static_cast(event); + if (key_event -> key() == Qt::Key_F2) { + setEditable(true); + QTextCursor qtc = textCursor(); + qtc.setPosition(qMax(0, document()->characterCount() - 1)); + setTextCursor(qtc); + } else if (hasFocus() && key_event -> key() == Qt::Key_Escape) { + endEdition(); + } + sceneEvent(event); // manually deliver the event to this item + return(true); // prevent this event from being delivered to any item + } + return(false); +} + +/* + @reimp QGraphicsItem::focusInEvent(QFocusEvent *) + @param e The QFocusEvent object describing the focus gain. + Start text edition when the item gains focus. +*/ +void PartTextField::focusInEvent(QFocusEvent *e) { + startEdition(); + QGraphicsTextItem::focusInEvent(e); +} + /** Permet a l'element texte de redevenir deplacable a la fin de l'edition de texte @param e Le QFocusEvent decrivant la perte de focus */ void PartTextField::focusOutEvent(QFocusEvent *e) { QGraphicsTextItem::focusOutEvent(e); - if (previous_text != toPlainText()) { - undoStack().push( - new ChangePartCommand( - TextFieldEditor::tr("contenu") + " " + name(), - this, - "text", - previous_text, - toPlainText() - ) - ); - previous_text = toPlainText(); - } - - // deselectionne le texte - QTextCursor qtc = textCursor(); - qtc.clearSelection(); - setTextCursor(qtc); - - setTextInteractionFlags(Qt::NoTextInteraction); - setFlag(QGraphicsItem::ItemIsFocusable, false); + endEdition(); } /** @@ -167,11 +223,10 @@ void PartTextField::focusOutEvent(QFocusEvent *e) { @param e Le QGraphicsSceneMouseEvent qui decrit le double-clic */ void PartTextField::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *e) { - setFlag(QGraphicsItem::ItemIsFocusable, true); - setTextInteractionFlags(Qt::TextEditorInteraction); - previous_text = toPlainText(); QGraphicsTextItem::mouseDoubleClickEvent(e); - setFocus(Qt::MouseFocusReason); + if (e -> button() == Qt::LeftButton) { + setEditable(true); + } } /** @@ -194,6 +249,11 @@ void PartTextField::setProperty(const QString &property, const QVariant &value) } else if (property == "size") { if (!value.canConvert(QVariant::Int)) return; setFont(QETApp::diagramTextsFont(value.toInt())); + real_font_size_ = value.toInt(); + } else if (property == "real_size") { + if (!value.canConvert(QVariant::Double)) return; + setFont(QETApp::diagramTextsFont(value.toInt())); + real_font_size_ = value.toDouble(); } else if (property == "text") { setPlainText(value.toString()); } else if (property == "rotation angle") { @@ -223,6 +283,8 @@ QVariant PartTextField::property(const QString &property) { return(pos().y()); } else if (property == "size") { return(font().pointSize()); + } else if (property == "real_size") { + return(real_font_size_); } else if (property == "text") { return(toPlainText()); } else if (property == "rotation angle") { @@ -275,7 +337,7 @@ bool PartTextField::isUseless() const { void PartTextField::startUserTransformation(const QRectF &initial_selection_rect) { Q_UNUSED(initial_selection_rect) saved_point_ = pos(); // scene coordinates, no need to mapFromScene() - saved_font_size_ = font().pointSize(); + saved_font_size_ = real_font_size_; } /** @@ -286,12 +348,10 @@ void PartTextField::handleUserTransformation(const QRectF &initial_selection_rec QPointF new_pos = mapPoints(initial_selection_rect, new_selection_rect, QList() << saved_point_).first(); setPos(new_pos); - // adjust the font size following the smallest scale factor - qreal sx = new_selection_rect.width() / initial_selection_rect.width(); + // adjust the font size following the vertical scale factor qreal sy = new_selection_rect.height() / initial_selection_rect.height(); - qreal smallest_scale_factor = sx > sy ? sy : sx; - qreal new_font_size = saved_font_size_ * smallest_scale_factor; - setProperty("size", qMax(1, qRound(new_font_size))); + qreal new_font_size = saved_font_size_ * sy; + setProperty("real_size", qMax(1, qRound(new_font_size))); } /** Dessine le texte statique. @@ -300,8 +360,12 @@ void PartTextField::handleUserTransformation(const QRectF &initial_selection_rec @param widget Widget sur lequel on dessine (facultatif) */ void PartTextField::paint(QPainter *painter, const QStyleOptionGraphicsItem *qsogi, QWidget *widget) { - QGraphicsTextItem::paint(painter, qsogi, widget); + // According to the source code of QGraphicsTextItem::paint(), this should + // avoid the drawing of the dashed rectangle around the text. + QStyleOptionGraphicsItem our_qsogi(*qsogi); + our_qsogi.state = QStyle::State_None; + QGraphicsTextItem::paint(painter, &our_qsogi, widget); #ifdef QET_DEBUG_EDITOR_TEXTS painter -> setPen(Qt::blue); painter -> drawRect(boundingRect()); @@ -314,6 +378,64 @@ void PartTextField::paint(QPainter *painter, const QStyleOptionGraphicsItem *qso #endif } +/** + @reimp CustomElementPart::setDecorator(ElementPrimitiveDecorator *) + Install or remove a sceneEventFilter on the decorator and ensure it will + adjust itself while the text is being edited. +*/ +void PartTextField::setDecorator(ElementPrimitiveDecorator *decorator) { + if (decorator) { + decorator -> installSceneEventFilter(this); + // ensure the decorator will adjust itself when the text area expands or shrinks + connect(document(), SIGNAL(contentsChanged()), decorator, SLOT(adjust())); + } + else { + decorator_ -> removeSceneEventFilter(this); + endEdition(); + } + decorator_ = decorator; +} + +/** + Accept the mouse \a event relayed by \a decorator if this text item has focus. +*/ +bool PartTextField::singleItemPressEvent(ElementPrimitiveDecorator *decorator, QGraphicsSceneMouseEvent *event) { + Q_UNUSED(decorator) + Q_UNUSED(event) + return(hasFocus()); +} + +/** + Accept the mouse \a event relayed by \a decorator if this text item has focus. +*/ +bool PartTextField::singleItemMoveEvent(ElementPrimitiveDecorator *decorator, QGraphicsSceneMouseEvent *event) { + Q_UNUSED(decorator) + Q_UNUSED(event) + return(hasFocus()); +} + +/** + Accept the mouse \a event relayed by \a decorator if this text item has focus. +*/ +bool PartTextField::singleItemReleaseEvent(ElementPrimitiveDecorator *decorator, QGraphicsSceneMouseEvent *event) { + Q_UNUSED(decorator) + Q_UNUSED(event) + return(hasFocus()); +} + +/** + Accept the mouse \a event relayed by \a decorator if this text item has focus. +*/ +bool PartTextField::singleItemDoubleClickEvent(ElementPrimitiveDecorator *decorator, QGraphicsSceneMouseEvent *event) { + Q_UNUSED(decorator) + // calling mouseDoubleClickEvent() will set this text item editable and grab keyboard focus + if (event -> button() == Qt::LeftButton) { + mouseDoubleClickEvent(event); + return(true); + } + return(false); +} + /** Cette methode s'assure que la position du champ de texte est coherente en repositionnant son origine (c-a-d le milieu du bord gauche du champ de @@ -331,6 +453,59 @@ void PartTextField::adjustItemPosition(int new_block_count) { setTransformOriginPoint(0.0, origin_offset); } +/** + @param editable Whether this text item should be interactively editable. +*/ +void PartTextField::setEditable(bool editable) { + if (editable) { + setFlag(QGraphicsItem::ItemIsFocusable, true); + setTextInteractionFlags(Qt::TextEditorInteraction); + setFocus(Qt::MouseFocusReason); + } + else { + setTextInteractionFlags(Qt::NoTextInteraction); + setFlag(QGraphicsItem::ItemIsFocusable, false); + } +} + +/** + Start text edition by storing the former value of the text. +*/ +void PartTextField::startEdition() { + // !previous_text.isNull() means the text is being edited + previous_text = toPlainText(); +} + +/** + End text edition, potentially generating a ChangePartCommand if the text + has changed. +*/ +void PartTextField::endEdition() { + if (!previous_text.isNull()) { + // the text was being edited + QString new_text = toPlainText(); + if (previous_text != new_text) { + // the text was changed + ChangePartCommand *text_change = new ChangePartCommand( + TextFieldEditor::tr("contenu") + " " + name(), + this, + "text", + previous_text, + new_text + ); + previous_text = QString(); + undoStack().push(text_change); + } + } + + // deselectionne le texte + QTextCursor qtc = textCursor(); + qtc.clearSelection(); + setTextCursor(qtc); + + setEditable(false); +} + #ifdef QET_DEBUG_EDITOR_TEXTS /** Dessine deux petites fleches pour mettre un point en valeur diff --git a/sources/editor/parttextfield.h b/sources/editor/parttextfield.h index 110f6a0bf..c219aab62 100644 --- a/sources/editor/parttextfield.h +++ b/sources/editor/parttextfield.h @@ -21,6 +21,7 @@ #include "customelementpart.h" class TextFieldEditor; class QETElementEditor; +class ElementPrimitiveDecorator; /** This class represents an editable text field which may be used to compose the drawing of an electrical element within the element editor. Users may specify @@ -65,10 +66,23 @@ class PartTextField : public QGraphicsTextItem, public CustomElementPart { virtual void handleUserTransformation(const QRectF &, const QRectF &); virtual void paint(QPainter *, const QStyleOptionGraphicsItem *, QWidget * = 0 ); + virtual void setDecorator(ElementPrimitiveDecorator *); + virtual bool singleItemPressEvent(ElementPrimitiveDecorator *, QGraphicsSceneMouseEvent *); + virtual bool singleItemMoveEvent(ElementPrimitiveDecorator *, QGraphicsSceneMouseEvent *); + virtual bool singleItemReleaseEvent(ElementPrimitiveDecorator *, QGraphicsSceneMouseEvent *); + virtual bool singleItemDoubleClickEvent(ElementPrimitiveDecorator *, QGraphicsSceneMouseEvent *); + public slots: void adjustItemPosition(int = 0); + void setEditable(bool); + void startEdition(); + void endEdition(); protected: + virtual void contextMenuEvent(QGraphicsSceneContextMenuEvent *); + virtual void hoverMoveEvent(QGraphicsSceneHoverEvent *); + virtual bool sceneEventFilter(QGraphicsItem *, QEvent *); + virtual void focusInEvent(QFocusEvent *); virtual void focusOutEvent(QFocusEvent *); virtual void mouseDoubleClickEvent(QGraphicsSceneMouseEvent *); virtual QVariant itemChange(GraphicsItemChange, const QVariant &); @@ -80,7 +94,9 @@ class PartTextField : public QGraphicsTextItem, public CustomElementPart { void drawPoint(QPainter *, const QPointF &); #endif QString previous_text; + qreal real_font_size_; QPointF saved_point_; - int saved_font_size_; + qreal saved_font_size_; + QGraphicsItem *decorator_; }; #endif diff --git a/sources/qet.cpp b/sources/qet.cpp index 87c4edebf..4ad78a60d 100644 --- a/sources/qet.cpp +++ b/sources/qet.cpp @@ -17,6 +17,7 @@ */ #include "qet.h" #include +#include /** Permet de convertir une chaine de caracteres ("n", "s", "e" ou "w") @@ -577,3 +578,59 @@ bool QET::writeXmlFile(QDomDocument &xml_doc, const QString &filepath, QString * return(true); } + +/** + @return the scene position where \a event occurred, provided it is + QGraphicsScene-related event; otherwise, this function returns a null + QPointF. +*/ +QPointF QET::graphicsSceneEventPos(QEvent *event) { + QPointF event_scene_pos; + if (event -> type() < QEvent::GraphicsSceneContextMenu) return(event_scene_pos); + if (event -> type() > QEvent::GraphicsSceneWheel) return(event_scene_pos); + + switch (event -> type()) { + case QEvent::GraphicsSceneContextMenu: { + QGraphicsSceneContextMenuEvent *qgs_event = static_cast(event); + event_scene_pos = qgs_event -> scenePos(); + break; + } + case QEvent::GraphicsSceneDragEnter: + case QEvent::GraphicsSceneDragLeave: + case QEvent::GraphicsSceneDragMove: + case QEvent::GraphicsSceneDrop: { + QGraphicsSceneDragDropEvent *qgs_event = static_cast(event); + event_scene_pos = qgs_event -> scenePos(); + break; + } + case QEvent::GraphicsSceneHelp: { + QGraphicsSceneHelpEvent *qgs_event = static_cast(event); + event_scene_pos = qgs_event -> scenePos(); + break; + } + + case QEvent::GraphicsSceneHoverEnter: + case QEvent::GraphicsSceneHoverLeave: + case QEvent::GraphicsSceneHoverMove: { + QGraphicsSceneHoverEvent *qgs_event = static_cast(event); + event_scene_pos = qgs_event -> scenePos(); + break; + } + case QEvent::GraphicsSceneMouseDoubleClick: + case QEvent::GraphicsSceneMouseMove: + case QEvent::GraphicsSceneMousePress: + case QEvent::GraphicsSceneMouseRelease: { + QGraphicsSceneMouseEvent *qgs_event = static_cast(event); + event_scene_pos = qgs_event -> scenePos(); + break; + } + case QEvent::GraphicsSceneWheel: { + QGraphicsSceneWheelEvent *qgs_event = static_cast(event); + event_scene_pos = qgs_event -> scenePos(); + break; + } + default: + break; + } + return(event_scene_pos); +} diff --git a/sources/qet.h b/sources/qet.h index 8145f5a3f..e98f3be17 100644 --- a/sources/qet.h +++ b/sources/qet.h @@ -43,6 +43,22 @@ namespace QET { ToNorthWest }; + /// List areas related to some common operations + enum OperationAreas { + ChangeInnerPoints = -4, + RotateArea = -3, + MoveArea = -2, + NoOperation = -1, + ResizeFromTopLeftCorner = 0, + ResizeFromTopCenterCorner = 1, + ResizeFromTopRightCorner = 2, + ResizeFromMiddleLeftCorner = 3, + ResizeFromMiddleRightCorner = 4, + ResizeFromBottomLeftCorner = 5, + ResizeFromBottomCenterCorner = 6, + ResizeFromBottomRightCorner = 7 + }; + /// Known kinds of conductor segments enum ConductorSegmentType { Horizontal = 1, ///< Horizontal segment @@ -149,5 +165,6 @@ namespace QET { bool compareCanonicalFilePaths(const QString &, const QString &); QString titleBlockColumnLengthToString(const TitleBlockColumnLength &); bool writeXmlFile(QDomDocument &, const QString &, QString * = 0); + QPointF graphicsSceneEventPos(QEvent *); } #endif