[qet] [2027] Implemented a primitive decorator, allowing groups of primitives to be easily resized.

[ Thread Index | Date Index | More lists.tuxfamily.org/qet Archives ]


Revision: 2027
Author:   xavier
Date:     2013-02-08 23:05:15 +0100 (Fri, 08 Feb 2013)
Log Message:
-----------
Implemented a primitive decorator, allowing groups of primitives to be easily resized.

Modified Paths:
--------------
    trunk/sources/editor/customelementpart.cpp
    trunk/sources/editor/customelementpart.h
    trunk/sources/editor/editorcommands.cpp
    trunk/sources/editor/editorcommands.h
    trunk/sources/editor/elementscene.cpp
    trunk/sources/editor/elementscene.h
    trunk/sources/editor/partarc.cpp
    trunk/sources/editor/partcircle.cpp
    trunk/sources/editor/partellipse.cpp
    trunk/sources/editor/partline.cpp
    trunk/sources/editor/partpolygon.cpp
    trunk/sources/editor/partrectangle.cpp
    trunk/sources/editor/partterminal.cpp
    trunk/sources/editor/parttext.cpp
    trunk/sources/editor/parttext.h
    trunk/sources/editor/parttextfield.cpp
    trunk/sources/editor/parttextfield.h
    trunk/sources/qet.cpp
    trunk/sources/qet.h

Added Paths:
-----------
    trunk/sources/editor/elementprimitivedecorator.cpp
    trunk/sources/editor/elementprimitivedecorator.h

Modified: trunk/sources/editor/customelementpart.cpp
===================================================================
--- trunk/sources/editor/customelementpart.cpp	2013-02-08 22:05:12 UTC (rev 2026)
+++ trunk/sources/editor/customelementpart.cpp	2013-02-08 22:05:15 UTC (rev 2027)
@@ -44,7 +44,62 @@
 	return(elementScene() -> undoStack());
 }
 
