qelectrotech-source-mirror/sources/editor/elementprimitivedecorator.cpp
scorpio810 6422dd096f Replace foreach function by for, please try and report problem
git-svn-id: svn+ssh://svn.tuxfamily.org/svnroot/qet/qet/trunk@4900 bfdf4180-ca20-0410-9c96-a3a8aa849046
2017-02-04 21:43:23 +00:00

664 lines
20 KiB
C++

/*
Copyright 2006-2017 The QElectroTech Team
This file is part of QElectroTech.
QElectroTech is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
QElectroTech is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#include "elementprimitivedecorator.h"
#include "elementscene.h"
#include "customelementpart.h"
#include "editorcommands.h"
#include "qet.h"
#include <QPainter>
#include <QGraphicsSceneHoverEvent>
#include <QStyleOptionGraphicsItem>
#include <QGraphicsScene>
/**
Constructor
@param parent Parent QGraphicsItem
*/
ElementPrimitiveDecorator::ElementPrimitiveDecorator(QGraphicsItem *parent):
QGraphicsObject(parent),
m_handler(10)
{
init();
m_handler.setOuterColor(Qt::darkGreen);
}
/**
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());
//if @decorated_items_ contain one item and if this item is a vertical or horizontal partline, apply a specific methode
if ((decorated_items_.count() == 1) && (decorated_items_.first() -> xmlName() == "line")) {
QRectF horto = decorated_items_.first() -> sceneGeometricRect();
if (!horto.width() || !horto.height()) {
return (getSceneBoundingRect(decorated_items_.first() -> toItem()));
}
}
QRectF rect = decorated_items_.first() -> sceneGeometricRect();
for (CustomElementPart *item: decorated_items_) {
rect = rect.united(item -> sceneGeometricRect());
}
return(rect);
}
/**
@return the outer bounds of the decorator as a rectangle.
*/
QRectF ElementPrimitiveDecorator::boundingRect() const
{
QVector<QRectF> rect_vector = m_handler.handlerRect(getResizingsPoints());
QRectF rect = effective_bounding_rect_;
rect |= rect_vector.first();
rect |= rect_vector.last();
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
QPen pen(Qt::DashLine);
pen.setCosmetic(true);
painter -> setPen(pen);
painter -> drawRect(modified_bounding_rect_);
//Draw the handlers
m_handler.drawHandler(painter, getResizingsPoints());
// 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<CustomElementPart *> &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<QGraphicsObject *>(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();
if (focusItem() != this) {
setFocus();
}
}
/**
@param items the new list of items this decorator is suposed to manipulate.
*/
void ElementPrimitiveDecorator::setItems(const QList<QGraphicsItem *> &items) {
QList<CustomElementPart *> primitives;
for (QGraphicsItem *item: items) {
if (CustomElementPart *part_item = dynamic_cast<CustomElementPart *>(item)) {
primitives << part_item;
}
}
setItems(primitives);
}
/**
@return the list of items this decorator is supposed to manipulate
*/
QList<CustomElementPart *> ElementPrimitiveDecorator::items() const {
return(decorated_items_);
}
/**
@return the list of items this decorator is supposed to manipulate
*/
QList<QGraphicsItem *> ElementPrimitiveDecorator::graphicsItems() const {
QList<QGraphicsItem *> list;
for (CustomElementPart *part_item: decorated_items_) {
if (QGraphicsItem *item = dynamic_cast<QGraphicsItem *>(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)
{
int p = m_handler.pointIsHoverHandler(event->pos(), getResizingsPoints());
if (p == 0 || p == 7)
setCursor(Qt::SizeFDiagCursor);
else if (p == 2 || p == 5)
setCursor(Qt::SizeBDiagCursor);
else if (p == 1 || p ==6)
setCursor(Qt::SizeVerCursor);
else if (p == 3 || p == 4)
setCursor(Qt::SizeHorCursor);
else if (p == -1 && modified_bounding_rect_.normalized().contains(event->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)
{
QPointF pos = event -> pos();
QVector <QPointF> points = getResizingsPoints();
current_operation_square_ = m_handler.pointIsHoverHandler(pos, points);
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(points.at(current_operation_square_));
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)
{
QPointF scene_pos = event -> scenePos();
QPointF movement = scene_pos - latest_pos_;
if (current_operation_square_ > QET::NoOperation) {
// This is a scaling operation.
// For convenience purposes, we may need to adjust mouse movements.
QET::ScalingMethod scaling_method = scalingMethod(event);
if (scaling_method > QET::FreeScaling) {
// real, non-rounded movement from the mouse press event
QPointF global_movement = scene_pos - first_pos_;
QPointF rounded_global_movement;
if (scaling_method == QET::SnapScalingPointToGrid) {
// real, rounded movement from the mouse press event
rounded_global_movement = snapConstPointToGrid(global_movement);
}
else {
QRectF new_bounding_rect = original_bounding_rect_;
applyMovementToRect(current_operation_square_, global_movement, new_bounding_rect);
const qreal scale_epsilon = 20.0; // rounds to 0.05
QPointF delta = deltaForRoundScaling(original_bounding_rect_, new_bounding_rect, scale_epsilon);
// real, rounded movement from the mouse press event
rounded_global_movement = global_movement + delta;
}
// 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(getResizingsPoints().at(current_operation_square_));
// 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 {
// Neither a movement nor a scaling operation -- perhaps the underlying item
// is interested in the mouse event for custom operations?
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;
}
/**
@reimp QGraphicsItem::keyPressEvent
*/
void ElementPrimitiveDecorator::keyPressEvent(QKeyEvent *e) {
const qreal movement_length = 1.0;
QPointF movement;
switch(e -> key()) {
case Qt::Key_Left: movement = QPointF(-movement_length, 0.0); break;
case Qt::Key_Right: movement = QPointF(+movement_length, 0.0); break;
case Qt::Key_Up: movement = QPointF(0.0, -movement_length); break;
case Qt::Key_Down: movement = QPointF(0.0, +movement_length); break;
}
if (!movement.isNull() && focusItem() == this) {
if (!moving_by_keys_) {
moving_by_keys_ = true;
keys_movement_ = movement;
} else {
keys_movement_ += movement;
}
for (QGraphicsItem *qgi: graphicsItems()) {
qgi -> setPos(qgi -> pos() + movement);
adjust();
}
}
QGraphicsObject::keyPressEvent(e);
}
/**
@reimp QGraphicsItem::keyReleaseEvent
*/
void ElementPrimitiveDecorator::keyReleaseEvent(QKeyEvent *e) {
// detecte le relachement d'une touche de direction ( = deplacement de parties)
if (
(e -> key() == Qt::Key_Left || e -> key() == Qt::Key_Right ||\
e -> key() == Qt::Key_Up || e -> key() == Qt::Key_Down) &&\
moving_by_keys_ && !e -> isAutoRepeat()
) {
// cree un objet d'annulation pour le mouvement qui vient de se finir
emit(actionFinished(new MovePartsCommand(keys_movement_, 0, graphicsItems())));
keys_movement_ = QPointF();
moving_by_keys_ = false;
}
QGraphicsObject::keyPressEvent(e);
}
/**
Initialize an ElementPrimitiveDecorator
*/
void ElementPrimitiveDecorator::init() {
setFlag(QGraphicsItem::ItemIsFocusable, true);
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();
for (CustomElementPart *item: decorated_items_) {
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;
for (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();
for (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()));
}
QVector<QPointF> ElementPrimitiveDecorator::getResizingsPoints() const
{
QRectF primitive_rect = modified_bounding_rect_;
QVector <QPointF> vector;
QPointF half;
vector << primitive_rect.topLeft(); //top left
half = primitive_rect.center();
half.setY(primitive_rect.top());
vector << half; //middle top
vector << primitive_rect.topRight(); //top right
half = primitive_rect.center();
half.setX(primitive_rect.left());
vector << half; //middle left
half = primitive_rect.center();
half.setX(primitive_rect.right());
vector << half; //middle right
vector << primitive_rect.bottomLeft(); //bottom left
half = primitive_rect.center();
half.setY(primitive_rect.bottom());
vector << half; //middle bottom
vector << primitive_rect.bottomRight(); //bottom right
return vector;
}
/**
Receive two rects, assuming they share a common corner and current is a \a
scaled version of \a original.
Calculate the scale ratios implied by this assumption, round them to the
nearest multiple of \a epsilon, then return the horizontal and vertical
offsets to be applied in order to pass from \a current to \a original scaled
by the rounded factors.
This method can be used to adjust a mouse movement so that it inputs a
round scaling operation.
*/
QPointF ElementPrimitiveDecorator::deltaForRoundScaling(const QRectF &original, const QRectF &current, qreal epsilon) {
qreal sx = current.width() / original.width();
qreal sy = current.height() / original.height();
qreal sx_rounded = QET::round(sx, epsilon);
qreal sy_rounded = QET::round(sy, epsilon);
QPointF delta(
original.width() * (sx_rounded - sx),
original.height() * (sy_rounded - sy)
);
return(delta);
}
/**
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));
}
/**
@param event Mouse event during the scale operations -- simply passed to mustSnapToGrid()
@return the scaling method to be used for the currently decorated items.
@see QET::ScalingMethod
@see mustSnapToGrid()
*/
QET::ScalingMethod ElementPrimitiveDecorator::scalingMethod(QGraphicsSceneMouseEvent *event) {
if (event && !mustSnapToGrid(event)) {
return(QET::FreeScaling);
}
if (CustomElementPart *single_item = singleItem()) {
return single_item -> preferredScalingMethod();
}
return QET::RoundScaleRatios;
}