1 /*
2 * tilesetdock.cpp
3 * Copyright 2008-2010, Thorbjørn Lindeijer <thorbjorn@lindeijer.nl>
4 * Copyright 2009, Edward Hutchins <eah1@yahoo.com>
5 * Copyright 2012, Stefan Beller <stefanbeller@googlemail.com>
6 *
7 * This file is part of Tiled.
8 *
9 * This program is free software; you can redistribute it and/or modify it
10 * under the terms of the GNU General Public License as published by the Free
11 * Software Foundation; either version 2 of the License, or (at your option)
12 * any later version.
13 *
14 * This program is distributed in the hope that it will be useful, but WITHOUT
15 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
16 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
17 * more details.
18 *
19 * You should have received a copy of the GNU General Public License along with
20 * this program. If not, see <http://www.gnu.org/licenses/>.
21 */
22
23 #include "tilesetdock.h"
24
25 #include "actionmanager.h"
26 #include "addremovemapobject.h"
27 #include "addremovetileset.h"
28 #include "containerhelpers.h"
29 #include "documentmanager.h"
30 #include "editablemanager.h"
31 #include "editabletile.h"
32 #include "erasetiles.h"
33 #include "map.h"
34 #include "mapdocument.h"
35 #include "mapobject.h"
36 #include "objectgroup.h"
37 #include "preferences.h"
38 #include "replacetileset.h"
39 #include "scriptmanager.h"
40 #include "session.h"
41 #include "swaptiles.h"
42 #include "tabbar.h"
43 #include "tile.h"
44 #include "tilelayer.h"
45 #include "tilesetdocument.h"
46 #include "tilesetdocumentsmodel.h"
47 #include "tilesetformat.h"
48 #include "tilesetmanager.h"
49 #include "tilesetmodel.h"
50 #include "tilesetview.h"
51 #include "tilestamp.h"
52 #include "tmxmapformat.h"
53 #include "utils.h"
54 #include "zoomable.h"
55
56 #include <QAction>
57 #include <QActionGroup>
58 #include <QComboBox>
59 #include <QDropEvent>
60 #include <QFileDialog>
61 #include <QHBoxLayout>
62 #include <QMenu>
63 #include <QMessageBox>
64 #include <QMimeData>
65 #include <QPushButton>
66 #include <QScopedValueRollback>
67 #include <QStackedWidget>
68 #include <QStylePainter>
69 #include <QToolBar>
70 #include <QToolButton>
71 #include <QUrl>
72 #include <QVBoxLayout>
73
74 #include <functional>
75
76 using namespace Tiled;
77
78 namespace {
79
80 class NoTilesetWidget : public QWidget
81 {
82 Q_OBJECT
83
84 public:
NoTilesetWidget(QWidget * parent=nullptr)85 explicit NoTilesetWidget(QWidget *parent = nullptr)
86 : QWidget(parent)
87 {
88 QPushButton *newTilesetButton = new QPushButton(this);
89 newTilesetButton->setText(tr("New Tileset..."));
90
91 QGridLayout *gridLayout = new QGridLayout(this);
92 gridLayout->addWidget(newTilesetButton, 0, 0, Qt::AlignCenter);
93
94 connect(newTilesetButton, &QPushButton::clicked, [] {
95 ActionManager::action("NewTileset")->trigger();
96 });
97 }
98 };
99
100 class TilesetMenuButton : public QToolButton
101 {
102 public:
TilesetMenuButton(QWidget * parent=nullptr)103 explicit TilesetMenuButton(QWidget *parent = nullptr)
104 : QToolButton(parent)
105 {
106 setArrowType(Qt::DownArrow);
107 setIconSize(Utils::smallIconSize());
108 setPopupMode(QToolButton::InstantPopup);
109 setAutoRaise(true);
110
111 setSizePolicy(sizePolicy().horizontalPolicy(),
112 QSizePolicy::Ignored);
113 }
114
115 protected:
paintEvent(QPaintEvent *)116 void paintEvent(QPaintEvent *) override
117 {
118 QStylePainter p(this);
119 QStyleOptionToolButton opt;
120 initStyleOption(&opt);
121
122 // Disable the menu arrow, since we already got a down arrow icon
123 opt.features &= ~QStyleOptionToolButton::HasMenu;
124
125 p.drawComplexControl(QStyle::CC_ToolButton, opt);
126 }
127 };
128
129
removeTileReferences(MapDocument * mapDocument,std::function<bool (const Cell &)> condition)130 static void removeTileReferences(MapDocument *mapDocument,
131 std::function<bool(const Cell &)> condition)
132 {
133 QUndoStack *undoStack = mapDocument->undoStack();
134
135 QList<MapObject*> objectsToRemove;
136
137 LayerIterator it(mapDocument->map());
138 while (Layer *layer = it.next()) {
139 switch (layer->layerType()) {
140 case Layer::TileLayerType: {
141 auto tileLayer = static_cast<TileLayer*>(layer);
142 const QRegion refs = tileLayer->region(condition);
143 if (!refs.isEmpty())
144 undoStack->push(new EraseTiles(mapDocument, tileLayer, refs));
145 break;
146 }
147 case Layer::ObjectGroupType: {
148 auto objectGroup = static_cast<ObjectGroup*>(layer);
149 for (MapObject *object : *objectGroup) {
150 if (condition(object->cell()))
151 objectsToRemove.append(object);
152 }
153 break;
154 }
155 case Layer::ImageLayerType:
156 case Layer::GroupLayerType:
157 break;
158 }
159 }
160
161 if (!objectsToRemove.isEmpty())
162 undoStack->push(new RemoveMapObjects(mapDocument, objectsToRemove));
163 }
164
165 } // anonymous namespace
166
TilesetDock(QWidget * parent)167 TilesetDock::TilesetDock(QWidget *parent)
168 : QDockWidget(parent)
169 , mTilesetDocumentsFilterModel(new TilesetDocumentsFilterModel(this))
170 , mTabBar(new TabBar)
171 , mSuperViewStack(new QStackedWidget)
172 , mViewStack(new QStackedWidget)
173 , mToolBar(new QToolBar)
174 , mNewTileset(new QAction(this))
175 , mEmbedTileset(new QAction(this))
176 , mExportTileset(new QAction(this))
177 , mEditTileset(new QAction(this))
178 , mReplaceTileset(new QAction(this))
179 , mRemoveTileset(new QAction(this))
180 , mSelectNextTileset(new QAction(this))
181 , mSelectPreviousTileset(new QAction(this))
182 , mDynamicWrappingToggle(new QAction(this))
183 , mTilesetMenuButton(new TilesetMenuButton(this))
184 , mTilesetMenu(new QMenu(this))
185 , mTilesetActionGroup(new QActionGroup(this))
186 {
187 setObjectName(QLatin1String("TilesetDock"));
188
189 mSelectNextTileset->setShortcut(Qt::Key_BracketRight);
190 mSelectPreviousTileset->setShortcut(Qt::Key_BracketLeft);
191
192 ActionManager::registerAction(mSelectNextTileset, "SelectNextTileset");
193 ActionManager::registerAction(mSelectPreviousTileset, "SelectPreviousTileset");
194
195 mTabBar->setUsesScrollButtons(true);
196 mTabBar->setExpanding(false);
197 mTabBar->setContextMenuPolicy(Qt::CustomContextMenu);
198
199 connect(mTabBar, &QTabBar::currentChanged, this, &TilesetDock::updateActions);
200 connect(mTabBar, &QTabBar::tabMoved, this, &TilesetDock::onTabMoved);
201 connect(mTabBar, &QWidget::customContextMenuRequested,
202 this, &TilesetDock::tabContextMenuRequested);
203
204 QWidget *w = new QWidget(this);
205
206 QHBoxLayout *horizontal = new QHBoxLayout;
207 horizontal->setSpacing(0);
208 horizontal->addWidget(mTabBar);
209 horizontal->addWidget(mTilesetMenuButton);
210
211 QVBoxLayout *vertical = new QVBoxLayout(w);
212 vertical->setSpacing(0);
213 vertical->setContentsMargins(0, 0, 0, 0);
214 vertical->addLayout(horizontal);
215 vertical->addWidget(mSuperViewStack);
216
217 mSuperViewStack->insertWidget(0, new NoTilesetWidget(this));
218 mSuperViewStack->insertWidget(1, mViewStack);
219
220 horizontal = new QHBoxLayout;
221 horizontal->setSpacing(0);
222 horizontal->addWidget(mToolBar, 1);
223 vertical->addLayout(horizontal);
224
225 mDynamicWrappingToggle->setCheckable(true);
226 mDynamicWrappingToggle->setIcon(QIcon(QLatin1String("://images/scalable/wrap.svg")));
227
228 mNewTileset->setIcon(QIcon(QLatin1String(":images/16/document-new.png")));
229 mEmbedTileset->setIcon(QIcon(QLatin1String(":images/16/document-import.png")));
230 mExportTileset->setIcon(QIcon(QLatin1String(":images/16/document-export.png")));
231 mEditTileset->setIcon(QIcon(QLatin1String(":images/16/document-properties.png")));
232 mReplaceTileset->setIcon(QIcon(QLatin1String(":images/scalable/replace.svg")));
233 mRemoveTileset->setIcon(QIcon(QLatin1String(":images/16/edit-delete.png")));
234
235 Utils::setThemeIcon(mNewTileset, "document-new");
236 Utils::setThemeIcon(mEmbedTileset, "document-import");
237 Utils::setThemeIcon(mExportTileset, "document-export");
238 Utils::setThemeIcon(mEditTileset, "document-properties");
239 Utils::setThemeIcon(mRemoveTileset, "edit-delete");
240
241 connect(mNewTileset, &QAction::triggered, this, &TilesetDock::newTileset);
242 connect(mEmbedTileset, &QAction::triggered, this, &TilesetDock::embedTileset);
243 connect(mExportTileset, &QAction::triggered, this, &TilesetDock::exportTileset);
244 connect(mEditTileset, &QAction::triggered, this, &TilesetDock::editTileset);
245 connect(mReplaceTileset, &QAction::triggered, this, &TilesetDock::replaceTileset);
246 connect(mRemoveTileset, &QAction::triggered, this, &TilesetDock::removeTileset);
247 connect(mSelectNextTileset, &QAction::triggered, this, [this] { mTabBar->setCurrentIndex(mTabBar->currentIndex() + 1); });
248 connect(mSelectPreviousTileset, &QAction::triggered, this, [this] { mTabBar->setCurrentIndex(mTabBar->currentIndex() - 1); });
249 connect(mDynamicWrappingToggle, &QAction::toggled, this, [this] (bool checked) {
250 if (TilesetView *view = currentTilesetView()) {
251 view->setDynamicWrapping(checked);
252
253 const QString fileName = currentTilesetDocument()->externalOrEmbeddedFileName();
254 Session::current().setFileStateValue(fileName, QLatin1String("dynamicWrapping"), checked);
255 }
256 });
257
258 auto stretch = new QWidget;
259 stretch->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum);
260
261 mToolBar->setIconSize(Utils::smallIconSize());
262 mToolBar->addAction(mNewTileset);
263 mToolBar->addAction(mEmbedTileset);
264 mToolBar->addAction(mExportTileset);
265 mToolBar->addAction(mEditTileset);
266 mToolBar->addAction(mReplaceTileset);
267 mToolBar->addAction(mRemoveTileset);
268 mToolBar->addWidget(stretch);
269 mToolBar->addAction(mDynamicWrappingToggle);
270
271 mZoomComboBox = new QComboBox;
272 horizontal->addWidget(mZoomComboBox);
273
274 connect(mViewStack, &QStackedWidget::currentChanged,
275 this, &TilesetDock::currentTilesetChanged);
276
277 connect(TilesetManager::instance(), &TilesetManager::tilesetImagesChanged,
278 this, &TilesetDock::tilesetChanged);
279
280 connect(mTilesetDocumentsFilterModel, &TilesetDocumentsModel::rowsInserted,
281 this, &TilesetDock::onTilesetRowsInserted);
282 connect(mTilesetDocumentsFilterModel, &TilesetDocumentsModel::rowsAboutToBeRemoved,
283 this, &TilesetDock::onTilesetRowsAboutToBeRemoved);
284 connect(mTilesetDocumentsFilterModel, &TilesetDocumentsModel::rowsMoved,
285 this, &TilesetDock::onTilesetRowsMoved);
286 connect(mTilesetDocumentsFilterModel, &TilesetDocumentsModel::layoutChanged,
287 this, &TilesetDock::onTilesetLayoutChanged);
288 connect(mTilesetDocumentsFilterModel, &TilesetDocumentsModel::dataChanged,
289 this, &TilesetDock::onTilesetDataChanged);
290
291 mTilesetMenuButton->setMenu(mTilesetMenu);
292 connect(mTilesetMenu, &QMenu::aboutToShow, this, &TilesetDock::refreshTilesetMenu);
293
294 setWidget(w);
295 retranslateUi();
296 setAcceptDrops(true);
297
298 updateActions();
299 }
300
~TilesetDock()301 TilesetDock::~TilesetDock()
302 {
303 }
304
setMapDocument(MapDocument * mapDocument)305 void TilesetDock::setMapDocument(MapDocument *mapDocument)
306 {
307 if (mMapDocument == mapDocument)
308 return;
309
310 // Hide while we update the tab bar, to avoid repeated layouting
311 // But, this causes problems on OS X (issue #1055)
312 #ifndef Q_OS_OSX
313 widget()->hide();
314 #endif
315
316 setCurrentTiles(nullptr);
317 setCurrentTile(nullptr);
318
319 // Clear all connections to the previous document
320 if (mMapDocument)
321 mMapDocument->disconnect(this);
322
323 mMapDocument = mapDocument;
324
325 mTilesetDocumentsFilterModel->setMapDocument(mapDocument);
326
327 if (mMapDocument) {
328 if (Object *object = mMapDocument->currentObject())
329 if (object->typeId() == Object::TileType)
330 setCurrentTile(static_cast<Tile*>(object));
331
332 connect(mMapDocument, &MapDocument::tilesetAdded,
333 this, &TilesetDock::updateActions);
334 connect(mMapDocument, &MapDocument::tilesetRemoved,
335 this, &TilesetDock::updateActions);
336 connect(mMapDocument, &MapDocument::tilesetReplaced,
337 this, &TilesetDock::updateActions);
338 }
339
340 updateActions();
341
342 #ifndef Q_OS_OSX
343 widget()->show();
344 #endif
345 }
346
347 /**
348 * Synchronizes the selection with the given stamp. Ignored when the stamp is
349 * changing because of a selection change in the TilesetDock.
350 */
selectTilesInStamp(const TileStamp & stamp)351 void TilesetDock::selectTilesInStamp(const TileStamp &stamp)
352 {
353 if (mEmittingStampCaptured)
354 return;
355
356 QSet<Tile*> tiles;
357
358 for (const TileStampVariation &variation : stamp.variations())
359 for (auto layer : variation.map->tileLayers())
360 for (const Cell &cell : *static_cast<TileLayer*>(layer))
361 if (Tile *tile = cell.tile())
362 tiles.insert(tile);
363
364 selectTiles(tiles.values());
365 }
366
selectTiles(const QList<Tile * > & tiles)367 void TilesetDock::selectTiles(const QList<Tile *> &tiles)
368 {
369 QHash<QItemSelectionModel*, QItemSelection> selections;
370
371 for (Tile *tile : tiles) {
372 Tileset *tileset = tile->tileset();
373 int tilesetIndex = mTilesets.indexOf(tileset->sharedPointer());
374 if (tilesetIndex != -1) {
375 TilesetView *view = tilesetViewAt(tilesetIndex);
376 if (!view->model()) // Lazily set up the model
377 setupTilesetModel(view, mTilesetDocuments.at(tilesetIndex));
378
379 const TilesetModel *model = view->tilesetModel();
380 const QModelIndex modelIndex = model->tileIndex(tile);
381 QItemSelectionModel *selectionModel = view->selectionModel();
382 selections[selectionModel].select(modelIndex, modelIndex);
383 }
384 }
385
386 if (!selections.isEmpty()) {
387 QScopedValueRollback<bool> synchronizingSelection(mSynchronizingSelection, true);
388
389 // Mark tiles as selected
390 for (auto i = selections.constBegin(); i != selections.constEnd(); ++i) {
391 QItemSelectionModel *selectionModel = i.key();
392 const QItemSelection &selection = i.value();
393 selectionModel->select(selection, QItemSelectionModel::ClearAndSelect);
394 }
395
396 // Update the current tile (useful for animation and collision editors)
397 auto first = selections.begin();
398 QItemSelectionModel *selectionModel = first.key();
399 const QItemSelection &selection = first.value();
400 const QModelIndex currentIndex = selection.first().topLeft();
401 if (selectionModel->currentIndex() != currentIndex)
402 selectionModel->setCurrentIndex(currentIndex, QItemSelectionModel::NoUpdate);
403 else
404 currentChanged(currentIndex);
405
406 // If all of the selected tiles are in the same tileset, switch the
407 // current tab to that tileset.
408 if (selections.size() == 1) {
409 auto tileset = tiles.first()->tileset()->sharedPointer();
410 const int tilesetTabIndex = mTilesets.indexOf(tileset);
411 mTabBar->setCurrentIndex(tilesetTabIndex);
412 }
413 }
414 }
415
changeEvent(QEvent * e)416 void TilesetDock::changeEvent(QEvent *e)
417 {
418 QDockWidget::changeEvent(e);
419 switch (e->type()) {
420 case QEvent::LanguageChange:
421 retranslateUi();
422 break;
423 default:
424 break;
425 }
426 }
427
dragEnterEvent(QDragEnterEvent * e)428 void TilesetDock::dragEnterEvent(QDragEnterEvent *e)
429 {
430 const QList<QUrl> urls = e->mimeData()->urls();
431 if (!urls.isEmpty() && !urls.at(0).toLocalFile().isEmpty())
432 e->acceptProposedAction();
433 }
434
dropEvent(QDropEvent * e)435 void TilesetDock::dropEvent(QDropEvent *e)
436 {
437 QStringList paths;
438 const auto urls = e->mimeData()->urls();
439 for (const QUrl &url : urls) {
440 const QString localFile = url.toLocalFile();
441 if (!localFile.isEmpty())
442 paths.append(localFile);
443 }
444 if (!paths.isEmpty()) {
445 emit localFilesDropped(paths);
446 e->acceptProposedAction();
447 }
448 }
449
currentTilesetChanged()450 void TilesetDock::currentTilesetChanged()
451 {
452 TilesetView *view = currentTilesetView();
453 if (!view)
454 return;
455
456 if (!mSynchronizingSelection)
457 updateCurrentTiles();
458
459 view->zoomable()->setComboBox(mZoomComboBox);
460
461 if (const QItemSelectionModel *s = view->selectionModel())
462 setCurrentTile(view->tilesetModel()->tileAt(s->currentIndex()));
463
464 mDynamicWrappingToggle->setChecked(view->dynamicWrapping());
465 }
466
selectionChanged()467 void TilesetDock::selectionChanged()
468 {
469 updateActions();
470
471 if (!mSynchronizingSelection)
472 updateCurrentTiles();
473 }
474
currentChanged(const QModelIndex & index)475 void TilesetDock::currentChanged(const QModelIndex &index)
476 {
477 if (!index.isValid())
478 return;
479
480 const TilesetModel *model = static_cast<const TilesetModel*>(index.model());
481 setCurrentTile(model->tileAt(index));
482 }
483
updateActions()484 void TilesetDock::updateActions()
485 {
486 bool external = false;
487 TilesetView *view = nullptr;
488 Tileset *tileset = nullptr;
489 const int index = mTabBar->currentIndex();
490
491 if (index > -1) {
492 view = tilesetViewAt(index);
493 tileset = mTilesets.at(index).data();
494
495 if (!view->model()) // Lazily set up the model
496 setupTilesetModel(view, mTilesetDocuments.at(index));
497
498 mViewStack->setCurrentIndex(index);
499 external = tileset->isExternal();
500 }
501
502 const auto map = mMapDocument ? mMapDocument->map() : nullptr;
503 const bool mapHasCurrentTileset = tileset && map && contains(map->tilesets(), tileset);
504
505 mEmbedTileset->setEnabled(tileset && external);
506 mExportTileset->setEnabled(tileset && !external);
507 mEditTileset->setEnabled(tileset);
508 mReplaceTileset->setEnabled(mapHasCurrentTileset);
509 mRemoveTileset->setEnabled(mapHasCurrentTileset);
510 mSelectNextTileset->setEnabled(index != -1 && index < mTabBar->count() - 1);
511 mSelectPreviousTileset->setEnabled(index > 0);
512 }
513
updateCurrentTiles()514 void TilesetDock::updateCurrentTiles()
515 {
516 TilesetView *view = currentTilesetView();
517 if (!view)
518 return;
519
520 const QItemSelectionModel *s = view->selectionModel();
521 if (!s)
522 return;
523
524 const QModelIndexList indexes = s->selection().indexes();
525 if (indexes.isEmpty())
526 return;
527
528 const QModelIndex &first = indexes.first();
529 int minX = first.column();
530 int maxX = first.column();
531 int minY = first.row();
532 int maxY = first.row();
533
534 for (const QModelIndex &index : indexes) {
535 if (minX > index.column()) minX = index.column();
536 if (maxX < index.column()) maxX = index.column();
537 if (minY > index.row()) minY = index.row();
538 if (maxY < index.row()) maxY = index.row();
539 }
540
541 // Create a tile layer from the current selection
542 auto tileLayer = std::make_unique<TileLayer>(QString(), 0, 0,
543 maxX - minX + 1,
544 maxY - minY + 1);
545
546 const TilesetModel *model = view->tilesetModel();
547 for (const QModelIndex &index : indexes) {
548 tileLayer->setCell(index.column() - minX,
549 index.row() - minY,
550 Cell(model->tileAt(index)));
551 }
552
553 setCurrentTiles(std::move(tileLayer));
554 }
555
indexPressed(const QModelIndex & index)556 void TilesetDock::indexPressed(const QModelIndex &index)
557 {
558 TilesetView *view = currentTilesetView();
559 if (Tile *tile = view->tilesetModel()->tileAt(index))
560 mMapDocument->setCurrentObject(tile, currentTilesetDocument());
561 }
562
createTilesetView(int index,TilesetDocument * tilesetDocument)563 void TilesetDock::createTilesetView(int index, TilesetDocument *tilesetDocument)
564 {
565 auto tileset = tilesetDocument->tileset();
566
567 mTilesets.insert(index, tileset);
568 mTilesetDocuments.insert(index, tilesetDocument);
569
570 TilesetView *view = new TilesetView;
571
572 // Hides the "New Tileset..." special view if it is shown.
573 mSuperViewStack->setCurrentIndex(1);
574
575 // Restore state from last time
576 const QString fileName = tilesetDocument->externalOrEmbeddedFileName();
577 const QVariantMap fileState = Session::current().fileState(fileName);
578 if (fileState.isEmpty()) {
579 // Compatibility with Tiled 1.3
580 QString path = QLatin1String("TilesetDock/TilesetScale/") + tileset->name();
581 qreal scale = Preferences::instance()->value(path, 1).toReal();
582 view->zoomable()->setScale(scale);
583 } else {
584 bool ok;
585 const qreal scale = fileState.value(QLatin1String("scaleInDock")).toReal(&ok);
586 if (scale > 0 && ok)
587 view->zoomable()->setScale(scale);
588
589 if (fileState.contains(QLatin1String("dynamicWrapping"))) {
590 const bool dynamicWrapping = fileState.value(QLatin1String("dynamicWrapping")).toBool();
591 view->setDynamicWrapping(dynamicWrapping);
592 }
593 }
594
595 // Insert view before the tab to make sure it is there when the tab index
596 // changes (happens when first tab is inserted).
597 mViewStack->insertWidget(index, view);
598 mTabBar->insertTab(index, tileset->name());
599 mTabBar->setTabToolTip(index, tileset->fileName());
600
601 connect(tilesetDocument, &TilesetDocument::fileNameChanged,
602 this, &TilesetDock::tilesetFileNameChanged);
603 connect(tilesetDocument, &TilesetDocument::tilesetChanged,
604 this, &TilesetDock::tilesetChanged);
605
606 connect(view, &TilesetView::clicked,
607 this, &TilesetDock::updateCurrentTiles);
608 connect(view, &TilesetView::swapTilesRequested,
609 this, &TilesetDock::swapTiles);
610 }
611
deleteTilesetView(int index)612 void TilesetDock::deleteTilesetView(int index)
613 {
614 TilesetDocument *tilesetDocument = mTilesetDocuments.at(index);
615 tilesetDocument->disconnect(this);
616
617 Tileset *tileset = tilesetDocument->tileset().data();
618 TilesetView *view = tilesetViewAt(index);
619
620 // Remember the scale
621 const QString fileName = tilesetDocument->externalOrEmbeddedFileName();
622 Session::current().setFileStateValue(fileName, QLatin1String("scaleInDock"), view->scale());
623
624 // Some cleanup for potentially old preferences from Tiled 1.3
625 const QString path = QLatin1String("TilesetDock/TilesetScale/") + tileset->name();
626 Preferences::instance()->remove(path);
627
628 mTilesets.remove(index);
629 mTilesetDocuments.removeAt(index);
630 delete view; // view needs to go before the tab
631 mTabBar->removeTab(index);
632
633 // Make the "New Tileset..." special tab reappear if there is no tileset open
634 if (mTilesets.isEmpty())
635 mSuperViewStack->setCurrentIndex(0);
636
637 // Make sure we don't reference this tileset anymore
638 if (mCurrentTiles && mCurrentTiles->referencesTileset(tileset)) {
639 auto cleaned = std::unique_ptr<TileLayer>(mCurrentTiles->clone());
640 cleaned->removeReferencesToTileset(tileset);
641 setCurrentTiles(std::move(cleaned));
642 }
643 if (mCurrentTile && mCurrentTile->tileset() == tileset)
644 setCurrentTile(nullptr);
645 }
646
moveTilesetView(int from,int to)647 void TilesetDock::moveTilesetView(int from, int to)
648 {
649 mTabBar->moveTab(from, to);
650 }
651
tilesetChanged(Tileset * tileset)652 void TilesetDock::tilesetChanged(Tileset *tileset)
653 {
654 // Update the affected tileset model, if it exists
655 const int index = indexOf(mTilesets, tileset);
656 if (index < 0)
657 return;
658
659 TilesetView *view = tilesetViewAt(index);
660
661 if (TilesetModel *model = view->tilesetModel()) {
662 view->updateBackgroundColor();
663 model->tilesetChanged();
664 }
665 }
666
667 /**
668 * Offers to replace the currently selected tileset.
669 */
replaceTileset()670 void TilesetDock::replaceTileset()
671 {
672 const int currentIndex = mViewStack->currentIndex();
673 if (currentIndex == -1)
674 return;
675
676 replaceTilesetAt(currentIndex);
677 }
678
replaceTilesetAt(int index)679 void TilesetDock::replaceTilesetAt(int index)
680 {
681 if (!mMapDocument)
682 return;
683
684 auto &sharedTileset = mTilesets.at(index);
685 int mapTilesetIndex = mMapDocument->map()->tilesets().indexOf(sharedTileset);
686 if (mapTilesetIndex == -1)
687 return;
688
689 SessionOption<QString> lastUsedTilesetFilter { "tileset.lastUsedFilter" };
690 QString filter = tr("All Files (*)");
691 QString selectedFilter = lastUsedTilesetFilter;
692 if (selectedFilter.isEmpty())
693 selectedFilter = TsxTilesetFormat().nameFilter();
694
695 FormatHelper<TilesetFormat> helper(FileFormat::Read, filter);
696
697 Session &session = Session::current();
698 QString start = session.lastPath(Session::ExternalTileset);
699
700 const auto fileName =
701 QFileDialog::getOpenFileName(this, tr("Replace Tileset"),
702 start,
703 helper.filter(),
704 &selectedFilter);
705
706 if (fileName.isEmpty())
707 return;
708
709 session.setLastPath(Session::ExternalTileset, QFileInfo(fileName).path());
710
711 lastUsedTilesetFilter = selectedFilter;
712
713 QString error;
714 SharedTileset tileset = TilesetManager::instance()->loadTileset(fileName, &error);
715 if (!tileset) {
716 QMessageBox::critical(window(), tr("Error Reading Tileset"), error);
717 return;
718 }
719
720 // Don't try to replace a tileset with itself
721 if (tileset == sharedTileset)
722 return;
723
724 QUndoCommand *command = new ReplaceTileset(mMapDocument,
725 mapTilesetIndex,
726 tileset);
727 mMapDocument->undoStack()->push(command);
728 }
729
730 /**
731 * Removes the currently selected tileset.
732 */
removeTileset()733 void TilesetDock::removeTileset()
734 {
735 const int currentIndex = mViewStack->currentIndex();
736 if (currentIndex != -1)
737 removeTilesetAt(mViewStack->currentIndex());
738 }
739
740 /**
741 * Removes the tileset at the given index. Prompting the user when the tileset
742 * is in use by the map.
743 */
removeTilesetAt(int index)744 void TilesetDock::removeTilesetAt(int index)
745 {
746 auto &sharedTileset = mTilesets.at(index);
747
748 int mapTilesetIndex = mMapDocument->map()->tilesets().indexOf(sharedTileset);
749 if (mapTilesetIndex == -1)
750 return;
751
752 Tileset *tileset = sharedTileset.data();
753 const bool inUse = mMapDocument->map()->isTilesetUsed(tileset);
754
755 // If the tileset is in use, warn the user and confirm removal
756 if (inUse) {
757 QMessageBox warning(QMessageBox::Warning,
758 tr("Remove Tileset"),
759 tr("The tileset \"%1\" is still in use by the "
760 "map!").arg(tileset->name()),
761 QMessageBox::Yes | QMessageBox::No,
762 this);
763 warning.setDefaultButton(QMessageBox::Yes);
764 warning.setInformativeText(tr("Remove this tileset and all references "
765 "to the tiles in this tileset?"));
766
767 if (warning.exec() != QMessageBox::Yes)
768 return;
769 }
770
771 QUndoCommand *remove = new RemoveTileset(mMapDocument, mapTilesetIndex);
772 QUndoStack *undoStack = mMapDocument->undoStack();
773
774 if (inUse) {
775 // Remove references to tiles in this tileset from the current map
776 auto referencesTileset = [tileset] (const Cell &cell) {
777 return cell.tileset() == tileset;
778 };
779 undoStack->beginMacro(remove->text());
780 removeTileReferences(mMapDocument, referencesTileset);
781 }
782 undoStack->push(remove);
783 if (inUse)
784 undoStack->endMacro();
785 }
786
newTileset()787 void TilesetDock::newTileset()
788 {
789 ActionManager::action("NewTileset")->trigger();
790 }
791
setCurrentTiles(std::unique_ptr<TileLayer> tiles)792 void TilesetDock::setCurrentTiles(std::unique_ptr<TileLayer> tiles)
793 {
794 if (mCurrentTiles == tiles)
795 return;
796
797 mCurrentTiles = std::move(tiles);
798
799 if (mCurrentTiles && mMapDocument) {
800 // Create a tile stamp with these tiles
801 Map::Parameters mapParameters = mMapDocument->map()->parameters();
802 mapParameters.width = mCurrentTiles->width();
803 mapParameters.height = mCurrentTiles->height();
804 mapParameters.infinite = false;
805
806 auto stamp = std::make_unique<Map>(mapParameters);
807 stamp->addLayer(mCurrentTiles->clone());
808 stamp->addTilesets(mCurrentTiles->usedTilesets());
809
810 QScopedValueRollback<bool> emittingStampCaptured(mEmittingStampCaptured, true);
811 emit stampCaptured(TileStamp(std::move(stamp)));
812 }
813 }
814
setCurrentTile(Tile * tile)815 void TilesetDock::setCurrentTile(Tile *tile)
816 {
817 if (mCurrentTile == tile)
818 return;
819
820 mCurrentTile = tile;
821 emit currentTileChanged(tile);
822
823 if (mMapDocument && tile) {
824 int tilesetIndex = indexOf(mTilesets, tile->tileset());
825 if (tilesetIndex != -1)
826 mMapDocument->setCurrentObject(tile, mTilesetDocuments.at(tilesetIndex));
827 }
828 }
829
retranslateUi()830 void TilesetDock::retranslateUi()
831 {
832 setWindowTitle(tr("Tilesets"));
833 mNewTileset->setText(tr("New Tileset"));
834 mEmbedTileset->setText(tr("&Embed Tileset"));
835 mExportTileset->setText(tr("&Export Tileset As..."));
836 mEditTileset->setText(tr("Edit Tile&set"));
837 mReplaceTileset->setText(tr("Replace Tileset"));
838 mRemoveTileset->setText(tr("&Remove Tileset"));
839 mSelectNextTileset->setText(tr("Select Next Tileset"));
840 mSelectPreviousTileset->setText(tr("Select Previous Tileset"));
841 mDynamicWrappingToggle->setText(tr("Dynamically Wrap Tiles"));
842 }
843
onTilesetRowsInserted(const QModelIndex & parent,int first,int last)844 void TilesetDock::onTilesetRowsInserted(const QModelIndex &parent, int first, int last)
845 {
846 for (int row = first; row <= last; ++row) {
847 const QModelIndex index = mTilesetDocumentsFilterModel->index(row, 0, parent);
848 const QVariant var = mTilesetDocumentsFilterModel->data(index, TilesetDocumentsModel::TilesetDocumentRole);
849 createTilesetView(row, var.value<TilesetDocument*>());
850 }
851 }
852
onTilesetRowsAboutToBeRemoved(const QModelIndex & parent,int first,int last)853 void TilesetDock::onTilesetRowsAboutToBeRemoved(const QModelIndex &parent, int first, int last)
854 {
855 Q_UNUSED(parent)
856
857 for (int index = last; index >= first; --index)
858 deleteTilesetView(index);
859 }
860
onTilesetRowsMoved(const QModelIndex & parent,int start,int end,const QModelIndex & destination,int row)861 void TilesetDock::onTilesetRowsMoved(const QModelIndex &parent, int start, int end, const QModelIndex &destination, int row)
862 {
863 Q_UNUSED(parent)
864 Q_UNUSED(destination)
865
866 if (start == row)
867 return;
868
869 while (start <= end) {
870 moveTilesetView(start, row);
871
872 if (row < start) {
873 ++start;
874 ++row;
875 } else {
876 --end;
877 }
878 }
879 }
880
onTilesetLayoutChanged(const QList<QPersistentModelIndex> & parents,QAbstractItemModel::LayoutChangeHint hint)881 void TilesetDock::onTilesetLayoutChanged(const QList<QPersistentModelIndex> &parents, QAbstractItemModel::LayoutChangeHint hint)
882 {
883 Q_UNUSED(parents)
884 Q_UNUSED(hint)
885
886 // Make sure the tileset tabs and views are still in the right order
887 for (int i = 0, rows = mTilesetDocumentsFilterModel->rowCount(); i < rows; ++i) {
888 const QModelIndex index = mTilesetDocumentsFilterModel->index(i, 0);
889 const QVariant var = mTilesetDocumentsFilterModel->data(index, TilesetDocumentsModel::TilesetDocumentRole);
890 TilesetDocument *tilesetDocument = var.value<TilesetDocument*>();
891 int currentIndex = mTilesetDocuments.indexOf(tilesetDocument);
892 if (currentIndex != i) {
893 Q_ASSERT(currentIndex > i);
894 moveTilesetView(currentIndex, i);
895 }
896 }
897 }
898
onTilesetDataChanged(const QModelIndex & topLeft,const QModelIndex & bottomRight)899 void TilesetDock::onTilesetDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight)
900 {
901 // Update the titles of the affected tabs
902 for (int i = topLeft.row(); i <= bottomRight.row(); ++i) {
903 const SharedTileset &tileset = mTilesets.at(i);
904 if (mTabBar->tabText(i) != tileset->name())
905 mTabBar->setTabText(i, tileset->name());
906 mTabBar->setTabToolTip(i, tileset->fileName());
907 }
908 }
909
onTabMoved(int from,int to)910 void TilesetDock::onTabMoved(int from, int to)
911 {
912 mTilesets.move(from, to);
913 mTilesetDocuments.move(from, to);
914
915 // Move the related tileset view
916 const QSignalBlocker blocker(mViewStack);
917 QWidget *widget = mViewStack->widget(from);
918 mViewStack->removeWidget(widget);
919 mViewStack->insertWidget(to, widget);
920 }
921
tabContextMenuRequested(const QPoint & pos)922 void TilesetDock::tabContextMenuRequested(const QPoint &pos)
923 {
924 int index = mTabBar->tabAt(pos);
925 if (index == -1)
926 return;
927
928 QMenu menu;
929
930 const QString fileName = mTilesetDocuments.at(index)->fileName();
931 Utils::addFileManagerActions(menu, fileName);
932
933 menu.addSeparator();
934 menu.addAction(mEditTileset->icon(), mEditTileset->text(), this, [tileset = mTilesets.at(index)] {
935 DocumentManager::instance()->openTileset(tileset);
936 });
937
938 menu.exec(mTabBar->mapToGlobal(pos));
939 }
940
setCurrentTileset(const SharedTileset & tileset)941 void TilesetDock::setCurrentTileset(const SharedTileset &tileset)
942 {
943 const int index = mTilesets.indexOf(tileset);
944 if (index != -1)
945 mTabBar->setCurrentIndex(index);
946 }
947
currentTileset() const948 SharedTileset TilesetDock::currentTileset() const
949 {
950 const int index = mViewStack->currentIndex();
951 if (index == -1)
952 return {};
953
954 return mTilesets.at(index);
955 }
956
currentTilesetDocument() const957 TilesetDocument *TilesetDock::currentTilesetDocument() const
958 {
959 const int index = mViewStack->currentIndex();
960 if (index == -1)
961 return nullptr;
962
963 return mTilesetDocuments.at(index);
964 }
965
setCurrentEditableTileset(EditableTileset * tileset)966 void TilesetDock::setCurrentEditableTileset(EditableTileset *tileset)
967 {
968 if (!tileset) {
969 ScriptManager::instance().throwNullArgError(0);
970 return;
971 }
972 setCurrentTileset(tileset->tileset()->sharedPointer());
973 }
974
currentEditableTileset() const975 EditableTileset *TilesetDock::currentEditableTileset() const
976 {
977 const int index = mTabBar->currentIndex();
978 if (index == -1)
979 return nullptr;
980
981 return mTilesetDocuments.at(index)->editable();
982 }
983
setSelectedTiles(const QList<QObject * > & tiles)984 void TilesetDock::setSelectedTiles(const QList<QObject *> &tiles)
985 {
986 QList<Tile*> plainTiles;
987
988 for (QObject *objectTile : tiles) {
989 auto editableTile = qobject_cast<EditableTile*>(objectTile);
990 if (!editableTile) {
991 ScriptManager::instance().throwError(QCoreApplication::translate("Script Errors", "Not a tile"));
992 return;
993 }
994 plainTiles.append(editableTile->tile());
995 }
996
997 selectTiles(plainTiles);
998 }
999
selectedTiles() const1000 QList<QObject *> TilesetDock::selectedTiles() const
1001 {
1002 QList<QObject *> result;
1003
1004 TilesetView *view = currentTilesetView();
1005 if (!view)
1006 return result;
1007
1008 const QItemSelectionModel *s = view->selectionModel();
1009 if (!s)
1010 return result;
1011
1012 const QModelIndexList indexes = s->selection().indexes();
1013 if (indexes.isEmpty())
1014 return result;
1015
1016 EditableTileset *editableTileset = currentEditableTileset();
1017
1018 const TilesetModel *model = view->tilesetModel();
1019 auto &editableManager = EditableManager::instance();
1020 for (const QModelIndex &index : indexes)
1021 if (Tile *tile = model->tileAt(index))
1022 result.append(editableManager.editableTile(editableTileset, tile));
1023
1024 return result;
1025 }
1026
currentTilesetView() const1027 TilesetView *TilesetDock::currentTilesetView() const
1028 {
1029 return static_cast<TilesetView *>(mViewStack->currentWidget());
1030 }
1031
tilesetViewAt(int index) const1032 TilesetView *TilesetDock::tilesetViewAt(int index) const
1033 {
1034 return static_cast<TilesetView *>(mViewStack->widget(index));
1035 }
1036
setupTilesetModel(TilesetView * view,TilesetDocument * tilesetDocument)1037 void TilesetDock::setupTilesetModel(TilesetView *view, TilesetDocument *tilesetDocument)
1038 {
1039 view->setModel(new TilesetModel(tilesetDocument, view));
1040
1041 QItemSelectionModel *s = view->selectionModel();
1042 connect(s, &QItemSelectionModel::selectionChanged,
1043 this, &TilesetDock::selectionChanged);
1044 connect(s, &QItemSelectionModel::currentChanged,
1045 this, &TilesetDock::currentChanged);
1046 connect(view, &TilesetView::pressed,
1047 this, &TilesetDock::indexPressed);
1048 }
1049
editTileset()1050 void TilesetDock::editTileset()
1051 {
1052 auto tileset = currentTileset();
1053 if (!tileset)
1054 return;
1055
1056 DocumentManager *documentManager = DocumentManager::instance();
1057 documentManager->openTileset(tileset);
1058 }
1059
exportTileset()1060 void TilesetDock::exportTileset()
1061 {
1062 auto tileset = currentTileset();
1063 if (!tileset)
1064 return;
1065
1066 if (tileset->isExternal())
1067 return;
1068
1069 int mapTilesetIndex = mMapDocument->map()->tilesets().indexOf(tileset);
1070 if (mapTilesetIndex == -1)
1071 return;
1072
1073 // To export a tileset we clone it, since the tileset could now be used by
1074 // other maps. This ensures undo can take the map back to using an embedded
1075 // tileset, without affecting those other maps.
1076 SharedTileset externalTileset = tileset->clone();
1077
1078 FormatHelper<TilesetFormat> helper(FileFormat::ReadWrite);
1079
1080 Session &session = Session::current();
1081
1082 QString suggestedFileName = session.lastPath(Session::ExternalTileset);
1083 suggestedFileName += QLatin1Char('/');
1084 suggestedFileName += externalTileset->name();
1085
1086 const QLatin1String extension(".tsx");
1087 if (!suggestedFileName.endsWith(extension))
1088 suggestedFileName.append(extension);
1089
1090 // todo: remember last used filter
1091 QString selectedFilter = TsxTilesetFormat().nameFilter();
1092 const QString fileName =
1093 QFileDialog::getSaveFileName(this, tr("Export Tileset"),
1094 suggestedFileName,
1095 helper.filter(), &selectedFilter);
1096
1097 if (fileName.isEmpty())
1098 return;
1099
1100 session.setLastPath(Session::ExternalTileset,
1101 QFileInfo(fileName).path());
1102
1103 TilesetFormat *format = helper.formatByNameFilter(selectedFilter);
1104 if (!format)
1105 return; // can't happen
1106
1107 if (!format->write(*externalTileset, fileName)) {
1108 QString error = format->errorString();
1109 QMessageBox::critical(window(),
1110 tr("Export Tileset"),
1111 tr("Error saving tileset: %1").arg(error));
1112 return;
1113 }
1114
1115 externalTileset->setFileName(fileName);
1116 externalTileset->setFormat(format->shortName());
1117
1118 QUndoCommand *command = new ReplaceTileset(mMapDocument,
1119 mapTilesetIndex,
1120 externalTileset);
1121 mMapDocument->undoStack()->push(command);
1122
1123 // Make sure the external tileset is selected
1124 int externalTilesetIndex = mTilesets.indexOf(externalTileset);
1125 if (externalTilesetIndex != -1)
1126 mTabBar->setCurrentIndex(externalTilesetIndex);
1127 }
1128
embedTileset()1129 void TilesetDock::embedTileset()
1130 {
1131 auto tileset = currentTileset();
1132 if (!tileset)
1133 return;
1134
1135 if (!tileset->isExternal())
1136 return;
1137
1138 // To embed a tileset we clone it, since further changes to that tileset
1139 // should not affect the exteral tileset.
1140 SharedTileset embeddedTileset = tileset->clone();
1141
1142 QUndoStack *undoStack = mMapDocument->undoStack();
1143 int mapTilesetIndex = mMapDocument->map()->tilesets().indexOf(tileset);
1144
1145 // Tileset may not be part of the map yet
1146 if (mapTilesetIndex == -1)
1147 undoStack->push(new AddTileset(mMapDocument, embeddedTileset));
1148 else
1149 undoStack->push(new ReplaceTileset(mMapDocument, mapTilesetIndex, embeddedTileset));
1150
1151 // Make sure the embedded tileset is selected
1152 int embeddedTilesetIndex = mTilesets.indexOf(embeddedTileset);
1153 if (embeddedTilesetIndex != -1)
1154 mTabBar->setCurrentIndex(embeddedTilesetIndex);
1155 }
1156
tilesetFileNameChanged(const QString & fileName)1157 void TilesetDock::tilesetFileNameChanged(const QString &fileName)
1158 {
1159 TilesetDocument *tilesetDocument = static_cast<TilesetDocument*>(sender());
1160 Tileset *tileset = tilesetDocument->tileset().data();
1161
1162 const int index = indexOf(mTilesets, tileset);
1163 Q_ASSERT(index != -1);
1164
1165 mTabBar->setTabToolTip(index, fileName);
1166
1167 updateActions();
1168 }
1169
refreshTilesetMenu()1170 void TilesetDock::refreshTilesetMenu()
1171 {
1172 mTilesetMenu->clear();
1173
1174 const int currentIndex = mTabBar->currentIndex();
1175
1176 for (int i = 0; i < mTabBar->count(); ++i) {
1177 QAction *action = mTilesetMenu->addAction(mTabBar->tabText(i),
1178 [=] { mTabBar->setCurrentIndex(i); });
1179
1180 action->setCheckable(true);
1181 mTilesetActionGroup->addAction(action);
1182 if (i == currentIndex)
1183 action->setChecked(true);
1184 }
1185
1186 mTilesetMenu->addSeparator();
1187 mTilesetMenu->addAction(ActionManager::action("AddExternalTileset"));
1188 }
1189
swapTiles(Tile * tileA,Tile * tileB)1190 void TilesetDock::swapTiles(Tile *tileA, Tile *tileB)
1191 {
1192 if (!mMapDocument)
1193 return;
1194
1195 QUndoStack *undoStack = mMapDocument->undoStack();
1196 undoStack->push(new SwapTiles(mMapDocument, tileA, tileB));
1197 }
1198
1199 #include "tilesetdock.moc"
1200 #include "moc_tilesetdock.cpp"
1201