1 /*
2  * tilesetview.cpp
3  * Copyright 2008-2010, Thorbjørn Lindeijer <thorbjorn@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 "tilesetview.h"
22 
23 #include "actionmanager.h"
24 #include "changeevents.h"
25 #include "changetilewangid.h"
26 #include "map.h"
27 #include "preferences.h"
28 #include "stylehelper.h"
29 #include "tile.h"
30 #include "tileset.h"
31 #include "tilesetdocument.h"
32 #include "tilesetmodel.h"
33 #include "utils.h"
34 #include "wangoverlay.h"
35 #include "zoomable.h"
36 
37 #include <QAbstractItemDelegate>
38 #include <QApplication>
39 #include <QCoreApplication>
40 #include <QGesture>
41 #include <QGestureEvent>
42 #include <QHeaderView>
43 #include <QMenu>
44 #include <QPainter>
45 #include <QPainterPath>
46 #include <QPinchGesture>
47 #include <QScrollBar>
48 #include <QUndoCommand>
49 #include <QWheelEvent>
50 #include <QtCore/qmath.h>
51 
52 #include <QDebug>
53 
54 using namespace Tiled;
55 
56 namespace {
57 
setupTilesetGridTransform(const Tileset & tileset,QTransform & transform,QRect & targetRect)58 static void setupTilesetGridTransform(const Tileset &tileset, QTransform &transform, QRect &targetRect)
59 {
60     if (tileset.orientation() == Tileset::Isometric) {
61         const QPoint tileCenter = targetRect.center();
62         targetRect.setHeight(targetRect.width());
63         targetRect.moveCenter(tileCenter);
64 
65         const QSize gridSize = tileset.gridSize();
66 
67         transform.translate(tileCenter.x(), tileCenter.y());
68 
69         const auto ratio = (qreal) gridSize.height() / gridSize.width();
70         const auto scaleX = 1.0 / sqrt(2.0);
71         const auto scaleY = scaleX * ratio;
72         transform.scale(scaleX, scaleY);
73 
74         transform.rotate(45.0);
75 
76         transform.translate(-tileCenter.x(), -tileCenter.y());
77     }
78 }
79 
80 /**
81  * The delegate for drawing tile items in the tileset view.
82  */
83 class TileDelegate : public QAbstractItemDelegate
84 {
85 public:
TileDelegate(TilesetView * tilesetView,QObject * parent=nullptr)86     TileDelegate(TilesetView *tilesetView, QObject *parent = nullptr)
87         : QAbstractItemDelegate(parent)
88         , mTilesetView(tilesetView)
89     { }
90 
91     void paint(QPainter *painter, const QStyleOptionViewItem &option,
92                const QModelIndex &index) const override;
93 
94     QSize sizeHint(const QStyleOptionViewItem &option,
95                    const QModelIndex &index) const override;
96 
97 private:
98     void drawFilmStrip(QPainter *painter, QRect targetRect) const;
99     void drawWangOverlay(QPainter *painter,
100                          const Tile *tile,
101                          QRect targetRect,
102                          const QModelIndex &index) const;
103 
104     TilesetView *mTilesetView;
105 };
106 
paint(QPainter * painter,const QStyleOptionViewItem & option,const QModelIndex & index) const107 void TileDelegate::paint(QPainter *painter,
108                          const QStyleOptionViewItem &option,
109                          const QModelIndex &index) const
110 {
111     const TilesetModel *model = static_cast<const TilesetModel*>(index.model());
112     const Tile *tile = model->tileAt(index);
113     if (!tile)
114         return;
115 
116     const QPixmap &tileImage = tile->image();
117     const int extra = mTilesetView->drawGrid() ? 1 : 0;
118     const qreal zoom = mTilesetView->scale();
119     const bool wrapping = mTilesetView->dynamicWrapping();
120 
121     QSize tileSize = tileImage.size();
122     if (tileImage.isNull()) {
123         Tileset *tileset = model->tileset();
124         if (tileset->isCollection()) {
125             tileSize = QSize(32, 32);
126         } else {
127             int max = std::max(tileset->tileWidth(), tileset->tileHeight());
128             int min = std::min(max, 32);
129             tileSize = QSize(min, min);
130         }
131     }
132 
133     // Compute rectangle to draw the image in: bottom- and left-aligned
134     QRect targetRect = option.rect.adjusted(0, 0, -extra, -extra);
135 
136     if (wrapping) {
137         qreal scale = std::min(static_cast<qreal>(targetRect.width()) / tileSize.width(),
138                                static_cast<qreal>(targetRect.height()) / tileSize.height());
139         tileSize *= scale;
140 
141         auto center = targetRect.center();
142         targetRect.setSize(tileSize);
143         targetRect.moveCenter(center);
144     } else {
145         tileSize *= zoom;
146         targetRect.setTop(targetRect.bottom() - tileSize.height() + 1);
147         targetRect.setRight(targetRect.left() + tileSize.width() - 1);
148     }
149 
150     // Draw the tile image
151     if (Zoomable *zoomable = mTilesetView->zoomable())
152         if (zoomable->smoothTransform())
153             painter->setRenderHint(QPainter::SmoothPixmapTransform);
154 
155     if (!tileImage.isNull())
156         painter->drawPixmap(targetRect, tileImage);
157     else
158         mTilesetView->imageMissingIcon().paint(painter, targetRect, Qt::AlignBottom | Qt::AlignLeft);
159 
160 
161     // Overlay with film strip when animated
162     if (mTilesetView->markAnimatedTiles() && tile->isAnimated())
163         drawFilmStrip(painter, targetRect);
164 
165     const auto highlight = option.palette.highlight();
166 
167     // Overlay with highlight color when selected
168     if (option.state & QStyle::State_Selected) {
169         const qreal opacity = painter->opacity();
170         painter->setOpacity(0.5);
171         painter->fillRect(targetRect, highlight);
172         painter->setOpacity(opacity);
173     }
174 
175     if (mTilesetView->isEditWangSet())
176         drawWangOverlay(painter, tile, targetRect, index);
177 }
178 
sizeHint(const QStyleOptionViewItem &,const QModelIndex & index) const179 QSize TileDelegate::sizeHint(const QStyleOptionViewItem & /* option */,
180                              const QModelIndex &index) const
181 {
182     const TilesetModel *m = static_cast<const TilesetModel*>(index.model());
183     const int extra = mTilesetView->drawGrid() ? 1 : 0;
184     const qreal scale = mTilesetView->scale();
185 
186     if (const Tile *tile = m->tileAt(index)) {
187         if (mTilesetView->dynamicWrapping()) {
188             Tileset *tileset = tile->tileset();
189             return QSize(tileset->tileWidth() * scale + extra,
190                          tileset->tileHeight() * scale + extra);
191         }
192 
193         const QPixmap &image = tile->image();
194         QSize tileSize = image.size();
195 
196         if (image.isNull()) {
197             Tileset *tileset = m->tileset();
198             if (tileset->isCollection()) {
199                 tileSize = QSize(32, 32);
200             } else {
201                 int max = std::max(tileset->tileWidth(), tileset->tileWidth());
202                 int min = std::min(max, 32);
203                 tileSize = QSize(min, min);
204             }
205         }
206 
207         return QSize(tileSize.width() * scale + extra,
208                      tileSize.height() * scale + extra);
209     }
210 
211     return QSize(extra, extra);
212 }
213 
drawFilmStrip(QPainter * painter,QRect targetRect) const214 void TileDelegate::drawFilmStrip(QPainter *painter, QRect targetRect) const
215 {
216     painter->save();
217 
218     qreal scale = qMin(targetRect.width() / 32.0,
219                        targetRect.height() / 32.0);
220 
221     painter->setClipRect(targetRect);
222     painter->translate(targetRect.right(),
223                        targetRect.bottom());
224     painter->scale(scale, scale);
225     painter->translate(-18, 3);
226     painter->rotate(-45);
227     painter->setOpacity(0.8);
228 
229     QRectF strip(0, 0, 32, 6);
230     painter->fillRect(strip, Qt::black);
231 
232     painter->setRenderHint(QPainter::Antialiasing);
233     painter->setBrush(Qt::white);
234     painter->setPen(Qt::NoPen);
235 
236     QRectF hole(0, 0, strip.height() * 0.6, strip.height() * 0.6);
237     qreal step = (strip.height() - hole.height()) + hole.width();
238     qreal margin = (strip.height() - hole.height()) / 2;
239 
240     for (qreal x = (step - hole.width()) / 2; x < strip.right(); x += step) {
241         hole.moveTo(x, margin);
242         painter->drawRoundedRect(hole, 25, 25, Qt::RelativeSize);
243     }
244 
245     painter->restore();
246 }
247 
drawWangOverlay(QPainter * painter,const Tile * tile,QRect targetRect,const QModelIndex & index) const248 void TileDelegate::drawWangOverlay(QPainter *painter,
249                                    const Tile *tile,
250                                    QRect targetRect,
251                                    const QModelIndex &index) const
252 {
253     WangSet *wangSet = mTilesetView->wangSet();
254     if (!wangSet)
255         return;
256 
257     painter->save();
258 
259     QTransform transform;
260     setupTilesetGridTransform(*tile->tileset(), transform, targetRect);
261     painter->setTransform(transform, true);
262 
263     paintWangOverlay(painter, wangSet->wangIdOfTile(tile),
264                      *wangSet,
265                      targetRect);
266 
267     if (mTilesetView->hoveredIndex() == index) {
268         qreal opacity = painter->opacity();
269         painter->setOpacity(0.5);
270         paintWangOverlay(painter, mTilesetView->wangId(),
271                          *wangSet,
272                          targetRect,
273                          WangOverlayOptions());
274         painter->setOpacity(opacity);
275     }
276 
277     painter->restore();
278 }
279 
280 } // anonymous namespace
281 
TilesetView(QWidget * parent)282 TilesetView::TilesetView(QWidget *parent)
283     : QTableView(parent)
284     , mZoomable(new Zoomable(this))
285     , mImageMissingIcon(QStringLiteral("://images/32/image-missing.png"))
286 {
287     setHorizontalScrollMode(QAbstractItemView::ScrollPerPixel);
288     setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
289     setItemDelegate(new TileDelegate(this, this));
290     setShowGrid(false);
291     setTabKeyNavigation(false);
292     setDropIndicatorShown(true);
293 
294     QHeaderView *hHeader = horizontalHeader();
295     QHeaderView *vHeader = verticalHeader();
296     hHeader->hide();
297     vHeader->hide();
298     hHeader->setSectionResizeMode(QHeaderView::ResizeToContents);
299     vHeader->setSectionResizeMode(QHeaderView::ResizeToContents);
300     hHeader->setMinimumSectionSize(1);
301     vHeader->setMinimumSectionSize(1);
302 
303     // Hardcode this view on 'left to right' since it doesn't work properly
304     // for 'right to left' languages.
305     setLayoutDirection(Qt::LeftToRight);
306 
307     Preferences *prefs = Preferences::instance();
308     mDrawGrid = prefs->showTilesetGrid();
309 
310     grabGesture(Qt::PinchGesture);
311 
312     connect(prefs, &Preferences::showTilesetGridChanged,
313             this, &TilesetView::setDrawGrid);
314 
315     connect(StyleHelper::instance(), &StyleHelper::styleApplied,
316             this, &TilesetView::updateBackgroundColor);
317 
318     connect(mZoomable, &Zoomable::scaleChanged, this, &TilesetView::adjustScale);
319 }
320 
setTilesetDocument(TilesetDocument * tilesetDocument)321 void TilesetView::setTilesetDocument(TilesetDocument *tilesetDocument)
322 {
323     if (mTilesetDocument)
324         mTilesetDocument->disconnect(this);
325 
326     mTilesetDocument = tilesetDocument;
327 
328     if (mTilesetDocument)
329         connect(mTilesetDocument, &Document::changed, this, &TilesetView::onChange);
330 }
331 
sizeHint() const332 QSize TilesetView::sizeHint() const
333 {
334     return Utils::dpiScaled(QSize(260, 100));
335 }
336 
sizeHintForColumn(int column) const337 int TilesetView::sizeHintForColumn(int column) const
338 {
339     Q_UNUSED(column)
340     const TilesetModel *model = tilesetModel();
341     if (!model)
342         return -1;
343     if (model->tileset()->isCollection())
344         return QTableView::sizeHintForColumn(column);
345 
346     const int gridSpace = mDrawGrid ? 1 : 0;
347     if (dynamicWrapping())
348         return model->tileset()->tileWidth() * scale() + gridSpace;
349 
350     const int tileWidth = model->tileset()->tileWidth();
351     return qRound(tileWidth * scale()) + gridSpace;
352 }
353 
sizeHintForRow(int row) const354 int TilesetView::sizeHintForRow(int row) const
355 {
356     Q_UNUSED(row)
357     const TilesetModel *model = tilesetModel();
358     if (!model)
359         return -1;
360     if (model->tileset()->isCollection())
361         return QTableView::sizeHintForRow(row);
362 
363     const int gridSpace = mDrawGrid ? 1 : 0;
364     if (dynamicWrapping())
365         return model->tileset()->tileHeight() * scale() + gridSpace;
366 
367     const int tileHeight = model->tileset()->tileHeight();
368     return qRound(tileHeight * scale()) + gridSpace;
369 }
370 
scale() const371 qreal TilesetView::scale() const
372 {
373     return mZoomable->scale();
374 }
375 
setDynamicWrapping(bool enabled)376 void TilesetView::setDynamicWrapping(bool enabled)
377 {
378     WrapBehavior behavior = enabled ? WrapDynamic : WrapFixed;
379     if (mWrapBehavior == behavior)
380         return;
381 
382     mWrapBehavior = behavior;
383     setVerticalScrollBarPolicy(dynamicWrapping() ? Qt::ScrollBarAlwaysOn
384                                                  : Qt::ScrollBarAsNeeded);
385     scheduleDelayedItemsLayout();
386     refreshColumnCount();
387 }
388 
dynamicWrapping() const389 bool TilesetView::dynamicWrapping() const
390 {
391     switch (mWrapBehavior) {
392     case WrapDefault:
393         if (tilesetModel())
394             return tilesetModel()->tileset()->isCollection();
395         break;
396     case WrapDynamic:
397         return true;
398     case WrapFixed:
399         return false;
400     }
401 
402     return false;
403 }
404 
setModel(QAbstractItemModel * model)405 void TilesetView::setModel(QAbstractItemModel *model)
406 {
407     QTableView::setModel(model);
408     updateBackgroundColor();
409     setVerticalScrollBarPolicy(dynamicWrapping() ? Qt::ScrollBarAlwaysOn : Qt::ScrollBarAsNeeded);
410     refreshColumnCount();
411 }
412 
setMarkAnimatedTiles(bool enabled)413 void TilesetView::setMarkAnimatedTiles(bool enabled)
414 {
415     if (mMarkAnimatedTiles == enabled)
416         return;
417 
418     mMarkAnimatedTiles = enabled;
419     viewport()->update();
420 }
421 
event(QEvent * event)422 bool TilesetView::event(QEvent *event)
423 {
424     if (event->type() == QEvent::Gesture) {
425         QGestureEvent *gestureEvent = static_cast<QGestureEvent *>(event);
426         if (QGesture *gesture = gestureEvent->gesture(Qt::PinchGesture))
427             mZoomable->handlePinchGesture(static_cast<QPinchGesture *>(gesture));
428     } else if (event->type() == QEvent::ShortcutOverride) {
429         auto keyEvent = static_cast<QKeyEvent*>(event);
430         if (Utils::isZoomInShortcut(keyEvent) ||
431                 Utils::isZoomOutShortcut(keyEvent) ||
432                 Utils::isResetZoomShortcut(keyEvent)) {
433             event->accept();
434             return true;
435         }
436     }
437 
438     return QTableView::event(event);
439 }
440 
keyPressEvent(QKeyEvent * event)441 void TilesetView::keyPressEvent(QKeyEvent *event)
442 {
443     if (Utils::isZoomInShortcut(event)) {
444         mZoomable->zoomIn();
445         return;
446     }
447     if (Utils::isZoomOutShortcut(event)) {
448         mZoomable->zoomOut();
449         return;
450     }
451     if (Utils::isResetZoomShortcut(event)) {
452         mZoomable->resetZoom();
453         return;
454     }
455 
456     // TODO: These shortcuts only work while the TilesetView is focused. It
457     // would be preferable if they could be used more globally.
458     if (mEditWangSet && mWangBehavior == AssignWholeId && !(event->modifiers() & Qt::ControlModifier)) {
459         WangId transformedWangId = mWangId;
460 
461         if (event->key() == Qt::Key_Z) {
462             if (event->modifiers() & Qt::ShiftModifier)
463                 transformedWangId.rotate(-1);
464             else
465                 transformedWangId.rotate(1);
466         } else if (event->key() == Qt::Key_X) {
467             transformedWangId.flipHorizontally();
468         } else if (event->key() == Qt::Key_Y) {
469             transformedWangId.flipVertically();
470         }
471 
472         if (mWangId != transformedWangId) {
473             setWangId(transformedWangId);
474             emit currentWangIdChanged(mWangId);
475             return;
476         }
477     }
478 
479     return QTableView::keyPressEvent(event);
480 }
481 
setRelocateTiles(bool enabled)482 void TilesetView::setRelocateTiles(bool enabled)
483 {
484     if (mRelocateTiles == enabled)
485         return;
486 
487     mRelocateTiles = enabled;
488 
489     if (enabled)
490         setDragDropMode(QTableView::InternalMove);
491     else
492         setDragDropMode(QTableView::NoDragDrop);
493 
494     setMouseTracking(true);
495     viewport()->update();
496 }
497 
setEditWangSet(bool enabled)498 void TilesetView::setEditWangSet(bool enabled)
499 {
500     if (mEditWangSet == enabled)
501         return;
502 
503     mEditWangSet = enabled;
504     setMouseTracking(true);
505     viewport()->update();
506 }
507 
setWangSet(WangSet * wangSet)508 void TilesetView::setWangSet(WangSet *wangSet)
509 {
510     if (mWangSet == wangSet)
511         return;
512 
513     mWangSet = wangSet;
514 
515     if (mEditWangSet)
516         viewport()->update();
517 }
518 
519 /**
520  * Sets the WangId and changes WangBehavior to WholeId.
521  */
setWangId(WangId wangId)522 void TilesetView::setWangId(WangId wangId)
523 {
524     mWangId = wangId;
525     mWangBehavior = AssignWholeId;
526 
527     if (mEditWangSet && hoveredIndex().isValid())
528         update(hoveredIndex());
529 }
530 
531 /**
532  * Sets the wangColor, and changes WangBehavior depending on the type of the
533  * WangSet.
534  */
setWangColor(int color)535 void TilesetView::setWangColor(int color)
536 {
537     mWangColorIndex = color;
538     mWangBehavior = AssignHoveredIndex;
539 }
540 
imageMissingIcon() const541 QIcon TilesetView::imageMissingIcon() const
542 {
543     return QIcon::fromTheme(QLatin1String("image-missing"), mImageMissingIcon);
544 }
545 
mousePressEvent(QMouseEvent * event)546 void TilesetView::mousePressEvent(QMouseEvent *event)
547 {
548     if (event->button() == Qt::MiddleButton && isActiveWindow()) {
549         mLastMousePos = event->globalPos();
550         setHandScrolling(true);
551         return;
552     }
553 
554     if (mEditWangSet) {
555         if (event->button() == Qt::LeftButton)
556             applyWangId();
557 
558         return;
559     }
560 
561     QTableView::mousePressEvent(event);
562 }
563 
mouseMoveEvent(QMouseEvent * event)564 void TilesetView::mouseMoveEvent(QMouseEvent *event)
565 {
566     if (mHandScrolling) {
567         auto *hBar = horizontalScrollBar();
568         auto *vBar = verticalScrollBar();
569         const QPoint d = event->globalPos() - mLastMousePos;
570 
571         int horizontalValue = hBar->value() + (isRightToLeft() ? d.x() : -d.x());
572         int verticalValue = vBar->value() - d.y();
573 
574         hBar->setValue(horizontalValue);
575         vBar->setValue(verticalValue);
576 
577         mLastMousePos = event->globalPos();
578         return;
579     }
580 
581     if (mEditWangSet) {
582         if (!mWangSet)
583             return;
584 
585         const QPoint pos = event->pos();
586         const QModelIndex hoveredIndex = indexAt(pos);
587         const QModelIndex previousHoveredIndex = mHoveredIndex;
588         mHoveredIndex = hoveredIndex;
589 
590         WangId wangId;
591 
592         if (mWangBehavior == AssignWholeId) {
593             wangId = mWangId;
594         } else {
595             QRect tileRect = visualRect(mHoveredIndex);
596             QTransform transform;
597             setupTilesetGridTransform(*tilesetDocument()->tileset(), transform, tileRect);
598 
599             const auto mappedPos = transform.inverted().map(pos);
600             QPoint tileLocalPos = mappedPos - tileRect.topLeft();
601             QPointF tileLocalPosF((qreal) tileLocalPos.x() / tileRect.width(),
602                                   (qreal) tileLocalPos.y() / tileRect.height());
603 
604             const int x = qBound(0, qFloor(tileLocalPosF.x() * 3), 2);
605             const int y = qBound(0, qFloor(tileLocalPosF.y() * 3), 2);
606             WangId::Index index = WangId::indexByGrid(x, y);
607 
608             if (index != WangId::NumIndexes) {  // center is dead zone
609                 switch (mWangSet->type()) {
610                 case WangSet::Edge:
611                     tileLocalPosF -= QPointF(0.5, 0.5);
612 
613                     if (tileLocalPosF.x() < tileLocalPosF.y()) {
614                         if (tileLocalPosF.x() > -tileLocalPosF.y())
615                             index = WangId::Bottom;
616                         else
617                             index = WangId::Left;
618                     } else {
619                         if (tileLocalPosF.x() > -tileLocalPosF.y())
620                             index = WangId::Right;
621                         else
622                             index = WangId::Top;
623                     }
624                     break;
625                 case WangSet::Corner:
626                     if (tileLocalPosF.x() > 0.5) {
627                         if (tileLocalPosF.y() > 0.5)
628                             index = WangId::BottomRight;
629                         else
630                             index = WangId::TopRight;
631                     } else {
632                         if (tileLocalPosF.y() > 0.5)
633                             index = WangId::BottomLeft;
634                         else
635                             index = WangId::TopLeft;
636                     }
637                     break;
638                 case WangSet::Mixed:
639                     break;
640                 }
641 
642                 wangId.setIndexColor(index, mWangColorIndex ? mWangColorIndex
643                                                             : WangId::INDEX_MASK);
644             }
645         }
646 
647         if (previousHoveredIndex != mHoveredIndex || wangId != mWangId) {
648             mWangId = wangId;
649 
650             if (previousHoveredIndex.isValid())
651                 update(previousHoveredIndex);
652             if (mHoveredIndex.isValid())
653                 update(mHoveredIndex);
654         }
655 
656         if (event->buttons() & Qt::LeftButton)
657             applyWangId();
658 
659         return;
660     }
661 
662     QTableView::mouseMoveEvent(event);
663 }
664 
mouseReleaseEvent(QMouseEvent * event)665 void TilesetView::mouseReleaseEvent(QMouseEvent *event)
666 {
667     if (event->button() == Qt::MiddleButton) {
668         setHandScrolling(false);
669         return;
670     }
671 
672     if (mEditWangSet) {
673         if (event->button() == Qt::LeftButton)
674             finishWangIdChange();
675 
676         return;
677     }
678 
679     QTableView::mouseReleaseEvent(event);
680 }
681 
leaveEvent(QEvent * event)682 void TilesetView::leaveEvent(QEvent *event)
683 {
684     if (mHoveredIndex.isValid()) {
685         const QModelIndex previousHoveredIndex = mHoveredIndex;
686         mHoveredIndex = QModelIndex();
687         update(previousHoveredIndex);
688     }
689 
690     QTableView::leaveEvent(event);
691 }
692 
693 /**
694  * Override to support zooming in and out using the mouse wheel, as well as to
695  * make the scrolling speed independent of Ctrl modifier and zoom level.
696  */
wheelEvent(QWheelEvent * event)697 void TilesetView::wheelEvent(QWheelEvent *event)
698 {
699     auto hor = horizontalScrollBar();
700     auto ver = verticalScrollBar();
701 
702     bool wheelZoomsByDefault = !dynamicWrapping() && Preferences::instance()->wheelZoomsByDefault();
703     bool control = event->modifiers() & Qt::ControlModifier;
704 
705     if ((wheelZoomsByDefault != control) && event->angleDelta().y()) {
706 
707 #if QT_VERSION < QT_VERSION_CHECK(5,14,0)
708         const QPointF &viewportPos = event->posF();
709 #else
710         const QPointF &viewportPos = event->position();
711 #endif
712         const QPointF contentPos(viewportPos.x() + hor->value(),
713                                  viewportPos.y() + ver->value());
714 
715         QPointF relativeContentPos;
716 
717         const QSize oldContentSize = viewportSizeHint();
718         if (!oldContentSize.isEmpty()) {
719             relativeContentPos = QPointF(contentPos.x() / oldContentSize.width(),
720                                          contentPos.y() / oldContentSize.height());
721         }
722 
723         mZoomable->handleWheelDelta(event->angleDelta().y());
724 
725         executeDelayedItemsLayout();
726 
727         const QSize newContentSizeHint = viewportSizeHint();
728         const QPointF newContentPos(relativeContentPos.x() * newContentSizeHint.width(),
729                                     relativeContentPos.y() * newContentSizeHint.height());
730 
731         hor->setValue(newContentPos.x() - viewportPos.x());
732         ver->setValue(newContentPos.y() - viewportPos.y());
733         return;
734     }
735 
736     QPoint delta = event->pixelDelta();
737     if (delta.isNull())
738         delta = Utils::dpiScaled(event->angleDelta());
739 
740     if (delta.x())
741         hor->setValue(hor->value() - delta.x());
742     if (delta.y())
743         ver->setValue(ver->value() - delta.y());
744 }
745 
746 /**
747  * Allow changing tile properties through a context menu.
748  */
contextMenuEvent(QContextMenuEvent * event)749 void TilesetView::contextMenuEvent(QContextMenuEvent *event)
750 {
751     const QModelIndex index = indexAt(event->pos());
752     const TilesetModel *model = tilesetModel();
753     if (!model)
754         return;
755 
756     Tile *tile = model->tileAt(index);
757 
758     QMenu menu;
759 
760     QIcon propIcon(QLatin1String(":images/16/document-properties.png"));
761 
762     if (tile) {
763         if (mEditWangSet) {
764             selectionModel()->setCurrentIndex(index,
765                                               QItemSelectionModel::SelectCurrent |
766                                               QItemSelectionModel::Clear);
767 
768             if (mWangSet) {
769                 QAction *setImage = menu.addAction(tr("Use as Terrain Set Image"));
770                 connect(setImage, &QAction::triggered, this, &TilesetView::selectWangSetImage);
771             }
772             if (mWangBehavior != AssignWholeId && mWangColorIndex) {
773                 QAction *setImage = menu.addAction(tr("Use as Terrain Image"));
774                 connect(setImage, &QAction::triggered, this, &TilesetView::selectWangColorImage);
775             }
776         } else if (mTilesetDocument) {
777             QAction *tileProperties = menu.addAction(propIcon,
778                                                      tr("Tile &Properties..."));
779             Utils::setThemeIcon(tileProperties, "document-properties");
780             connect(tileProperties, &QAction::triggered, this, &TilesetView::editTileProperties);
781         } else {
782             // Assuming we're used in the MapEditor
783 
784             // Enable "swap" if there are exactly 2 tiles selected
785             bool exactlyTwoTilesSelected =
786                     (selectionModel()->selectedIndexes().size() == 2);
787 
788             QAction *swapTilesAction = menu.addAction(tr("&Swap Tiles"));
789             swapTilesAction->setEnabled(exactlyTwoTilesSelected);
790             connect(swapTilesAction, &QAction::triggered, this, &TilesetView::swapTiles);
791         }
792 
793         menu.addSeparator();
794     }
795 
796     QAction *toggleGrid = menu.addAction(tr("Show &Grid"));
797     toggleGrid->setCheckable(true);
798     toggleGrid->setChecked(mDrawGrid);
799 
800     Preferences *prefs = Preferences::instance();
801     connect(toggleGrid, &QAction::toggled,
802             prefs, &Preferences::setShowTilesetGrid);
803 
804     ActionManager::applyMenuExtensions(&menu, MenuIds::tilesetViewTiles);
805 
806     menu.exec(event->globalPos());
807 }
808 
resizeEvent(QResizeEvent * event)809 void TilesetView::resizeEvent(QResizeEvent *event)
810 {
811     QTableView::resizeEvent(event);
812     refreshColumnCount();
813 }
814 
onChange(const ChangeEvent & change)815 void TilesetView::onChange(const ChangeEvent &change)
816 {
817     switch (change.type) {
818     case ChangeEvent::WangSetChanged: {
819         auto &wangSetChange = static_cast<const WangSetChangeEvent&>(change);
820         if (mEditWangSet && wangSetChange.wangSet == mWangSet &&
821                 (wangSetChange.properties & WangSetChangeEvent::TypeProperty)) {
822             viewport()->update();
823         }
824         break;
825     }
826     default:
827         break;
828     }
829 }
830 
selectWangSetImage()831 void TilesetView::selectWangSetImage()
832 {
833     if (Tile *tile = currentTile())
834         emit wangSetImageSelected(tile);
835 }
836 
selectWangColorImage()837 void TilesetView::selectWangColorImage()
838 {
839     if (Tile *tile = currentTile())
840         emit wangColorImageSelected(tile, mWangColorIndex);
841 }
842 
editTileProperties()843 void TilesetView::editTileProperties()
844 {
845     Q_ASSERT(mTilesetDocument);
846 
847     Tile *tile = currentTile();
848     if (!tile)
849         return;
850 
851     mTilesetDocument->setCurrentObject(tile);
852     emit mTilesetDocument->editCurrentObject();
853 }
854 
swapTiles()855 void TilesetView::swapTiles()
856 {
857     const QModelIndexList selectedIndexes = selectionModel()->selectedIndexes();
858     if (selectedIndexes.size() != 2)
859         return;
860 
861     const TilesetModel *model = tilesetModel();
862     Tile *tile1 = model->tileAt(selectedIndexes[0]);
863     Tile *tile2 = model->tileAt(selectedIndexes[1]);
864 
865     if (!tile1 || !tile2)
866         return;
867 
868     emit swapTilesRequested(tile1, tile2);
869 }
870 
setDrawGrid(bool drawGrid)871 void TilesetView::setDrawGrid(bool drawGrid)
872 {
873     mDrawGrid = drawGrid;
874     scheduleDelayedItemsLayout();
875     refreshColumnCount();
876 }
877 
adjustScale()878 void TilesetView::adjustScale()
879 {
880     scheduleDelayedItemsLayout();
881     refreshColumnCount();
882 }
883 
refreshColumnCount()884 void TilesetView::refreshColumnCount()
885 {
886     if (!tilesetModel())
887         return;
888 
889     if (!dynamicWrapping()) {
890         tilesetModel()->setColumnCountOverride(0);
891         return;
892     }
893 
894     const QSize maxSize = maximumViewportSize();
895     const int gridSpace = mDrawGrid ? 1 : 0;
896     const int tileWidth = tilesetModel()->tileset()->tileWidth();
897     const int scaledTileSize = std::max<int>(tileWidth * scale(), 1) + gridSpace;
898     const int columnCount = std::max(maxSize.width() / scaledTileSize, 1);
899     tilesetModel()->setColumnCountOverride(columnCount);
900 }
901 
applyWangId()902 void TilesetView::applyWangId()
903 {
904     if (!mHoveredIndex.isValid() || !mWangSet)
905         return;
906 
907     Tile *tile = tilesetModel()->tileAt(mHoveredIndex);
908     if (!tile)
909         return;
910 
911     WangId previousWangId = mWangSet->wangIdOfTile(tile);
912     WangId newWangId = previousWangId;
913 
914     if (mWangBehavior == AssignWholeId) {
915         newWangId = mWangId;
916     } else {
917         for (int i = 0; i < WangId::NumIndexes; ++i) {
918             if (mWangId.indexColor(i))
919                 newWangId.setIndexColor(i, mWangColorIndex);
920         }
921     }
922 
923     if (newWangId == previousWangId)
924         return;
925 
926     bool wasUnused = !mWangSet->wangIdIsUsed(newWangId);
927 
928     QUndoCommand *command = new ChangeTileWangId(mTilesetDocument, mWangSet, tile, newWangId);
929     mTilesetDocument->undoStack()->push(command);
930     mWangIdChanged = true;
931 
932     if (!mWangSet->wangIdIsUsed(previousWangId))
933         emit wangIdUsedChanged(previousWangId);
934 
935     if (wasUnused)
936         emit wangIdUsedChanged(newWangId);
937 }
938 
finishWangIdChange()939 void TilesetView::finishWangIdChange()
940 {
941     if (!mWangIdChanged)
942         return;
943 
944     mTilesetDocument->undoStack()->push(new ChangeTileWangId);
945     mWangIdChanged = false;
946 }
947 
currentTile() const948 Tile *TilesetView::currentTile() const
949 {
950     const TilesetModel *model = tilesetModel();
951     return model ? model->tileAt(currentIndex()) : nullptr;
952 }
953 
setHandScrolling(bool handScrolling)954 void TilesetView::setHandScrolling(bool handScrolling)
955 {
956     if (mHandScrolling == handScrolling)
957         return;
958 
959     mHandScrolling = handScrolling;
960 
961     if (mHandScrolling)
962         setCursor(QCursor(Qt::ClosedHandCursor));
963     else
964         unsetCursor();
965 }
966 
updateBackgroundColor()967 void TilesetView::updateBackgroundColor()
968 {
969     QColor base = QApplication::palette().dark().color();
970 
971     if (TilesetModel *model = tilesetModel()) {
972         Tileset *tileset = model->tileset();
973         if (tileset->backgroundColor().isValid())
974             base = tileset->backgroundColor();
975     }
976 
977     QPalette p = palette();
978     p.setColor(QPalette::Base, base);
979     setPalette(p);
980 }
981 
982 #include "moc_tilesetview.cpp"
983