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