+/// @return this primitive as a QGraphicsItem
+QGraphicsItem *CustomElementPart::toItem() {
+	return(dynamic_cast<QGraphicsItem *>(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
 	@param new_selection_rect New selection rectangle, in scene coordinates

Modified: trunk/sources/editor/customelementpart.h
===================================================================
--- trunk/sources/editor/customelementpart.h	2013-02-08 22:05:12 UTC (rev 2026)
+++ trunk/sources/editor/customelementpart.h	2013-02-08 22:05:15 UTC (rev 2027)
@@ -21,8 +21,10 @@
 #include <QtXml>
 #include <QImage>
 class CustomElement;
+class ElementPrimitiveDecorator;
+class ElementScene;
 class QETElementEditor;
-class ElementScene;
+
 /**
 	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 @@
 	/// @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<QPointF> mapPoints(const QRectF &, const QRectF &, const QList<QPointF> &);
 };

Modified: trunk/sources/editor/editorcommands.cpp
===================================================================
--- trunk/sources/editor/editorcommands.cpp	2013-02-08 22:05:12 UTC (rev 2026)
+++ trunk/sources/editor/editorcommands.cpp	2013-02-08 22:05:15 UTC (rev 2027)
@@ -684,3 +684,104 @@
 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<CustomElementPart *> &primitives) {
+	scaled_primitives_ = primitives;
+	adjustText();
+}
+
+/**
+	@return the list of primitives to be scaled by this command
+*/
+QList<CustomElementPart *> 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<QRectF, QRectF> ScalePartsCommand::transformation() {
+	return(QPair<QRectF, QRectF>(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()));
+	}
+}

Modified: trunk/sources/editor/editorcommands.h
===================================================================
--- trunk/sources/editor/editorcommands.h	2013-02-08 22:05:12 UTC (rev 2026)
+++ trunk/sources/editor/editorcommands.h	2013-02-08 22:05:15 UTC (rev 2027)
@@ -391,4 +391,42 @@
 	/// 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<CustomElementPart *> &);
+	QList<CustomElementPart *> scaledPrimitives() const;
+	void setTransformation(const QRectF &, const QRectF &);
+	QPair<QRectF, QRectF> transformation();
+	
+	protected:
+	void scale(const QRectF &before, const QRectF &after);
+	void adjustText();
+	
+	// attributes
+	private:
+	/// List of moved primitives
+	QList<CustomElementPart *> 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

Added: trunk/sources/editor/elementprimitivedecorator.cpp
===================================================================
--- trunk/sources/editor/elementprimitivedecorator.cpp	                        (rev 0)
+++ trunk/sources/editor/elementprimitivedecorator.cpp	2013-02-08 22:05:15 UTC (rev 2027)
@@ -0,0 +1,614 @@
+#include "elementprimitivedecorator.h"
+#include "elementscene.h"
+#include "customelementpart.h"
+#include "editorcommands.h"
+#include "qet.h"
+#include <QPainter>
+#include <QDebug>
+#include <QGraphicsSceneHoverEvent>
+#include <QStyleOptionGraphicsItem>
+#include <QGraphicsScene>
+#include <QTransform>
+
+/**
+	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<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();
+	grabKeyboard();
+}
+
+/**
+	@param items the new list of items this decorator is suposed to manipulate.
+*/
+void ElementPrimitiveDecorator::setItems(const QList<QGraphicsItem *> &items) {
+	QList<CustomElementPart *> primitives;
+	foreach (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;
+	foreach (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) {
+	QList<QRectF> 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<QRectF> 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<QRectF> 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<QRectF> 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<QRectF> 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<QRectF> 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));
+}

Added: trunk/sources/editor/elementprimitivedecorator.h
===================================================================
--- trunk/sources/editor/elementprimitivedecorator.h	                        (rev 0)
+++ trunk/sources/editor/elementprimitivedecorator.h	2013-02-08 22:05:15 UTC (rev 2027)
@@ -0,0 +1,90 @@
+#ifndef ELEMENTPRIMITIVEDECORATOR_H
+#define ELEMENTPRIMITIVEDECORATOR_H
+#include <QGraphicsObject>
+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<QGraphicsItem *> &);
+	void setItems(const QList<CustomElementPart *> &);
+	QList<CustomElementPart *> items() const;
+	QList<QGraphicsItem *> 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<QRectF> getResizingSquares();
+	QRectF getGenericSquare(const QPointF &);
+	int resizingSquareAtPos(const QPointF &);
+	
+	// attributes
+	private:
+	QList<CustomElementPart *> 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

Modified: trunk/sources/editor/elementscene.cpp
===================================================================
--- trunk/sources/editor/elementscene.cpp	2013-02-08 22:05:12 UTC (rev 2026)
+++ trunk/sources/editor/elementscene.cpp	2013-02-08 22:05:15 UTC (rev 2027)
@@ -17,6 +17,7 @@
 */
 #include "elementscene.h"
 #include "qetelementeditor.h"
+#include "elementprimitivedecorator.h"
 #include <cmath>
 #include "partline.h"
 #include "partrectangle.h"
@@ -44,17 +45,22 @@
 	_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 @@
 				break;
 			case Normal:
 			default:
-				QList<QGraphicsItem *> 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 @@
 			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 @@
 			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::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 @@
 	l'element.
 */
 bool ElementScene::borderContainsEveryParts() const {
-	return(borderRect().contains(itemsBoundingRect()));
+	return(borderRect().contains(elementContentBoundingRect(items())));
 }
 
 /**
@@ -800,7 +772,10 @@
 	
 	// 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 @@
 	hotspot_editor -> setElementHeight(static_cast<uint>(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);
@@ -1022,6 +997,19 @@
 }
 
 /**
+	@return the list of primitives currently present on the scene.
+*/
+QList<CustomElementPart *> ElementScene::primitives() const {
+	QList<CustomElementPart *> primitives_list;
+	foreach (QGraphicsItem *item, items()) {
+		if (CustomElementPart *primitive = dynamic_cast<CustomElementPart *>(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,8 +1020,14 @@
 	// enleve les bornes
 	QList<QGraphicsItem *> terminals;
 	foreach(QGraphicsItem *qgi, all_items_list) {
-		if (qgraphicsitem_cast<PartTerminal *>(qgi)) {
+		if (
+			qgi -> type() == ElementPrimitiveDecorator::Type ||
+			qgi -> type() == QGraphicsRectItem::Type
+		) {
 			all_items_list.removeAt(all_items_list.indexOf(qgi));
+		}
+		else if (qgraphicsitem_cast<PartTerminal *>(qgi)) {
+			all_items_list.removeAt(all_items_list.indexOf(qgi));
 			terminals << qgi;
 		}
 	}
@@ -1089,9 +1083,12 @@
 	@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::addContent(const ElementContent &content, QString *error_message) {
 	Q_UNUSED(error_message);
 	foreach(QGraphicsItem *part, content) {
-		addItem(part);
+		addPrimitive(part);
 	}
 	return(content);
 }
@@ -1249,12 +1246,21 @@
 	// 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
 */
 void ElementScene::initPasteArea() {
@@ -1300,3 +1306,61 @@
 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<QGraphicsItem *> 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<ElementView *>(view)) {
+				command -> setElementView(element_view);
+				break;
+			}
+		}
+	}
+	
+	undoStack().push(command);
+}

Modified: trunk/sources/editor/elementscene.h
===================================================================
--- trunk/sources/editor/elementscene.h	2013-02-08 22:05:12 UTC (rev 2026)
+++ trunk/sources/editor/elementscene.h	2013-02-08 22:05:15 UTC (rev 2027)
@@ -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 @@
 	/// 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 @@
 	virtual QRectF boundingRectFromXml(const QDomDocument &);
 	virtual void fromXml(const QDomDocument &, const QPointF & = QPointF(), bool = true, ElementContent * = 0);
 	virtual void reset();
+	virtual QList<CustomElementPart *> primitives() const;
 	virtual QList<QGraphicsItem *> zItems(bool = false) const;
 	virtual ElementContent selectedContent() const;
 	virtual void getPasteArea(const QRectF &);
@@ -149,15 +156,17 @@
 	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 @@
 	void slot_raise();
 	void slot_lower();
 	void slot_sendBackward();
+	void managePrimitivesGroups();
+	void stackAction(ElementEditionCommand *);
 	
 	signals:
 	/**

Modified: trunk/sources/editor/partarc.cpp
===================================================================
--- trunk/sources/editor/partarc.cpp	2013-02-08 22:05:12 UTC (rev 2026)
+++ trunk/sources/editor/partarc.cpp	2013-02-08 22:05:15 UTC (rev 2027)
@@ -29,7 +29,7 @@
 	_angle(-90),
 	start_angle(0)
 {
-	setFlags(QGraphicsItem::ItemIsMovable | QGraphicsItem::ItemIsSelectable);
+	setFlags(QGraphicsItem::ItemIsSelectable);
 #if QT_VERSION >= 0x040600
 	setFlag(QGraphicsItem::ItemSendsGeometryChanges, true);
 #endif

Modified: trunk/sources/editor/partcircle.cpp
===================================================================
--- trunk/sources/editor/partcircle.cpp	2013-02-08 22:05:12 UTC (rev 2026)
+++ trunk/sources/editor/partcircle.cpp	2013-02-08 22:05:15 UTC (rev 2027)
@@ -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

Modified: trunk/sources/editor/partellipse.cpp
===================================================================
--- trunk/sources/editor/partellipse.cpp	2013-02-08 22:05:12 UTC (rev 2026)
+++ trunk/sources/editor/partellipse.cpp	2013-02-08 22:05:15 UTC (rev 2027)
@@ -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

Modified: trunk/sources/editor/partline.cpp
===================================================================
--- trunk/sources/editor/partline.cpp	2013-02-08 22:05:12 UTC (rev 2026)
+++ trunk/sources/editor/partline.cpp	2013-02-08 22:05:15 UTC (rev 2027)
@@ -32,7 +32,7 @@
 	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

Modified: trunk/sources/editor/partpolygon.cpp
===================================================================
--- trunk/sources/editor/partpolygon.cpp	2013-02-08 22:05:12 UTC (rev 2026)
+++ trunk/sources/editor/partpolygon.cpp	2013-02-08 22:05:15 UTC (rev 2027)
@@ -29,7 +29,7 @@
 	CustomElementGraphicPart(editor),
 	closed(false)
 {
-	setFlags(QGraphicsItem::ItemIsMovable | QGraphicsItem::ItemIsSelectable);
+	setFlags(QGraphicsItem::ItemIsSelectable);
 #if QT_VERSION >= 0x040600
 	setFlag(QGraphicsItem::ItemSendsGeometryChanges, true);
 #endif

Modified: trunk/sources/editor/partrectangle.cpp
===================================================================
--- trunk/sources/editor/partrectangle.cpp	2013-02-08 22:05:12 UTC (rev 2026)
+++ trunk/sources/editor/partrectangle.cpp	2013-02-08 22:05:15 UTC (rev 2027)
@@ -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

Modified: trunk/sources/editor/partterminal.cpp
===================================================================
--- trunk/sources/editor/partterminal.cpp	2013-02-08 22:05:12 UTC (rev 2026)
+++ trunk/sources/editor/partterminal.cpp	2013-02-08 22:05:15 UTC (rev 2027)
@@ -30,7 +30,7 @@
 	_orientation(QET::North)
 {
 	updateSecondPoint();
-	setFlags(QGraphicsItem::ItemIsMovable | QGraphicsItem::ItemIsSelectable);
+	setFlags(QGraphicsItem::ItemIsSelectable);
 #if QT_VERSION >= 0x040600
 	setFlag(QGraphicsItem::ItemSendsGeometryChanges, true);
 #endif

Modified: trunk/sources/editor/parttext.cpp
===================================================================
--- trunk/sources/editor/parttext.cpp	2013-02-08 22:05:12 UTC (rev 2026)
+++ trunk/sources/editor/parttext.cpp	2013-02-08 22:05:15 UTC (rev 2027)
@@ -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 @@
 	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 @@
 }
 
 /**
-	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 @@
 	@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 @@
 	} 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 @@
 		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 @@
 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 @@
 	QPointF new_pos = mapPoints(initial_selection_rect, new_selection_rect, QList<QPointF>() << 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,8 +320,13 @@
 	@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);
 	painter -> drawRect(boundingRect());
@@ -333,6 +340,124 @@
 }
 
 /**
+	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<QGraphicsSceneMouseEvent *>(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<QKeyEvent *>(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
 	texte) a la position originale. Cela est notamment utile lorsque le champ
@@ -349,6 +474,59 @@
 	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

Modified: trunk/sources/editor/parttext.h
===================================================================
--- trunk/sources/editor/parttext.h	2013-02-08 22:05:12 UTC (rev 2026)
+++ trunk/sources/editor/parttext.h	2013-02-08 22:05:15 UTC (rev 2027)
@@ -20,6 +20,7 @@
 #include <QtGui>
 #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 @@
 	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 @@
 	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

Modified: trunk/sources/editor/parttextfield.cpp
===================================================================
--- trunk/sources/editor/parttextfield.cpp	2013-02-08 22:05:12 UTC (rev 2026)
+++ trunk/sources/editor/parttextfield.cpp	2013-02-08 22:05:15 UTC (rev 2027)
@@ -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 @@
 	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;
@@ -135,31 +140,82 @@
 }
 
 /**
+	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<QGraphicsSceneMouseEvent *>(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<QKeyEvent *>(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 @@
 	@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 @@
 	} 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 @@
 		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 @@
 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 @@
 	QPointF new_pos = mapPoints(initial_selection_rect, new_selection_rect, QList<QPointF>() << 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 @@
 	@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());
@@ -315,6 +379,64 @@
 }
 
 /**
+	@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
 	texte) a la position originale. Cela est notamment utile lorsque le champ
@@ -331,6 +453,59 @@
 	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

Modified: trunk/sources/editor/parttextfield.h
===================================================================
--- trunk/sources/editor/parttextfield.h	2013-02-08 22:05:12 UTC (rev 2026)
+++ trunk/sources/editor/parttextfield.h	2013-02-08 22:05:15 UTC (rev 2027)
@@ -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 @@
 	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 @@
 	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

Modified: trunk/sources/qet.cpp
===================================================================
--- trunk/sources/qet.cpp	2013-02-08 22:05:12 UTC (rev 2026)
+++ trunk/sources/qet.cpp	2013-02-08 22:05:15 UTC (rev 2027)
@@ -17,6 +17,7 @@
 */
 #include "qet.h"
 #include <limits>
+#include <QGraphicsSceneContextMenuEvent>
 
 /**
 	Permet de convertir une chaine de caracteres ("n", "s", "e" ou "w")
@@ -577,3 +578,59 @@
 	
 	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<QGraphicsSceneContextMenuEvent *>(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<QGraphicsSceneDragDropEvent *>(event);
+			event_scene_pos = qgs_event -> scenePos();
+			break;
+		}
+		case QEvent::GraphicsSceneHelp: {
+			QGraphicsSceneHelpEvent *qgs_event = static_cast<QGraphicsSceneHelpEvent *>(event);
+			event_scene_pos = qgs_event -> scenePos();
+			break;
+		}
+		
+		case QEvent::GraphicsSceneHoverEnter:
+		case QEvent::GraphicsSceneHoverLeave:
+		case QEvent::GraphicsSceneHoverMove: {
+			QGraphicsSceneHoverEvent *qgs_event = static_cast<QGraphicsSceneHoverEvent *>(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<QGraphicsSceneMouseEvent *>(event);
+			event_scene_pos = qgs_event -> scenePos();
+			break;
+		}
+		case QEvent::GraphicsSceneWheel: {
+			QGraphicsSceneWheelEvent *qgs_event = static_cast<QGraphicsSceneWheelEvent *>(event);
+			event_scene_pos = qgs_event -> scenePos();
+			break;
+		}
+		default:
+			break;
+	}
+	return(event_scene_pos);
+}

Modified: trunk/sources/qet.h
===================================================================
--- trunk/sources/qet.h	2013-02-08 22:05:12 UTC (rev 2026)
+++ trunk/sources/qet.h	2013-02-08 22:05:15 UTC (rev 2027)
@@ -43,6 +43,22 @@
 		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 @@
 	bool compareCanonicalFilePaths(const QString &, const QString &);
 	QString titleBlockColumnLengthToString(const TitleBlockColumnLength  &);
 	bool writeXmlFile(QDomDocument &, const QString &, QString * = 0);
+	QPointF graphicsSceneEventPos(QEvent *);
 }
 #endif


Mail converted by MHonArc 2.6.19+ http://listengine.tuxfamily.org/