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