Commit 1052758a authored by Stefan Majewsky's avatar Stefan Majewsky

Rewrite the ConstraintVisualizer yet again.

The interactive parts become an Interactor, and the visual changes in
the last iteration are mostly reverted. The handles are simple rect
items again, and now they are always visible to improve the
discoverability of this feature.

This commit also solves a long-standing bug: Sometimes, when enlarging
the scene rect beyond the view's boundaries, its size increased much
more than wanted. This is now gone.

svn path=/trunk/KDE/kdegames/palapeli/; revision=1111398
parent 5aa3ec3c
......@@ -5,6 +5,7 @@ set(palapeli_SRCS
creator/propertywidget.cpp
creator/puzzlecreator.cpp
creator/slicerconfwidget.cpp
engine/constraintinteractor.cpp
engine/constraintvisualizer.cpp
engine/interactor.cpp
engine/interactors.cpp
......
/***************************************************************************
* Copyright 2010 Stefan Majewsky <majewsky@gmx.net>
*
* This program 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.
*
* This program 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 this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
***************************************************************************/
#include "constraintinteractor.h"
#include "scene.h"
#include <KLocalizedString>
Palapeli::ConstraintInteractor::ConstraintInteractor(QGraphicsView* view)
: Palapeli::Interactor(Palapeli::MouseInteractor, view)
{
setMetadata(i18n("Change size of puzzle table by dragging its edges"), QIcon());
}
QList<Palapeli::ConstraintInteractor::Side> Palapeli::ConstraintInteractor::touchingSides(const QPointF& scenePos) const
{
const QRectF sceneRect = scene()->sceneRect();
const QSizeF handleSize = sceneRect.size() / 20;
QList<Side> result;
if ((scenePos.x() > sceneRect.left()) && (scenePos.x() < sceneRect.left() + handleSize.width()))
result << LeftSide;
else if ((scenePos.x() < sceneRect.right()) && (scenePos.x() > sceneRect.right() - handleSize.width()))
result << RightSide;
if ((scenePos.y() > sceneRect.top()) && (scenePos.y() < sceneRect.top() + handleSize.height()))
result << TopSide;
else if ((scenePos.y() < sceneRect.bottom()) && (scenePos.y() > sceneRect.bottom() - handleSize.height()))
result << BottomSide;
return result;
}
bool Palapeli::ConstraintInteractor::acceptMousePosition(const QPoint& pos)
{
if (!scene())
return false;
if (!m_draggingSides.isEmpty())
return true; //dragging in progress
//check mouse position
return !touchingSides(view()->mapToScene(pos)).isEmpty();
}
void Palapeli::ConstraintInteractor::mousePressEvent(const Palapeli::MouseEvent& event)
{
if (!scene())
return;
//determine touching sides
m_draggingSides = touchingSides(event.scenePos);
//record the position where we grabbed the handles (more precisely: its distance to the sides of the scene rect)
m_baseSceneRectOffset = QPointF();
const QRectF sceneRect = scene()->sceneRect();
if (m_draggingSides.contains(LeftSide))
m_baseSceneRectOffset.rx() = event.scenePos.x() - sceneRect.left();
else if (m_draggingSides.contains(RightSide))
m_baseSceneRectOffset.rx() = event.scenePos.x() - sceneRect.right();
if (m_draggingSides.contains(TopSide))
m_baseSceneRectOffset.ry() = event.scenePos.y() - sceneRect.top();
else if (m_draggingSides.contains(BottomSide))
m_baseSceneRectOffset.ry() = event.scenePos.y() - sceneRect.bottom();
}
void Palapeli::ConstraintInteractor::mouseMoveEvent(const Palapeli::MouseEvent& event)
{
//in this method, we need the scene() as Palapeli::Scene for the piecesBoundingRect
Palapeli::Scene* scene = qobject_cast<Palapeli::Scene*>(this->scene());
if (!scene)
return;
QRectF sceneRect = scene->sceneRect();
//change scene rect
const QPointF newBounds = event.scenePos - m_baseSceneRectOffset;
if (m_draggingSides.contains(LeftSide))
sceneRect.setLeft(newBounds.x());
else if (m_draggingSides.contains(RightSide))
sceneRect.setRight(newBounds.x());
if (m_draggingSides.contains(TopSide))
sceneRect.setTop(newBounds.y());
else if (m_draggingSides.contains(BottomSide))
sceneRect.setBottom(newBounds.y());
scene->setSceneRect(sceneRect | scene->piecesBoundingRect());
}
void Palapeli::ConstraintInteractor::mouseReleaseEvent(const Palapeli::MouseEvent& event)
{
Q_UNUSED(event)
m_draggingSides.clear();
}
......@@ -16,36 +16,29 @@
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
***************************************************************************/
#ifndef PALAPELI_CONSTRAINTVISUALIZER_P_H
#define PALAPELI_CONSTRAINTVISUALIZER_P_H
#ifndef PALAPELI_CONSTRAINTINTERACTOR_H
#define PALAPELI_CONSTRAINTINTERACTOR_H
#include <QCursor>
#include <QGraphicsItem>
#include <QPropertyAnimation>
#include "interactor.h"
namespace Palapeli
{
class CvHandleItem : public QObject, public QGraphicsPathItem
class ConstraintInteractor : public Palapeli::Interactor
{
Q_OBJECT
Q_PROPERTY(qreal opacity READ opacity WRITE setOpacity)
public:
CvHandleItem(const QCursor& cursor, const QColor& baseColor, QGraphicsItem* parent = 0);
qreal opacity() const;
void setOpacity(qreal opacity);
void setOpacityAnimated(qreal opacity);
ConstraintInteractor(QGraphicsView* view);
protected:
virtual void hoverEnterEvent(QGraphicsSceneHoverEvent* event);
virtual void hoverLeaveEvent(QGraphicsSceneHoverEvent* event);
virtual void mouseMoveEvent(QGraphicsSceneMouseEvent* event);
virtual void mousePressEvent(QGraphicsSceneMouseEvent* event);
virtual QVariant itemChange(GraphicsItemChange change, const QVariant& value);
virtual bool acceptMousePosition(const QPoint& pos);
virtual void mousePressEvent(const Palapeli::MouseEvent& event);
virtual void mouseMoveEvent(const Palapeli::MouseEvent& event);
virtual void mouseReleaseEvent(const Palapeli::MouseEvent& event);
private:
QColor m_baseColor;
qreal m_opacity;
QPropertyAnimation* m_animator;
enum Side { LeftSide = 0, RightSide, TopSide, BottomSide };
QList<Side> touchingSides(const QPointF& scenePos) const;
QList<Side> m_draggingSides;
QPointF m_baseSceneRectOffset;
};
}
#endif // PALAPELI_CONSTRAINTVISUALIZER_P_H
#endif // PALAPELI_CONSTRAINTINTERACTOR_H
......@@ -17,111 +17,38 @@
***************************************************************************/
#include "constraintvisualizer.h"
#include "constraintvisualizer_p.h"
#include "scene.h"
#include <QGraphicsSceneMouseEvent>
//BEGIN Palapeli::CvHandleItem
Palapeli::CvHandleItem::CvHandleItem(const QCursor& cursor, const QColor& baseColor, QGraphicsItem* parent)
: QGraphicsPathItem(parent)
, m_baseColor(baseColor)
, m_opacity(0)
, m_animator(new QPropertyAnimation(this, "opacity", this))
{
setPen(Qt::NoPen);
setOpacity(0.01); //not visible in the beginning (HACK: QGV is overly clever and won't deliver any events to opacity=0.0 items)
setCursor(cursor);
setAcceptHoverEvents(true); //we need these for the animated show/hide
setAcceptedMouseButtons(Qt::LeftButton); //this is for dragging
setFlag(QGraphicsItem::ItemIsSelectable); //see CvHandleItem::itemChange
setFlag(QGraphicsItem::ItemIgnoresParentOpacity); //which controls the appearance of the shadow items
}
qreal Palapeli::CvHandleItem::opacity() const
{
return m_opacity;
}
void Palapeli::CvHandleItem::setOpacity(qreal opacity)
{
if (m_opacity == opacity)
return;
m_opacity = opacity;
QColor brushColor(m_baseColor);
brushColor.setAlpha(m_baseColor.alpha() * opacity);
setBrush(brushColor);
}
void Palapeli::CvHandleItem::setOpacityAnimated(qreal targetOpacity)
{
if (m_opacity == targetOpacity)
return;
m_animator->setDuration(150 * qAbs(targetOpacity - m_opacity));
m_animator->setStartValue(m_opacity);
m_animator->setEndValue(targetOpacity);
m_animator->start();
}
QVariant Palapeli::CvHandleItem::itemChange(GraphicsItemChange change, const QVariant& value)
{
//HACK: The MovePieceInteractor does not propagate mouse events to non-selectable items. We therefore have the ItemIsSelectable flag set in this item, but deny any activation through this method.
if (change == ItemSelectedChange)
return qVariantFromValue(false);
return QGraphicsPathItem::itemChange(change, value);
}
void Palapeli::CvHandleItem::hoverEnterEvent(QGraphicsSceneHoverEvent* event)
{
Q_UNUSED(event)
setOpacityAnimated(1);
}
void Palapeli::CvHandleItem::hoverLeaveEvent(QGraphicsSceneHoverEvent* event)
{
Q_UNUSED(event)
setOpacityAnimated(0.01); //HACK: QGV is overly clever and won't deliver any events to opacity=0.0 items
}
void Palapeli::CvHandleItem::mouseMoveEvent(QGraphicsSceneMouseEvent* event)
{
//let parent handle this type of event
Palapeli::ConstraintVisualizer* cv = qgraphicsitem_cast<Palapeli::ConstraintVisualizer*>(parentItem());
cv->mouseMove(event, this);
}
void Palapeli::CvHandleItem::mousePressEvent(QGraphicsSceneMouseEvent* event)
{
//let parent handle this type of event
Palapeli::ConstraintVisualizer* cv = qgraphicsitem_cast<Palapeli::ConstraintVisualizer*>(parentItem());
cv->mousePress(event, this);
}
//END Palapeli::CvHandleItem
#include <QCursor>
#include <QPropertyAnimation>
Palapeli::ConstraintVisualizer::ConstraintVisualizer(Palapeli::Scene* scene)
: m_scene(scene)
, m_active(false)
, m_shadowItems(SideCount)
, m_handleItems(HandleCount)
, m_handleExtent(0.0)
, m_animator(new QPropertyAnimation(this, "opacity", this))
{
setOpacity(0.2);
//create gray items (with null size!)
setOpacity(0.3);
//create shadow items (with null size until first update())
QColor rectColor(Qt::black);
rectColor.setAlpha(80);
for (int i = 0; i < SideCount; ++i)
{
m_shadowItems[i] = new QGraphicsRectItem(QRect(), this);
m_shadowItems[i] = new QGraphicsRectItem(this);
m_shadowItems[i]->setPen(Qt::NoPen);
m_shadowItems[i]->setBrush(rectColor);
}
rectColor.setAlpha(0.6 * rectColor.alpha());
//create handle items (also with null size)
rectColor.setAlpha(rectColor.alpha() / 2);
Qt::CursorShape shapes[] = { Qt::SizeHorCursor, Qt::SizeFDiagCursor, Qt::SizeVerCursor, Qt::SizeBDiagCursor };
for (int i = 0; i < HandleCount; ++i)
m_handleItems[i] = new Palapeli::CvHandleItem(shapes[i % 4], rectColor, this);
{
m_handleItems[i] = new QGraphicsRectItem(this);
m_handleItems[i]->setPen(Qt::NoPen);
m_handleItems[i]->setBrush(rectColor);
m_handleItems[i]->setCursor(shapes[i % 4]);
}
//more initialization
QObject::setParent(scene); //delete myself automatically when the scene is destroyed
scene->addItem(this);
......@@ -139,7 +66,7 @@ void Palapeli::ConstraintVisualizer::setActive(bool active)
if (m_active == active)
return;
m_active = active;
const qreal targetOpacity = active ? 1.0 : 0.2;
const qreal targetOpacity = active ? 1.0 : 0.3;
m_animator->setDuration(150 * qAbs(targetOpacity - opacity()));
m_animator->setStartValue(opacity());
m_animator->setEndValue(targetOpacity);
......@@ -151,7 +78,7 @@ void Palapeli::ConstraintVisualizer::update(const QRectF& sceneRect)
if (m_sceneRect == sceneRect)
return;
m_sceneRect = sceneRect;
m_handleExtent = qMin(sceneRect.width(), sceneRect.height()) / 20;
const QSizeF handleSize = sceneRect.size() / 20;
//find a fictional viewport rect which we want to cover (except for the contained scene rect)
const qreal viewportRectSizeFactor = 10;
QRectF viewportRect = sceneRect;
......@@ -177,73 +104,8 @@ void Palapeli::ConstraintVisualizer::update(const QRectF& sceneRect)
itemRect.setLeft(sceneRect.left()); //same as above
itemRect.setRight(sceneRect.right());
m_shadowItems[BottomSide]->setRect(itemRect);
//some helper variables
QPointF stepLeft(-m_handleExtent, 0);
QPointF stepRight(m_handleExtent, 0);
QPointF stepUp(0, -m_handleExtent);
QPointF stepDown(0, m_handleExtent);
//adjust left and right handle
QPainterPath p1;
p1.moveTo(sceneRect.bottomLeft());
p1.lineTo(sceneRect.bottomLeft() + stepRight + stepUp);
p1.lineTo(sceneRect.topLeft() + stepRight + stepDown);
p1.lineTo(sceneRect.topLeft());
p1.closeSubpath();
m_handleItems[LeftHandle]->setPath(p1);
QPainterPath p2;
p2.moveTo(sceneRect.bottomRight());
p2.lineTo(sceneRect.bottomRight() + stepLeft + stepUp);
p2.lineTo(sceneRect.topRight() + stepLeft + stepDown);
p2.lineTo(sceneRect.topRight());
p2.closeSubpath();
m_handleItems[RightHandle]->setPath(p2);
//adjust top and bottom handle
QPainterPath p3;
p3.moveTo(sceneRect.topLeft());
p3.lineTo(sceneRect.topLeft() + stepDown + stepRight);
p3.lineTo(sceneRect.topRight() + stepDown + stepLeft);
p3.lineTo(sceneRect.topRight());
p3.closeSubpath();
m_handleItems[TopHandle]->setPath(p3);
QPainterPath p4;
p4.moveTo(sceneRect.bottomLeft());
p4.lineTo(sceneRect.bottomLeft() + stepUp + stepRight);
p4.lineTo(sceneRect.bottomRight() + stepUp + stepLeft);
p4.lineTo(sceneRect.bottomRight());
p4.closeSubpath();
m_handleItems[BottomHandle]->setPath(p4);
//adjust edge handles
QPainterPath p5;
p5.moveTo(sceneRect.topLeft());
p5.lineTo(sceneRect.topLeft() + 2 * stepRight);
p5.lineTo(sceneRect.topLeft() + 2 * stepDown);
p5.closeSubpath();
m_handleItems[TopLeftHandle]->setPath(p5);
m_handleItems[TopLeftHandle]->setZValue(1);
QPainterPath p6;
p6.moveTo(sceneRect.topRight());
p6.lineTo(sceneRect.topRight() + 2 * stepLeft);
p6.lineTo(sceneRect.topRight() + 2 * stepDown);
p6.closeSubpath();
m_handleItems[TopRightHandle]->setPath(p6);
m_handleItems[TopRightHandle]->setZValue(1);
QPainterPath p7;
p7.moveTo(sceneRect.bottomLeft());
p7.lineTo(sceneRect.bottomLeft() + 2 * stepRight);
p7.lineTo(sceneRect.bottomLeft() + 2 * stepUp);
p7.closeSubpath();
m_handleItems[BottomLeftHandle]->setPath(p7);
m_handleItems[BottomLeftHandle]->setZValue(1);
QPainterPath p8;
p8.moveTo(sceneRect.bottomRight());
p8.lineTo(sceneRect.bottomRight() + 2 * stepLeft);
p8.lineTo(sceneRect.bottomRight() + 2 * stepUp);
p8.closeSubpath();
m_handleItems[BottomRightHandle]->setPath(p8);
m_handleItems[BottomRightHandle]->setZValue(1);
#if 0
//adjust edge handles
QRectF handleRect(QPointF(), QSizeF(m_handleExtent, m_handleExtent));
QRectF handleRect(QPointF(), handleSize);
handleRect.moveTopLeft(sceneRect.topLeft());
m_handleItems[TopLeftHandle]->setRect(handleRect);
handleRect.moveTopRight(sceneRect.topRight());
......@@ -253,67 +115,19 @@ void Palapeli::ConstraintVisualizer::update(const QRectF& sceneRect)
handleRect.moveBottomRight(sceneRect.bottomRight());
m_handleItems[BottomRightHandle]->setRect(handleRect);
//adjust top/bottom handles
handleRect.setSize(QSizeF(sceneRect.width() - 2 * m_handleExtent, m_handleExtent));
handleRect.setSize(QSizeF(sceneRect.width() - 2 * handleSize.width(), handleSize.height()));
handleRect.moveCenter(sceneRect.center());
handleRect.moveTop(sceneRect.top());
m_handleItems[TopHandle]->setRect(handleRect);
handleRect.moveBottom(sceneRect.bottom());
m_handleItems[BottomHandle]->setRect(handleRect);
//adjust left/right handles
handleRect.setSize(QSizeF(m_handleExtent, sceneRect.height() - 2 * m_handleExtent));
handleRect.setSize(QSizeF(handleSize.width(), sceneRect.height() - 2 * handleSize.height()));
handleRect.moveCenter(sceneRect.center());
handleRect.moveLeft(sceneRect.left());
m_handleItems[LeftHandle]->setRect(handleRect);
handleRect.moveRight(sceneRect.right());
m_handleItems[RightHandle]->setRect(handleRect);
#endif
}
void Palapeli::ConstraintVisualizer::mousePress(QGraphicsSceneMouseEvent* event, Palapeli::CvHandleItem* sender)
{
const int handleIndex = m_handleItems.indexOf(sender);
if (handleIndex == -1)
{
event->ignore();
return;
}
event->accept();
m_lastScreenPos = event->screenPos();
//determine which coordinates can be moved
m_draggingSides.clear();
if ((handleIndex + 1) % HandleCount < 3)
m_draggingSides << LeftSide;
else if ((handleIndex + 5) % HandleCount < 3)
m_draggingSides << RightSide;
if ((handleIndex + 7) % HandleCount < 3)
m_draggingSides << TopSide;
else if ((handleIndex + 3) % HandleCount < 3)
m_draggingSides << BottomSide;
}
void Palapeli::ConstraintVisualizer::mouseMove(QGraphicsSceneMouseEvent* event, Palapeli::CvHandleItem* sender)
{
Q_UNUSED(sender)
event->accept();
//prevent infinite loops (When the mouse reaches the end of the scene, the following will happen: 1. The ConstraintVisualizer enlarges the scene rect. 2. The QGraphicsView moves its viewport to accommodate the new scene rect. 3. The QGraphicsView might notice that the scene mouse position has changed, and fire a new mouseMoveEvent. 4. Repeat with step 1.)
if (m_lastScreenPos == event->screenPos())
return;
m_lastScreenPos = event->screenPos();
//modify scene rect
QRectF newSceneRect = m_sceneRect;
const QPointF pos = event->scenePos();
const QPointF posDiff = event->scenePos() - event->lastScenePos();
if (m_draggingSides.contains(LeftSide))
newSceneRect.setLeft(m_sceneRect.left() + posDiff.x());
if (m_draggingSides.contains(RightSide))
newSceneRect.setRight(m_sceneRect.right() + posDiff.x());
if (m_draggingSides.contains(TopSide))
newSceneRect.setTop(m_sceneRect.top() + posDiff.y());
if (m_draggingSides.contains(BottomSide))
newSceneRect.setBottom(m_sceneRect.bottom() + posDiff.y());
newSceneRect |= m_scene->piecesBoundingRect();
m_scene->setSceneRect(newSceneRect);
}
#include "constraintvisualizer.moc"
#include "constraintvisualizer_p.moc"
......@@ -39,26 +39,17 @@ namespace Palapeli
public Q_SLOTS:
void setActive(bool active);
void update(const QRectF& sceneRect);
protected:
friend class CvHandleItem;
void mouseMove(QGraphicsSceneMouseEvent* event, Palapeli::CvHandleItem* sender);
void mousePress(QGraphicsSceneMouseEvent* event, Palapeli::CvHandleItem* sender);
private:
enum Side { LeftSide = 0, RightSide, TopSide, BottomSide, SideCount };
//WARNING: Do not change the order of entries in the following enum. Some code in ConstraintVisualizer relies on it.
enum HandlePosition { LeftHandle = 0, TopLeftHandle, TopHandle, TopRightHandle, RightHandle, BottomRightHandle, BottomHandle, BottomLeftHandle, HandleCount };
Palapeli::Scene* m_scene;
bool m_active;
QVector<QGraphicsRectItem*> m_shadowItems;
QVector<Palapeli::CvHandleItem*> m_handleItems;
QVector<QGraphicsRectItem*> m_shadowItems, m_handleItems;
QGraphicsPathItem* m_indicatorItem;
QRectF m_sceneRect;
qreal m_handleExtent;
QPropertyAnimation* m_animator;
//the following are only used in the handles' mouse events
QList<Side> m_draggingSides;
QPoint m_lastScreenPos;
};
}
......
......@@ -17,14 +17,13 @@
***************************************************************************/
#include "interactormanager.h"
#include "constraintinteractor.h"
#include "interactors.h"
#include <QKeyEvent>
#include <QMouseEvent>
#include <QWheelEvent>
//TODO: Rewrite the interactive parts of the ConstraintVisualizer into an Interactor.
Palapeli::InteractorManager::InteractorManager(QGraphicsView* view)
: m_view(view)
{
......@@ -33,11 +32,13 @@ Palapeli::InteractorManager::InteractorManager(QGraphicsView* view)
m_interactors["MoveViewport"] = new Palapeli::MoveViewportInteractor(view);
m_interactors["ZoomViewport"] = new Palapeli::ZoomViewportInteractor(view);
m_interactors["RubberBand"] = new Palapeli::RubberBandInteractor(view);
//setup triggers
m_interactors["Constraints"] = new Palapeli::ConstraintInteractor(view);
//setup triggers (WARNING: the insertion order implements priority)
typedef Palapeli::InteractorTrigger PIT;
m_triggers << qMakePair(PIT("LeftButton;NoModifier"), m_interactors["MovePiece"]);
m_triggers << qMakePair(PIT("RightButton;NoModifier"), m_interactors["MoveViewport"]);
m_triggers << qMakePair(PIT("wheel:Vertical;NoModifier"), m_interactors["ZoomViewport"]);
m_triggers << qMakePair(PIT("LeftButton;NoModifier"), m_interactors["Constraints"]);
m_triggers << qMakePair(PIT("LeftButton;NoModifier"), m_interactors["RubberBand"]);
//initialize quasi-static data
m_keyModifierMap[Qt::Key_Shift] = Qt::ShiftModifier;
......
......@@ -287,7 +287,7 @@ void Palapeli::RubberBandInteractor::mousePressEvent(const Palapeli::MouseEvent&
void Palapeli::RubberBandInteractor::mouseMoveEvent(const Palapeli::MouseEvent& event)
{
//let the interactor pick up the mouse move event only if rubberbanding is actually active (TODO: this is a bug in InteractorManager -> it should recognize chains of mouse events consisting of a series of press-move-move-...-move-release events)
//let the interactor pick up the mouse move event only if rubberbanding is actually active
if (!m_item->isVisible())
return;
QSizeF size(event.scenePos.x() - m_basePosition.x(), event.scenePos.y() - m_basePosition.y());
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment