#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)); }