1 /*
2  * objectselectionitem.cpp
3  * Copyright 2015-2016, Thorbjørn Lindeijer <bjorn@lindeijer.nl>
4  *
5  * This file is part of Tiled.
6  *
7  * This program is free software; you can redistribute it and/or modify it
8  * under the terms of the GNU General Public License as published by the Free
9  * Software Foundation; either version 2 of the License, or (at your option)
10  * any later version.
11  *
12  * This program is distributed in the hope that it will be useful, but WITHOUT
13  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
14  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
15  * more details.
16  *
17  * You should have received a copy of the GNU General Public License along with
18  * this program. If not, see <http://www.gnu.org/licenses/>.
19  */
20 
21 #include "objectselectionitem.h"
22 
23 #include "geometry.h"
24 #include "grouplayer.h"
25 #include "map.h"
26 #include "mapdocument.h"
27 #include "mapobject.h"
28 #include "mapobjectitem.h"
29 #include "maprenderer.h"
30 #include "mapscene.h"
31 #include "objectgroup.h"
32 #include "objectreferenceitem.h"
33 #include "preferences.h"
34 #include "tile.h"
35 #include "utils.h"
36 #include "variantpropertymanager.h"
37 
38 #include <QGuiApplication>
39 #include <QTimerEvent>
40 #include <QVector2D>
41 
42 #include "qtcompat_p.h"
43 
44 #include <cmath>
45 
46 namespace Tiled {
47 
48 static constexpr qreal labelMargin = 2;
49 static constexpr qreal labelDistance = 4;
50 
51 static constexpr qreal selectionZValue = 1.0;   // selection outlines above labels
52 static constexpr qreal hoverZValue = 0.5;       // hover below selection
53 
objectLabelVisibility()54 static Preferences::ObjectLabelVisiblity objectLabelVisibility()
55 {
56     return Preferences::instance()->objectLabelVisibility();
57 }
58 
59 
60 class MapObjectOutline : public QGraphicsObject
61 {
62 public:
63     enum Role {
64         SelectionIndicator,
65         HoverIndicator
66     };
67 
MapObjectOutline(MapObject * object,Role role,QGraphicsItem * parent=nullptr)68     MapObjectOutline(MapObject *object, Role role, QGraphicsItem *parent = nullptr)
69         : QGraphicsObject(parent)
70         , mObject(object)
71     {
72 
73         switch (role) {
74         case SelectionIndicator:
75             setZValue(selectionZValue);
76             mUpdateTimer = startTimer(100);
77             break;
78         case HoverIndicator:
79             setZValue(hoverZValue);
80             setOpacity(0.6);
81             break;
82         }
83     }
84 
mapObject() const85     MapObject *mapObject() const { return mObject; }
86     void syncWithMapObject(const MapRenderer &renderer);
87 
88     QRectF boundingRect() const override;
89     void paint(QPainter *painter,
90                const QStyleOptionGraphicsItem *,
91                QWidget *) override;
92 
93 protected:
94     void timerEvent(QTimerEvent *event) override;
95 
96 private:
97     QRectF mBoundingRect;
98     MapObject *mObject;
99 
100     // Marching ants effect
101     int mUpdateTimer = -1;
102     int mOffset = 0;
103 };
104 
syncWithMapObject(const MapRenderer & renderer)105 void MapObjectOutline::syncWithMapObject(const MapRenderer &renderer)
106 {
107     QPointF pixelPos = renderer.pixelToScreenCoords(mObject->position());
108     QRectF bounds = mObject->screenBounds(renderer);
109     bounds.translate(-pixelPos);
110 
111     if (auto mapScene = static_cast<MapScene*>(scene()))
112         pixelPos += mapScene->absolutePositionForLayer(*mObject->objectGroup());
113 
114     setPos(pixelPos);
115     setRotation(mObject->rotation());
116 
117     if (mBoundingRect != bounds) {
118         prepareGeometryChange();
119         mBoundingRect = bounds;
120     }
121 }
122 
boundingRect() const123 QRectF MapObjectOutline::boundingRect() const
124 {
125     return mBoundingRect;
126 }
127 
paint(QPainter * painter,const QStyleOptionGraphicsItem *,QWidget *)128 void MapObjectOutline::paint(QPainter *painter,
129                              const QStyleOptionGraphicsItem *,
130                              QWidget *)
131 {
132     const QLineF lines[4] = {
133         QLineF(mBoundingRect.topLeft(), mBoundingRect.topRight()),
134         QLineF(mBoundingRect.bottomLeft(), mBoundingRect.bottomRight()),
135         QLineF(mBoundingRect.topLeft(), mBoundingRect.bottomLeft()),
136         QLineF(mBoundingRect.topRight(), mBoundingRect.bottomRight())
137     };
138 
139     const qreal devicePixelRatio = painter->device()->devicePixelRatioF();
140     const qreal dashLength = std::ceil(Utils::dpiScaled(2) * devicePixelRatio);
141 
142     // Draw a solid white line
143     QPen pen(Qt::white, 1.5 * devicePixelRatio, Qt::SolidLine);
144     pen.setCosmetic(true);
145     painter->setRenderHint(QPainter::Antialiasing);
146     painter->setPen(pen);
147     painter->drawLines(lines, 4);
148 
149     // Draw a black dashed line above the white line
150     pen.setColor(Qt::black);
151     pen.setCapStyle(Qt::FlatCap);
152     pen.setDashPattern({dashLength, dashLength});
153     pen.setDashOffset(mOffset);
154     painter->setPen(pen);
155     painter->drawLines(lines, 4);
156 }
157 
timerEvent(QTimerEvent * event)158 void MapObjectOutline::timerEvent(QTimerEvent *event)
159 {
160     if (event->timerId() == mUpdateTimer) {
161         // Update offset used in drawing black dashed line
162         mOffset++;
163         update();
164     } else {
165         QGraphicsObject::timerEvent(event);
166     }
167 }
168 
169 
MapObjectLabel(const MapObject * object,QGraphicsItem * parent)170 MapObjectLabel::MapObjectLabel(const MapObject *object, QGraphicsItem *parent)
171     : QGraphicsItem(parent)
172     , mObject(object)
173     , mColor(mObject->effectiveColor())
174 {
175     setFlags(QGraphicsItem::ItemIgnoresTransformations |
176              QGraphicsItem::ItemIgnoresParentOpacity);
177 }
178 
syncWithMapObject(const MapRenderer & renderer)179 void MapObjectLabel::syncWithMapObject(const MapRenderer &renderer)
180 {
181     const bool nameVisible = mObject->isVisible() && !mObject->name().isEmpty();
182     setVisible(nameVisible);
183 
184     if (!nameVisible)
185         return;
186 
187     const QFontMetricsF metrics(QGuiApplication::font());
188     QRectF boundingRect = metrics.boundingRect(mObject->name());
189 
190     const qreal margin = Utils::dpiScaled(labelMargin);
191     const qreal distance = Utils::dpiScaled(labelDistance);
192     const qreal textY = -boundingRect.bottom() - margin - distance;
193 
194     boundingRect.translate(-boundingRect.width() / 2, textY);
195 
196     mTextPos = QPointF(boundingRect.left(), textY);
197 
198     boundingRect.adjust(-margin*2, -margin, margin*2, margin);
199 
200     QPointF pixelPos = renderer.pixelToScreenCoords(mObject->position());
201     QRectF bounds = mObject->screenBounds(renderer);
202 
203     // Adjust the bounding box for object rotation
204     bounds = rotateAt(pixelPos, mObject->rotation()).mapRect(bounds);
205 
206     // Center the object name on the object bounding box
207     QPointF pos((bounds.left() + bounds.right()) / 2, bounds.top());
208 
209     if (auto mapScene = static_cast<MapScene*>(scene()))
210         pos += mapScene->absolutePositionForLayer(*mObject->objectGroup());
211 
212     setPos(pos);
213 
214     if (mBoundingRect != boundingRect) {
215         prepareGeometryChange();
216         mBoundingRect = boundingRect;
217     }
218 
219     updateColor();
220 }
221 
updateColor()222 void MapObjectLabel::updateColor()
223 {
224     const QColor color = mObject->effectiveColor();
225     if (mColor != color) {
226         mColor = color;
227         update();
228     }
229 }
230 
boundingRect() const231 QRectF MapObjectLabel::boundingRect() const
232 {
233     return mBoundingRect.adjusted(0, 0, 1, 1);
234 }
235 
paint(QPainter * painter,const QStyleOptionGraphicsItem *,QWidget *)236 void MapObjectLabel::paint(QPainter *painter,
237                            const QStyleOptionGraphicsItem *,
238                            QWidget *)
239 {
240     painter->setRenderHint(QPainter::Antialiasing);
241     painter->setBrush(Qt::black);
242     painter->setPen(Qt::NoPen);
243     painter->drawRoundedRect(mBoundingRect.translated(1, 1), 4, 4);
244     painter->setBrush(mColor);
245     painter->drawRoundedRect(mBoundingRect, 4, 4);
246 
247     painter->drawRoundedRect(mBoundingRect, 4, 4);
248     painter->setPen(Qt::black);
249     painter->drawText(mTextPos + QPointF(1,1), mObject->name());
250     painter->setPen(Qt::white);
251     painter->drawText(mTextPos, mObject->name());
252 }
253 
254 
ObjectSelectionItem(MapDocument * mapDocument,QGraphicsItem * parent)255 ObjectSelectionItem::ObjectSelectionItem(MapDocument *mapDocument,
256                                          QGraphicsItem *parent)
257     : QGraphicsObject(parent)
258     , mMapDocument(mapDocument)
259 {
260     setFlag(QGraphicsItem::ItemHasNoContents);
261 
262     connect(mapDocument, &Document::changed,
263             this, &ObjectSelectionItem::changeEvent);
264     connect(mapDocument, &Document::propertyAdded,
265             this, &ObjectSelectionItem::propertyAdded);
266     connect(mapDocument, &Document::propertyRemoved,
267             this, &ObjectSelectionItem::propertyRemoved);
268     connect(mapDocument, &Document::propertyChanged,
269             this, &ObjectSelectionItem::propertyChanged);
270     connect(mapDocument, &Document::propertiesChanged,
271             this, &ObjectSelectionItem::propertiesChanged);
272 
273     connect(mapDocument, &MapDocument::selectedObjectsChanged,
274             this, &ObjectSelectionItem::selectedObjectsChanged);
275     connect(mapDocument, &MapDocument::aboutToBeSelectedObjectsChanged,
276             this, &ObjectSelectionItem::aboutToBeSelectedObjectsChanged);
277 
278     connect(mapDocument, &MapDocument::mapChanged,
279             this, &ObjectSelectionItem::mapChanged);
280 
281     connect(mapDocument, &MapDocument::layerAdded,
282             this, &ObjectSelectionItem::layerAdded);
283 
284     connect(mapDocument, &MapDocument::layerAboutToBeRemoved,
285             this, &ObjectSelectionItem::layerAboutToBeRemoved);
286 
287     connect(mapDocument, &MapDocument::hoveredMapObjectChanged,
288             this, &ObjectSelectionItem::hoveredMapObjectChanged);
289 
290     connect(mapDocument, &MapDocument::tilesetTilePositioningChanged,
291             this, &ObjectSelectionItem::tilesetTilePositioningChanged);
292 
293     connect(mapDocument, &MapDocument::tileTypeChanged,
294             this, &ObjectSelectionItem::tileTypeChanged);
295 
296     Preferences *prefs = Preferences::instance();
297 
298     connect(prefs, &Preferences::objectLabelVisibilityChanged,
299             this, &ObjectSelectionItem::objectLabelVisibilityChanged);
300     connect(prefs, &Preferences::showObjectReferencesChanged,
301             this, &ObjectSelectionItem::showObjectReferencesChanged);
302     connect(prefs, &Preferences::objectLineWidthChanged,
303             this, &ObjectSelectionItem::objectLineWidthChanged);
304 
305     connect(prefs, &Preferences::objectTypesChanged,
306             this, &ObjectSelectionItem::updateItemColors);
307 
308     if (objectLabelVisibility() == Preferences::AllObjectLabels)
309         addRemoveObjectLabels();
310 
311     if (Preferences::instance()->showObjectReferences())
312         addRemoveObjectReferences();
313 }
314 
~ObjectSelectionItem()315 ObjectSelectionItem::~ObjectSelectionItem()
316 {
317 }
318 
updateItemPositions()319 void ObjectSelectionItem::updateItemPositions()
320 {
321     // A bit of a heavy function, should be called when something changes that
322     // could affect the position of any overlay item (like map change or when
323     // parallax mode is enabled).
324 
325     const MapRenderer &renderer = *mMapDocument->renderer();
326 
327     for (MapObjectLabel *label : qAsConst(mObjectLabels))
328         label->syncWithMapObject(renderer);
329 
330     for (MapObjectOutline *outline : qAsConst(mObjectOutlines))
331         outline->syncWithMapObject(renderer);
332 
333     for (const auto &items : qAsConst(mReferencesBySourceObject)) {
334         for (ObjectReferenceItem *item : items) {
335             item->syncWithSourceObject(renderer);
336             item->syncWithTargetObject(renderer);
337         }
338     }
339 
340     if (mHoveredMapObjectItem)
341         mHoveredMapObjectItem->syncWithMapObject();
342 }
343 
mapRenderer() const344 const MapRenderer &ObjectSelectionItem::mapRenderer() const
345 {
346     return *mMapDocument->renderer();
347 }
348 
changeEvent(const ChangeEvent & event)349 void ObjectSelectionItem::changeEvent(const ChangeEvent &event)
350 {
351     switch (event.type) {
352     case ChangeEvent::LayerChanged:
353         layerChanged(static_cast<const LayerChangeEvent&>(event));
354         break;
355     case ChangeEvent::MapObjectsChanged:
356         syncOverlayItems(static_cast<const MapObjectsChangeEvent&>(event).mapObjects);
357         break;
358     case ChangeEvent::MapObjectsAdded:
359         objectsAdded(static_cast<const MapObjectsEvent&>(event).mapObjects);
360         break;
361     case ChangeEvent::MapObjectsAboutToBeRemoved:
362         objectsAboutToBeRemoved(static_cast<const MapObjectsEvent&>(event).mapObjects);
363         break;
364     case ChangeEvent::ObjectGroupChanged:
365         if (static_cast<const ObjectGroupChangeEvent&>(event).properties & ObjectGroupChangeEvent::ColorProperty)
366             updateItemColors();
367         break;
368     default:
369         break;
370     }
371 }
372 
propertyAdded(Object * object,const QString & name)373 void ObjectSelectionItem::propertyAdded(Object *object, const QString &name)
374 {
375     if (object->typeId() != Object::MapObjectType)
376         return;
377     if (!Preferences::instance()->showObjectReferences())
378         return;
379     if (object->property(name).userType() != objectRefTypeId())
380         return;
381 
382     addRemoveObjectReferences(static_cast<MapObject*>(object));
383 }
384 
propertyRemoved(Object * object,const QString & name)385 void ObjectSelectionItem::propertyRemoved(Object *object, const QString &name)
386 {
387     Q_UNUSED(name)
388 
389     if (object->typeId() != Object::MapObjectType)
390         return;
391     if (!mReferencesBySourceObject.contains(static_cast<MapObject*>(object)))
392         return;
393 
394     addRemoveObjectReferences(static_cast<MapObject*>(object));
395 }
396 
propertyChanged(Object * object,const QString & name)397 void ObjectSelectionItem::propertyChanged(Object *object, const QString &name)
398 {
399     if (object->typeId() != Object::MapObjectType)
400         return;
401     if (!Preferences::instance()->showObjectReferences())
402         return;
403     if (object->property(name).userType() != objectRefTypeId() &&
404             !mReferencesBySourceObject.contains(static_cast<MapObject*>(object)))
405         return;
406 
407     addRemoveObjectReferences(static_cast<MapObject*>(object));
408 }
409 
propertiesChanged(Object * object)410 void ObjectSelectionItem::propertiesChanged(Object *object)
411 {
412     if (object->typeId() != Object::MapObjectType)
413         return;
414     if (!Preferences::instance()->showObjectReferences())
415         return;
416 
417     addRemoveObjectReferences(static_cast<MapObject*>(object));
418 }
419 
selectedObjectsChanged()420 void ObjectSelectionItem::selectedObjectsChanged()
421 {
422     addRemoveObjectLabels();
423     addRemoveObjectOutlines();
424 }
425 
aboutToBeSelectedObjectsChanged()426 void ObjectSelectionItem::aboutToBeSelectedObjectsChanged()
427 {
428     addRemoveObjectHoverItems();
429 }
430 
hoveredMapObjectChanged(MapObject * object,MapObject * previous)431 void ObjectSelectionItem::hoveredMapObjectChanged(MapObject *object,
432                                                   MapObject *previous)
433 {
434     Preferences *prefs = Preferences::instance();
435     auto visibility = prefs->objectLabelVisibility();
436 
437     if (visibility != Preferences::AllObjectLabels) {
438         bool labelForHoveredObject = prefs->labelForHoveredObject();
439 
440         // Make sure any newly hovered object has a label
441         if (object && labelForHoveredObject && !mObjectLabels.contains(object)) {
442             MapObjectLabel *labelItem = new MapObjectLabel(object, this);
443             labelItem->syncWithMapObject(*mMapDocument->renderer());
444             mObjectLabels.insert(object, labelItem);
445         }
446 
447         // Maybe remove the label from the previous object
448         if (MapObjectLabel *label = mObjectLabels.value(previous)) {
449             if (!(visibility == Preferences::SelectedObjectLabels &&
450                   mMapDocument->selectedObjects().contains(previous))) {
451                 delete label;
452                 mObjectLabels.remove(previous);
453             }
454         }
455     }
456 
457     if (object && prefs->highlightHoveredObject()) {
458         mHoveredMapObjectItem = std::make_unique<MapObjectItem>(object, mMapDocument, this);
459         mHoveredMapObjectItem->setEnabled(false);
460         mHoveredMapObjectItem->setIsHoverIndicator(true);
461         mHoveredMapObjectItem->setZValue(hoverZValue);  // show below selection outlines
462     } else {
463         mHoveredMapObjectItem.reset();
464     }
465 }
466 
mapChanged()467 void ObjectSelectionItem::mapChanged()
468 {
469     updateItemPositions();
470 }
471 
collectObjects(const GroupLayer & groupLayer,QList<MapObject * > & objects)472 static void collectObjects(const GroupLayer &groupLayer, QList<MapObject*> &objects)
473 {
474     for (Layer *layer : groupLayer) {
475         switch (layer->layerType()) {
476         case Layer::ObjectGroupType:
477             objects.append(static_cast<ObjectGroup*>(layer)->objects());
478             break;
479         case Layer::GroupLayerType:
480             collectObjects(*static_cast<GroupLayer*>(layer), objects);
481             break;
482         default:
483             break;
484         }
485     }
486 }
487 
layerAdded(Layer * layer)488 void ObjectSelectionItem::layerAdded(Layer *layer)
489 {
490     QList<MapObject*> newObjects;
491 
492     if (auto objectGroup = layer->asObjectGroup())
493         newObjects = objectGroup->objects();
494     else if (auto groupLayer = layer->asGroupLayer())
495         collectObjects(*groupLayer, newObjects);
496 
497     if (newObjects.isEmpty())
498         return;
499 
500     // The layer may already have objects, for example when the addition is the
501     // undo of a removal.
502     if (objectLabelVisibility() == Preferences::AllObjectLabels) {
503         const MapRenderer &renderer = *mMapDocument->renderer();
504 
505         for (MapObject *object : qAsConst(newObjects)) {
506             Q_ASSERT(!mObjectLabels.contains(object));
507 
508             MapObjectLabel *labelItem = new MapObjectLabel(object, this);
509             labelItem->syncWithMapObject(renderer);
510             mObjectLabels.insert(object, labelItem);
511         }
512     }
513 
514     if (Preferences::instance()->showObjectReferences())
515         addRemoveObjectReferences();
516 }
517 
layerAboutToBeRemoved(GroupLayer * parentLayer,int index)518 void ObjectSelectionItem::layerAboutToBeRemoved(GroupLayer *parentLayer, int index)
519 {
520     auto layer = parentLayer ? parentLayer->layerAt(index) : mMapDocument->map()->layerAt(index);
521     if (auto objectGroup = layer->asObjectGroup()) {
522         objectsAboutToBeRemoved(objectGroup->objects());
523     } else if (auto groupLayer = layer->asGroupLayer()) {
524         QList<MapObject*> affectedObjects;
525         collectObjects(*groupLayer, affectedObjects);
526         objectsAboutToBeRemoved(affectedObjects);
527     }
528 }
529 
layerChanged(const LayerChangeEvent & event)530 void ObjectSelectionItem::layerChanged(const LayerChangeEvent &event)
531 {
532     ObjectGroup *objectGroup = event.layer->asObjectGroup();
533     GroupLayer *groupLayer = event.layer->asGroupLayer();
534     if (!(objectGroup || groupLayer))
535         return;
536 
537     // If labels for all objects are visible, some labels may need to be added
538     // or removed based on layer visibility.
539     if (event.properties & LayerChangeEvent::VisibleProperty) {
540         if (objectLabelVisibility() == Preferences::AllObjectLabels)
541             addRemoveObjectLabels();
542 
543         if (Preferences::instance()->showObjectReferences())
544             addRemoveObjectReferences();
545     }
546 
547     // If an object or group layer changed, that means its offset may have
548     // changed, which affects the outlines of selected objects on that layer
549     // and the positions of any name labels that are shown.
550     if (event.properties & LayerChangeEvent::OffsetProperty) {
551         if (objectGroup) {
552             syncOverlayItems(objectGroup->objects());
553         } else {
554             QList<MapObject*> affectedObjects;
555             collectObjects(*groupLayer, affectedObjects);
556             syncOverlayItems(affectedObjects);
557         }
558     }
559 }
560 
syncOverlayItems(const QList<MapObject * > & objects)561 void ObjectSelectionItem::syncOverlayItems(const QList<MapObject*> &objects)
562 {
563     const MapRenderer &renderer = *mMapDocument->renderer();
564 
565     for (MapObject *object : objects) {
566         if (MapObjectOutline *outlineItem = mObjectOutlines.value(object))
567             outlineItem->syncWithMapObject(renderer);
568 
569         if (MapObjectOutline *outlineItem = mObjectHoverItems.value(object))
570             outlineItem->syncWithMapObject(renderer);
571 
572         if (MapObjectLabel *labelItem = mObjectLabels.value(object))
573             labelItem->syncWithMapObject(renderer);
574 
575         const auto sourceItems = mReferencesBySourceObject.value(object);
576         for (auto item : sourceItems)
577             item->syncWithSourceObject(renderer);
578 
579         const auto targetItems = mReferencesByTargetObject.value(object);
580         for (auto item : targetItems)
581             item->syncWithTargetObject(renderer);
582 
583         if (mHoveredMapObjectItem && mHoveredMapObjectItem->mapObject() == object)
584             mHoveredMapObjectItem->syncWithMapObject();
585     }
586 }
587 
updateItemColors() const588 void ObjectSelectionItem::updateItemColors() const
589 {
590     for (MapObjectLabel *label : mObjectLabels)
591         label->updateColor();
592 
593     for (const auto &referenceItems : qAsConst(mReferencesBySourceObject))
594         for (ObjectReferenceItem *item : referenceItems)
595             item->updateColor();
596 }
597 
objectsAdded(const QList<MapObject * > & objects)598 void ObjectSelectionItem::objectsAdded(const QList<MapObject *> &objects)
599 {
600     if (objectLabelVisibility() == Preferences::AllObjectLabels) {
601         const MapRenderer &renderer = *mMapDocument->renderer();
602 
603         for (MapObject *object : objects) {
604             Q_ASSERT(!mObjectLabels.contains(object));
605 
606             MapObjectLabel *labelItem = new MapObjectLabel(object, this);
607             labelItem->syncWithMapObject(renderer);
608             mObjectLabels.insert(object, labelItem);
609         }
610     }
611 
612     // This could be a tad slow if there are many objects, but currently it is
613     // necessary because the list of added objects could include target
614     // objects. To optimize this, we could instantiate reference items also if
615     // the target was not found and only try to repair those here.
616     if (Preferences::instance()->showObjectReferences())
617         addRemoveObjectReferences();
618 }
619 
objectsAboutToBeRemoved(const QList<MapObject * > & objects)620 void ObjectSelectionItem::objectsAboutToBeRemoved(const QList<MapObject *> &objects)
621 {
622     if (objectLabelVisibility() == Preferences::AllObjectLabels)
623         for (MapObject *object : objects)
624             delete mObjectLabels.take(object);
625 
626     for (MapObject *object : objects) {
627         // Remove any references originating from this object
628         auto it = mReferencesBySourceObject.find(object);
629         if (it != mReferencesBySourceObject.end()) {
630             const QList<ObjectReferenceItem*> &items = *it;
631             for (auto item : items) {
632                 auto &itemsByTarget = mReferencesByTargetObject[item->targetObject()];
633                 itemsByTarget.removeOne(item);
634                 if (itemsByTarget.isEmpty())
635                     mReferencesByTargetObject.remove(item->targetObject());
636 
637                 delete item;
638             }
639             mReferencesBySourceObject.erase(it);
640         }
641 
642         // Remove any references pointing to this object
643         it = mReferencesByTargetObject.find(object);
644         if (it != mReferencesByTargetObject.end()) {
645             const QList<ObjectReferenceItem*> &items = *it;
646             for (auto item : items) {
647                 auto &itemsBySource = mReferencesBySourceObject[item->sourceObject()];
648                 itemsBySource.removeOne(item);
649                 if (itemsBySource.isEmpty())
650                     mReferencesBySourceObject.remove(item->sourceObject());
651 
652                 delete item;
653             }
654             mReferencesByTargetObject.erase(it);
655         }
656     }
657 }
658 
tilesetTilePositioningChanged(Tileset * tileset)659 void ObjectSelectionItem::tilesetTilePositioningChanged(Tileset *tileset)
660 {
661     // Tile offset and alignment affect the position of selection outlines and labels
662     const MapRenderer &renderer = *mMapDocument->renderer();
663 
664     for (MapObjectLabel *label : qAsConst(mObjectLabels))
665         if (label->mapObject()->cell().tileset() == tileset)
666             label->syncWithMapObject(renderer);
667 
668     for (MapObjectOutline *outline : qAsConst(mObjectOutlines))
669         if (outline->mapObject()->cell().tileset() == tileset)
670             outline->syncWithMapObject(renderer);
671 
672     if (mHoveredMapObjectItem && mHoveredMapObjectItem->mapObject()->cell().tileset() == tileset)
673         mHoveredMapObjectItem->syncWithMapObject();
674 }
675 
tileTypeChanged(Tile * tile)676 void ObjectSelectionItem::tileTypeChanged(Tile *tile)
677 {
678     auto isObjectAffected = [tile] (const MapObject *object) -> bool {
679         if (!object->type().isEmpty())
680             return false;
681 
682         const auto &cell = object->cell();
683         return cell.tileset() == tile->tileset() && cell.tileId() == tile->id();
684     };
685 
686     for (MapObjectLabel *label : qAsConst(mObjectLabels))
687         if (isObjectAffected(label->mapObject()))
688             label->updateColor();
689 
690     for (auto it = mReferencesByTargetObject.constBegin(), it_end = mReferencesByTargetObject.constEnd(); it != it_end; ++it) {
691         if (isObjectAffected(it.key())) {
692             for (ObjectReferenceItem *item : it.value())
693                 item->updateColor();
694         }
695     }
696 }
697 
objectLabelVisibilityChanged()698 void ObjectSelectionItem::objectLabelVisibilityChanged()
699 {
700     addRemoveObjectLabels();
701 }
702 
showObjectReferencesChanged()703 void ObjectSelectionItem::showObjectReferencesChanged()
704 {
705     addRemoveObjectReferences();
706 }
707 
objectLineWidthChanged()708 void ObjectSelectionItem::objectLineWidthChanged()
709 {
710     // Object reference items should redraw when line width is changed
711     for (const auto &items : qAsConst(mReferencesBySourceObject))
712         for (ObjectReferenceItem *item : items)
713             item->update();
714 }
715 
addRemoveObjectLabels()716 void ObjectSelectionItem::addRemoveObjectLabels()
717 {
718     QHash<MapObject*, MapObjectLabel*> labelItems;
719     const MapRenderer &renderer = *mMapDocument->renderer();
720 
721     auto ensureLabel = [&] (MapObject *object) {
722         if (labelItems.contains(object))
723             return;
724 
725         MapObjectLabel *labelItem = mObjectLabels.take(object);
726         if (!labelItem) {
727             labelItem = new MapObjectLabel(object, this);
728             labelItem->syncWithMapObject(renderer);
729         }
730 
731         labelItems.insert(object, labelItem);
732     };
733 
734     Preferences *prefs = Preferences::instance();
735     if (prefs->labelForHoveredObject())
736         if (MapObject *object = mMapDocument->hoveredMapObject())
737             ensureLabel(object);
738 
739     switch (objectLabelVisibility()) {
740     case Preferences::AllObjectLabels: {
741         LayerIterator iterator(mMapDocument->map());
742         while (Layer *layer = iterator.next()) {
743             if (layer->isHidden())
744                 continue;
745 
746             if (ObjectGroup *objectGroup = layer->asObjectGroup())
747                 for (MapObject *object : objectGroup->objects())
748                     ensureLabel(object);
749         }
750     }
751         // We want labels on selected objects regardless layer visibility
752         Q_FALLTHROUGH();
753 
754     case Preferences::SelectedObjectLabels:
755         for (MapObject *object : mMapDocument->selectedObjects())
756             ensureLabel(object);
757         break;
758 
759     case Preferences::NoObjectLabels:
760         break;
761     }
762 
763     qDeleteAll(mObjectLabels); // delete remaining items
764     mObjectLabels.swap(labelItems);
765 }
766 
addRemoveObjectOutlines()767 void ObjectSelectionItem::addRemoveObjectOutlines()
768 {
769     QHash<MapObject*, MapObjectOutline*> outlineItems;
770     const MapRenderer &renderer = *mMapDocument->renderer();
771 
772     for (MapObject *mapObject : mMapDocument->selectedObjects()) {
773         MapObjectOutline *outlineItem = mObjectOutlines.take(mapObject);
774         if (!outlineItem) {
775             outlineItem = new MapObjectOutline(mapObject, MapObjectOutline::SelectionIndicator, this);
776             outlineItem->syncWithMapObject(renderer);
777         }
778         outlineItems.insert(mapObject, outlineItem);
779     }
780 
781     qDeleteAll(mObjectOutlines); // delete remaining items
782     mObjectOutlines.swap(outlineItems);
783 }
784 
addRemoveObjectHoverItems()785 void ObjectSelectionItem::addRemoveObjectHoverItems()
786 {
787     QHash<MapObject*, MapObjectOutline*> hoverItems;
788     const MapRenderer &renderer = *mMapDocument->renderer();
789 
790     for (MapObject *mapObject : mMapDocument->aboutToBeSelectedObjects()) {
791         auto hoverItem = mObjectHoverItems.take(mapObject);
792         if (!hoverItem) {
793             hoverItem = new MapObjectOutline(mapObject, MapObjectOutline::HoverIndicator, this);
794             hoverItem->syncWithMapObject(renderer);
795             hoverItem->setEnabled(false);
796         }
797         hoverItems.insert(mapObject, hoverItem);
798     }
799 
800     qDeleteAll(mObjectHoverItems); // delete remaining items
801     mObjectHoverItems.swap(hoverItems);
802 }
803 
addRemoveObjectReferences()804 void ObjectSelectionItem::addRemoveObjectReferences()
805 {
806     QHash<MapObject*, QList<ObjectReferenceItem*>> referencesBySourceObject;
807     QHash<MapObject*, QList<ObjectReferenceItem*>> referencesByTargetObject;
808     const MapRenderer &renderer = *mMapDocument->renderer();
809 
810     auto ensureReferenceItem = [&] (MapObject *sourceObject, const QString &property, ObjectRef ref) {
811         MapObject *targetObject = DisplayObjectRef(ref, mMapDocument).object();
812         if (!targetObject)
813             return;
814 
815         QList<ObjectReferenceItem*> &items = referencesBySourceObject[sourceObject];
816 
817         if (mReferencesBySourceObject.contains(sourceObject)) {
818             QList<ObjectReferenceItem*> &existingItems = mReferencesBySourceObject[sourceObject];
819             auto it = std::find_if(existingItems.begin(),
820                                    existingItems.end(),
821                                    [=] (ObjectReferenceItem *item) {
822                 return item->targetObject() == targetObject
823                         && item->property() == property;
824             });
825 
826             if (it != existingItems.end()) {
827                 items.append(*it);
828                 referencesByTargetObject[targetObject].append(*it);
829 
830                 existingItems.erase(it);
831                 return;
832             }
833         }
834 
835         auto item = new ObjectReferenceItem(sourceObject, targetObject, property, this);
836         item->syncWithSourceObject(renderer);
837         item->syncWithTargetObject(renderer);
838         items.append(item);
839         referencesByTargetObject[targetObject].append(item);
840     };
841 
842     if (Preferences::instance()->showObjectReferences()) {
843         LayerIterator iterator(mMapDocument->map());
844         while (Layer *layer = iterator.next()) {
845             if (layer->isHidden())
846                 continue;
847 
848             if (ObjectGroup *objectGroup = layer->asObjectGroup()) {
849                 for (MapObject *object : objectGroup->objects()) {
850                     const auto &props = object->properties();
851                     for (auto it = props.cbegin(), it_end = props.cend(); it != it_end; ++it) {
852                         if (it->userType() == objectRefTypeId())
853                             ensureReferenceItem(object, it.key(), it->value<ObjectRef>());
854                     }
855                 }
856             }
857         }
858     }
859 
860     // delete remaining items
861     for (const auto &items : mReferencesBySourceObject)
862         qDeleteAll(items);
863 
864     mReferencesBySourceObject.swap(referencesBySourceObject);
865     mReferencesByTargetObject.swap(referencesByTargetObject);
866 }
867 
addRemoveObjectReferences(MapObject * object)868 void ObjectSelectionItem::addRemoveObjectReferences(MapObject *object)
869 {
870     QList<ObjectReferenceItem*> &items = mReferencesBySourceObject[object];
871     QList<ObjectReferenceItem*> existingItems;
872     items.swap(existingItems);
873 
874     const MapRenderer &renderer = *mMapDocument->renderer();
875 
876     auto ensureReferenceItem = [&] (MapObject *sourceObject, const QString &property, ObjectRef ref) {
877         MapObject *targetObject = DisplayObjectRef(ref, mMapDocument).object();
878         if (!targetObject)
879             return;
880 
881         auto it = std::find_if(existingItems.begin(),
882                                existingItems.end(),
883                                [=] (ObjectReferenceItem *item) {
884             return item->targetObject() == targetObject
885                     && item->property() == property;
886         });
887 
888         if (it != existingItems.end()) {
889             items.append(*it);
890             existingItems.erase(it);
891             return;
892         }
893 
894         auto item = new ObjectReferenceItem(sourceObject, targetObject, property, this);
895         item->syncWithSourceObject(renderer);
896         item->syncWithTargetObject(renderer);
897         items.append(item);
898         mReferencesByTargetObject[targetObject].append(item);
899     };
900 
901     if (Preferences::instance()->showObjectReferences()) {
902         const auto &props = object->properties();
903         for (auto it = props.cbegin(), it_end = props.cend(); it != it_end; ++it) {
904             if (it->userType() == objectRefTypeId())
905                 ensureReferenceItem(object, it.key(), it->value<ObjectRef>());
906         }
907     }
908 
909     // Delete remaining existing items, also removing them from mReferencesByTargetObject
910     for (ObjectReferenceItem *item : qAsConst(existingItems)) {
911         auto &itemsByTarget = mReferencesByTargetObject[item->targetObject()];
912         itemsByTarget.removeOne(item);
913         if (itemsByTarget.isEmpty())
914             mReferencesByTargetObject.remove(item->targetObject());
915 
916         delete item;
917     }
918 }
919 
920 } // namespace Tiled
921 
922 #include "moc_objectselectionitem.cpp"
923