Skip to content

Commit

Permalink
Added object name labels for selected objects
Browse files Browse the repository at this point in the history
* The labels don't scale with the zoom level, to keep them readable
  when zoomed out and to keep them from growing way too large when
  zooming in.

* The labels don't rotate along with the object and sit centered at
  the top of the bounding rectangle of the object.

* The labels can be applied to all object types.

* For now the labels are only shown for selected objects. Options to
  turn the labels off or to show them for all objects will be added.

Closes #631
Closes #972
  • Loading branch information
bjorn committed Aug 31, 2015
1 parent 3a101ba commit cecf725
Show file tree
Hide file tree
Showing 3 changed files with 167 additions and 39 deletions.
2 changes: 1 addition & 1 deletion src/tiled/mapobjectitem.cpp
Expand Up @@ -137,7 +137,7 @@ void MapObjectItem::setPolygon(const QPolygonF &polygon)
QColor MapObjectItem::objectColor(const MapObject *object)
{
// See if this object type has a color associated with it
foreach (const ObjectType &type, Preferences::instance()->objectTypes()) {
for (const ObjectType &type : Preferences::instance()->objectTypes()) {
if (type.name.compare(object->type(), Qt::CaseInsensitive) == 0)
return type.color;
}
Expand Down
193 changes: 158 additions & 35 deletions src/tiled/objectselectionitem.cpp
Expand Up @@ -24,9 +24,12 @@
#include "map.h"
#include "mapdocument.h"
#include "mapobject.h"
#include "mapobjectitem.h"
#include "maprenderer.h"
#include "tile.h"

#include <QGuiApplication>

namespace Tiled {
namespace Internal {

Expand Down Expand Up @@ -108,17 +111,14 @@ static QRectF objectBounds(const MapObject *object,
class MapObjectOutline : public QGraphicsItem
{
public:
MapObjectOutline(MapObject *object,
MapDocument *mapDocument,
QGraphicsItem *parent = nullptr)
MapObjectOutline(MapObject *object, QGraphicsItem *parent = nullptr)
: QGraphicsItem(parent)
, mObject(object)
, mMapDocument(mapDocument)
{
syncWithMapObject();
setZValue(1); // makes sure outlines are above labels
}

void syncWithMapObject();
void syncWithMapObject(MapRenderer *renderer);

QRectF boundingRect() const override;
void paint(QPainter *painter,
Expand All @@ -128,13 +128,10 @@ class MapObjectOutline : public QGraphicsItem
private:
QRectF mBoundingRect;
MapObject *mObject;
MapDocument *mMapDocument;
};

void MapObjectOutline::syncWithMapObject()
void MapObjectOutline::syncWithMapObject(MapRenderer *renderer)
{
MapRenderer *renderer = mMapDocument->renderer();

const QPointF pixelPos = renderer->pixelToScreenCoords(mObject->position());
QRectF bounds = objectBounds(mObject, renderer);

Expand All @@ -158,20 +155,113 @@ void MapObjectOutline::paint(QPainter *painter,
const QStyleOptionGraphicsItem *,
QWidget *)
{
const QLineF top(mBoundingRect.topLeft(), mBoundingRect.topRight());
const QLineF left(mBoundingRect.topLeft(), mBoundingRect.bottomLeft());
const QLineF right(mBoundingRect.topRight(), mBoundingRect.bottomRight());
const QLineF bottom(mBoundingRect.bottomLeft(), mBoundingRect.bottomRight());
const QLineF horizontal[2] = {
QLineF(mBoundingRect.topLeft(), mBoundingRect.topRight()),
QLineF(mBoundingRect.bottomLeft(), mBoundingRect.bottomRight())
};

const QLineF vertical[2] = {
QLineF(mBoundingRect.topLeft(), mBoundingRect.bottomLeft()),
QLineF(mBoundingRect.topRight(), mBoundingRect.bottomRight())
};

QPen dashPen(Qt::DashLine);
dashPen.setCosmetic(true);
dashPen.setDashOffset(qMax(qreal(0), x()));
painter->setPen(dashPen);
painter->drawLines(QVector<QLineF>() << top << bottom);
painter->drawLines(horizontal, 2);

dashPen.setDashOffset(qMax(qreal(0), y()));
painter->setPen(dashPen);
painter->drawLines(QVector<QLineF>() << left << right);
painter->drawLines(vertical, 2);
}


class MapObjectLabel : public QGraphicsItem
{
public:
MapObjectLabel(MapObject *object, QGraphicsItem *parent = nullptr)
: QGraphicsItem(parent)
, mObject(object)
{
setFlags(QGraphicsItem::ItemIgnoresTransformations |
QGraphicsItem::ItemIgnoresParentOpacity);
}

void syncWithMapObject(MapRenderer *renderer);

QRectF boundingRect() const override;
void paint(QPainter *painter,
const QStyleOptionGraphicsItem *,
QWidget *) override;

private:
QRectF mBoundingRect;
MapObject *mObject;

static constexpr qreal margin = 3;
static constexpr qreal distance = 12;
};

void MapObjectLabel::syncWithMapObject(MapRenderer *renderer)
{
const bool hasName = !mObject->name().isEmpty();
setVisible(hasName);

if (!hasName)
return;

const QFontMetricsF metrics(QGuiApplication::font());
QRectF boundingRect = metrics.boundingRect(mObject->name());
boundingRect.translate(-boundingRect.width() / 2, -distance);
boundingRect.adjust(-margin*2, -margin, margin*2, margin);

QPointF pixelPos = renderer->pixelToScreenCoords(mObject->position());
QRectF bounds = objectBounds(mObject, renderer);

// Adjust the bounding box for object rotation
QTransform transform;
transform.translate(pixelPos.x(), pixelPos.y());
transform.rotate(mObject->rotation());
transform.translate(-pixelPos.x(), -pixelPos.y());
bounds = transform.mapRect(bounds);

// Center the object name on the object bounding box
QPointF pos((bounds.left() + bounds.right()) / 2, bounds.top());

setPos(pos + mObject->objectGroup()->offset());

if (mBoundingRect != boundingRect) {
prepareGeometryChange();
mBoundingRect = boundingRect;
}
}

QRectF MapObjectLabel::boundingRect() const
{
return mBoundingRect.adjusted(0, 0, 1, 1);
}

void MapObjectLabel::paint(QPainter *painter,
const QStyleOptionGraphicsItem *,
QWidget *)
{
QColor color = MapObjectItem::objectColor(mObject);

painter->setRenderHint(QPainter::Antialiasing);
painter->setBrush(Qt::black);
painter->setPen(Qt::NoPen);
painter->drawRoundedRect(mBoundingRect.translated(1, 1), 4, 4);
painter->setBrush(color);
painter->drawRoundedRect(mBoundingRect, 4, 4);

QPointF textPos(-(mBoundingRect.width() - margin*4) / 2, -distance);

painter->drawRoundedRect(mBoundingRect, 4, 4);
painter->setPen(Qt::black);
painter->drawText(textPos + QPointF(1,1), mObject->name());
painter->setPen(Qt::white);
painter->drawText(textPos, mObject->name());
}


Expand All @@ -190,42 +280,75 @@ ObjectSelectionItem::ObjectSelectionItem(MapDocument *mapDocument)
this, &ObjectSelectionItem::layerChanged);

connect(mapDocument, &MapDocument::objectsChanged,
this, &ObjectSelectionItem::syncObjectOutlines);
this, &ObjectSelectionItem::syncOverlayItems);
}

void ObjectSelectionItem::selectedObjectsChanged()
{
QMap<MapObject*, MapObjectOutline*> outlineItems;

for (MapObject *mapObject : mMapDocument->selectedObjects()) {
MapObjectOutline *outlineItem = mObjectOutlines.take(mapObject);
if (!outlineItem)
outlineItem = new MapObjectOutline(mapObject, mMapDocument, this);
outlineItems.insert(mapObject, outlineItem);
}

qDeleteAll(mObjectOutlines); // delete remaining items
mObjectOutlines.swap(outlineItems);
addRemoveObjectLabels();
addRemoveObjectOutlines();
}

void ObjectSelectionItem::mapChanged()
{
syncObjectOutlines(mMapDocument->selectedObjects());
syncOverlayItems(mMapDocument->selectedObjects());
}

void ObjectSelectionItem::layerChanged(int index)
{
// If an object layer changed, that means its offset may have changed,
// which affects the outlines of selected objects on that layer.
if (index != -1 && mMapDocument->map()->layerAt(index)->isObjectGroup())
syncObjectOutlines(mMapDocument->selectedObjects());
// which affects the outlines of selected objects on that layer and the
// positions of any name labels that are shown.
if (ObjectGroup *objectGroup = mMapDocument->map()->layerAt(index)->asObjectGroup())
syncOverlayItems(objectGroup->objects());
}

void ObjectSelectionItem::syncObjectOutlines(const QList<MapObject*> &objects)
void ObjectSelectionItem::syncOverlayItems(const QList<MapObject*> &objects)
{
for (MapObject *object : objects)
MapRenderer *renderer = mMapDocument->renderer();

for (MapObject *object : objects) {
if (MapObjectOutline *outlineItem = mObjectOutlines.value(object))
outlineItem->syncWithMapObject();
outlineItem->syncWithMapObject(renderer);
if (MapObjectLabel *labelItem = mObjectLabels.value(object))
labelItem->syncWithMapObject(renderer);
}
}

void ObjectSelectionItem::addRemoveObjectLabels()
{
QHash<MapObject*, MapObjectLabel*> labelItems;
MapRenderer *renderer = mMapDocument->renderer();

for (MapObject *mapObject : mMapDocument->selectedObjects()) {
MapObjectLabel *labelItem = mObjectLabels.take(mapObject);
if (!labelItem) {
labelItem = new MapObjectLabel(mapObject, this);
labelItem->syncWithMapObject(renderer);
}
labelItems.insert(mapObject, labelItem);
}

qDeleteAll(mObjectLabels); // delete remaining items
mObjectLabels.swap(labelItems);
}

void ObjectSelectionItem::addRemoveObjectOutlines()
{
QHash<MapObject*, MapObjectOutline*> outlineItems;
MapRenderer *renderer = mMapDocument->renderer();

for (MapObject *mapObject : mMapDocument->selectedObjects()) {
MapObjectOutline *outlineItem = mObjectOutlines.take(mapObject);
if (!outlineItem) {
outlineItem = new MapObjectOutline(mapObject, this);
outlineItem->syncWithMapObject(renderer);
}
outlineItems.insert(mapObject, outlineItem);
}

qDeleteAll(mObjectOutlines); // delete remaining items
mObjectOutlines.swap(outlineItems);
}

} // namespace Internal
Expand Down
11 changes: 8 additions & 3 deletions src/tiled/objectselectionitem.h
Expand Up @@ -22,7 +22,7 @@
#define TILED_INTERNAL_OBJECTSELECTIONITEM_H

#include <QGraphicsObject>
#include <QMap>
#include <QHash>

namespace Tiled {

Expand All @@ -31,6 +31,7 @@ class MapObject;
namespace Internal {

class MapDocument;
class MapObjectLabel;
class MapObjectOutline;

class ObjectSelectionItem : public QGraphicsObject
Expand All @@ -49,11 +50,15 @@ private slots:
void selectedObjectsChanged();
void mapChanged();
void layerChanged(int index);
void syncObjectOutlines(const QList<MapObject *> &objects);
void syncOverlayItems(const QList<MapObject *> &objects);

private:
void addRemoveObjectLabels();
void addRemoveObjectOutlines();

MapDocument *mMapDocument;
QMap<MapObject*, MapObjectOutline*> mObjectOutlines;
QHash<MapObject*, MapObjectLabel*> mObjectLabels;
QHash<MapObject*, MapObjectOutline*> mObjectOutlines;
};

} // namespace Internal
Expand Down

0 comments on commit cecf725

Please sign in to comment.