1 /*
2 * tileseteditor.cpp
3 * Copyright 2016, Thorbjørn Lindeijer <bjorn@lindijer.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 "tileseteditor.h"
22
23 #include "actionmanager.h"
24 #include "addremovemapobject.h"
25 #include "addremovetiles.h"
26 #include "addremovewangset.h"
27 #include "changewangcolordata.h"
28 #include "changewangsetdata.h"
29 #include "documentmanager.h"
30 #include "erasetiles.h"
31 #include "maintoolbar.h"
32 #include "mapdocument.h"
33 #include "mapobject.h"
34 #include "objectgroup.h"
35 #include "objecttemplate.h"
36 #include "preferences.h"
37 #include "propertiesdock.h"
38 #include "session.h"
39 #include "templatesdock.h"
40 #include "tile.h"
41 #include "tileanimationeditor.h"
42 #include "tilecollisiondock.h"
43 #include "tilelayer.h"
44 #include "tilesetdocument.h"
45 #include "tilesetmanager.h"
46 #include "tilesetmodel.h"
47 #include "tilesetview.h"
48 #include "toolmanager.h"
49 #include "undodock.h"
50 #include "utils.h"
51 #include "wangcolorview.h"
52 #include "wangdock.h"
53 #include "zoomable.h"
54
55 #include <QAction>
56 #include <QCheckBox>
57 #include <QComboBox>
58 #include <QCoreApplication>
59 #include <QDropEvent>
60 #include <QFileDialog>
61 #include <QFileInfo>
62 #include <QLabel>
63 #include <QMainWindow>
64 #include <QMessageBox>
65 #include <QMimeData>
66 #include <QScopedValueRollback>
67 #include <QStackedWidget>
68 #include <QUndoGroup>
69
70 #include <functional>
71
72 #include <QDebug>
73
74 namespace Tiled {
75
76 namespace preferences {
77 static Preference<QSize> tilesetEditorSize { "TilesetEditor/Size" };
78 static Preference<QByteArray> tilesetEditorState { "TilesetEditor/State" };
79 } // namespace preferences
80
81 class TilesetEditorWindow : public QMainWindow
82 {
83 Q_OBJECT
84
85 public:
TilesetEditorWindow(TilesetEditor * editor,QWidget * parent=nullptr)86 TilesetEditorWindow(TilesetEditor *editor, QWidget *parent = nullptr)
87 : QMainWindow(parent)
88 , mEditor(editor)
89 {
90 setAcceptDrops(true);
91 }
92
93 signals:
94 void urlsDropped(const QList<QUrl> &urls);
95
96 protected:
97 void dragEnterEvent(QDragEnterEvent *) override;
98 void dropEvent(QDropEvent *) override;
99
100 private:
101 TilesetEditor *mEditor;
102 };
103
dragEnterEvent(QDragEnterEvent * e)104 void TilesetEditorWindow::dragEnterEvent(QDragEnterEvent *e)
105 {
106 Tileset *tileset = mEditor->currentTileset();
107 if (!tileset || !tileset->isCollection())
108 return; // only collection tilesets can accept drops
109
110 const QList<QUrl> urls = e->mimeData()->urls();
111 if (!urls.isEmpty() && !urls.at(0).toLocalFile().isEmpty())
112 e->acceptProposedAction();
113 }
114
dropEvent(QDropEvent * e)115 void TilesetEditorWindow::dropEvent(QDropEvent *e)
116 {
117 const auto urls = e->mimeData()->urls();
118 if (!urls.isEmpty()) {
119 emit urlsDropped(urls);
120 e->acceptProposedAction();
121 }
122 }
123
124
TilesetEditor(QObject * parent)125 TilesetEditor::TilesetEditor(QObject *parent)
126 : Editor(parent)
127 , mMainWindow(new TilesetEditorWindow(this))
128 , mMainToolBar(new MainToolBar(mMainWindow))
129 , mWidgetStack(new QStackedWidget(mMainWindow))
130 , mAddTiles(new QAction(this))
131 , mRemoveTiles(new QAction(this))
132 , mRelocateTiles(new QAction(this))
133 , mShowAnimationEditor(new QAction(this))
134 , mDynamicWrappingToggle(new QAction(this))
135 , mPropertiesDock(new PropertiesDock(mMainWindow))
136 , mUndoDock(new UndoDock(mMainWindow))
137 , mTileCollisionDock(new TileCollisionDock(mMainWindow))
138 , mTemplatesDock(new TemplatesDock(mMainWindow))
139 , mWangDock(new WangDock(mMainWindow))
140 , mZoomComboBox(new QComboBox)
141 , mStatusInfoLabel(new QLabel)
142 , mTileAnimationEditor(new TileAnimationEditor(mMainWindow))
143 {
144 mMainWindow->setDockOptions(mMainWindow->dockOptions() | QMainWindow::GroupedDragging);
145 mMainWindow->setDockNestingEnabled(true);
146 mMainWindow->setCentralWidget(mWidgetStack);
147
148 QAction *editCollision = mTileCollisionDock->toggleViewAction();
149 QAction *editWang = mWangDock->toggleViewAction();
150
151 ActionManager::registerAction(editCollision, "EditCollision");
152 ActionManager::registerAction(editWang, "EditWang");
153 ActionManager::registerAction(mAddTiles, "AddTiles");
154 ActionManager::registerAction(mRemoveTiles, "RemoveTiles");
155 ActionManager::registerAction(mRelocateTiles, "RelocateTiles");
156 ActionManager::registerAction(mShowAnimationEditor, "ShowAnimationEditor");
157 ActionManager::registerAction(mDynamicWrappingToggle, "DynamicWrappingToggle");
158
159 mAddTiles->setIcon(QIcon(QLatin1String(":images/16/add.png")));
160 mRemoveTiles->setIcon(QIcon(QLatin1String(":images/16/remove.png")));
161 mRelocateTiles->setIcon(QIcon(QLatin1String(":images/22/stock-tool-move-22.png")));
162 mRelocateTiles->setCheckable(true);
163 mRelocateTiles->setIconVisibleInMenu(false);
164 mShowAnimationEditor->setIcon(QIcon(QLatin1String(":images/24/animation-edit.png")));
165 mShowAnimationEditor->setCheckable(true);
166 mShowAnimationEditor->setIconVisibleInMenu(false);
167 editCollision->setIcon(QIcon(QLatin1String(":images/48/tile-collision-editor.png")));
168 editCollision->setIconVisibleInMenu(false);
169 editWang->setIcon(QIcon(QLatin1String(":images/24/terrain.png")));
170 editWang->setIconVisibleInMenu(false);
171 mDynamicWrappingToggle->setCheckable(true);
172 mDynamicWrappingToggle->setIcon(QIcon(QLatin1String("://images/scalable/wrap.svg")));
173
174 Utils::setThemeIcon(mAddTiles, "add");
175 Utils::setThemeIcon(mRemoveTiles, "remove");
176
177 mTilesetToolBar = mMainWindow->addToolBar(tr("Tileset"));
178 mTilesetToolBar->setObjectName(QLatin1String("TilesetToolBar"));
179 mTilesetToolBar->addAction(mAddTiles);
180 mTilesetToolBar->addAction(mRemoveTiles);
181 mTilesetToolBar->addSeparator();
182 mTilesetToolBar->addAction(mRelocateTiles);
183 mTilesetToolBar->addAction(editWang);
184 mTilesetToolBar->addAction(editCollision);
185 mTilesetToolBar->addAction(mShowAnimationEditor);
186 mTilesetToolBar->addSeparator();
187 mTilesetToolBar->addAction(mDynamicWrappingToggle);
188
189 mTemplatesDock->setPropertiesDock(mPropertiesDock);
190
191 resetLayout();
192
193 connect(mMainWindow, &TilesetEditorWindow::urlsDropped, this, &TilesetEditor::addTiles);
194
195 connect(mWidgetStack, &QStackedWidget::currentChanged, this, &TilesetEditor::currentWidgetChanged);
196
197 connect(mAddTiles, &QAction::triggered, this, &TilesetEditor::openAddTilesDialog);
198 connect(mRemoveTiles, &QAction::triggered, this, &TilesetEditor::removeTiles);
199
200 connect(mRelocateTiles, &QAction::toggled, this, &TilesetEditor::setRelocateTiles);
201 connect(editCollision, &QAction::toggled, this, &TilesetEditor::setEditCollision);
202 connect(editWang, &QAction::toggled, this, &TilesetEditor::setEditWang);
203 connect(mShowAnimationEditor, &QAction::toggled, mTileAnimationEditor, &TileAnimationEditor::setVisible);
204 connect(mDynamicWrappingToggle, &QAction::toggled, this, [this] (bool checked) {
205 if (TilesetView *view = currentTilesetView()) {
206 view->setDynamicWrapping(checked);
207
208 const QString fileName = mCurrentTilesetDocument->externalOrEmbeddedFileName();
209 Session::current().setFileStateValue(fileName, QLatin1String("dynamicWrapping"), checked);
210 }
211 });
212
213 connect(mTileAnimationEditor, &TileAnimationEditor::closed, this, &TilesetEditor::onAnimationEditorClosed);
214
215 connect(mWangDock, &WangDock::currentWangSetChanged, this, &TilesetEditor::currentWangSetChanged);
216 connect(mWangDock, &WangDock::currentWangIdChanged, this, &TilesetEditor::currentWangIdChanged);
217 connect(mWangDock, &WangDock::wangColorChanged, this, &TilesetEditor::wangColorChanged);
218 connect(mWangDock, &WangDock::addWangSetRequested, this, &TilesetEditor::addWangSet);
219 connect(mWangDock, &WangDock::duplicateWangSetRequested, this, &TilesetEditor::duplicateWangSet);
220 connect(mWangDock, &WangDock::removeWangSetRequested, this, &TilesetEditor::removeWangSet);
221 connect(mWangDock->wangColorView(), &WangColorView::wangColorColorPicked,
222 this, &TilesetEditor::setWangColorColor);
223 connect(DocumentManager::instance(), &DocumentManager::selectCustomPropertyRequested,
224 mPropertiesDock, &PropertiesDock::selectCustomProperty);
225
226 connect(this, &TilesetEditor::currentTileChanged, mTileAnimationEditor, &TileAnimationEditor::setTile);
227 connect(this, &TilesetEditor::currentTileChanged, mTileCollisionDock, &TileCollisionDock::setTile);
228 connect(this, &TilesetEditor::currentTileChanged, mTemplatesDock, &TemplatesDock::setTile);
229
230 connect(mTileCollisionDock, &TileCollisionDock::dummyMapDocumentChanged,
231 this, [this] {
232 mPropertiesDock->setDocument(mCurrentTilesetDocument);
233 });
234 connect(mTileCollisionDock, &TileCollisionDock::hasSelectedObjectsChanged,
235 this, &TilesetEditor::hasSelectedCollisionObjectsChanged);
236 connect(mTileCollisionDock, &TileCollisionDock::statusInfoChanged,
237 mStatusInfoLabel, &QLabel::setText);
238 connect(mTileCollisionDock, &TileCollisionDock::visibilityChanged,
239 this, &Editor::enabledStandardActionsChanged);
240
241 connect(mTemplatesDock, &TemplatesDock::currentTemplateChanged,
242 mTileCollisionDock->toolManager(), &ToolManager::setObjectTemplate);
243
244 connect(TilesetManager::instance(), &TilesetManager::tilesetImagesChanged,
245 this, &TilesetEditor::updateTilesetView);
246
247 retranslateUi();
248 connect(Preferences::instance(), &Preferences::languageChanged, this, &TilesetEditor::retranslateUi);
249 }
250
saveState()251 void TilesetEditor::saveState()
252 {
253 preferences::tilesetEditorSize = mMainWindow->size();
254 preferences::tilesetEditorState = mMainWindow->saveState();
255
256 mTileCollisionDock->saveState();
257 }
258
restoreState()259 void TilesetEditor::restoreState()
260 {
261 QSize size = preferences::tilesetEditorSize;
262 if (!size.isEmpty()) {
263 mMainWindow->resize(size);
264 mMainWindow->restoreState(preferences::tilesetEditorState);
265 }
266
267 mTileCollisionDock->restoreState();
268 }
269
addDocument(Document * document)270 void TilesetEditor::addDocument(Document *document)
271 {
272 TilesetDocument *tilesetDocument = qobject_cast<TilesetDocument*>(document);
273 Q_ASSERT(tilesetDocument);
274
275 TilesetView *view = new TilesetView(mWidgetStack);
276 view->setTilesetDocument(tilesetDocument);
277 view->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOn);
278 view->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn);
279
280 TilesetModel *tilesetModel = new TilesetModel(tilesetDocument, view);
281 view->setModel(tilesetModel);
282
283 connect(tilesetDocument, &TilesetDocument::tileWangSetChanged,
284 tilesetModel, &TilesetModel::tilesChanged);
285
286 connect(tilesetDocument, &TilesetDocument::tilesetChanged,
287 this, &TilesetEditor::tilesetChanged);
288 connect(tilesetDocument, &TilesetDocument::selectedTilesChanged,
289 this, &TilesetEditor::selectedTilesChanged);
290
291 connect(view, &TilesetView::wangSetImageSelected, this, &TilesetEditor::setWangSetImage);
292 connect(view, &TilesetView::wangColorImageSelected, this, &TilesetEditor::setWangColorImage);
293 connect(view, &TilesetView::wangIdUsedChanged, mWangDock, &WangDock::onWangIdUsedChanged);
294 connect(view, &TilesetView::currentWangIdChanged, mWangDock, &WangDock::onCurrentWangIdChanged);
295
296 QItemSelectionModel *s = view->selectionModel();
297 connect(s, &QItemSelectionModel::selectionChanged, this, &TilesetEditor::selectionChanged);
298 connect(s, &QItemSelectionModel::currentChanged, this, &TilesetEditor::currentChanged);
299 connect(view, &TilesetView::pressed, this, &TilesetEditor::indexPressed);
300
301 mViewForTileset.insert(tilesetDocument, view);
302 mWidgetStack->addWidget(view);
303
304 restoreDocumentState(tilesetDocument);
305 }
306
removeDocument(Document * document)307 void TilesetEditor::removeDocument(Document *document)
308 {
309 TilesetDocument *tilesetDocument = qobject_cast<TilesetDocument*>(document);
310 Q_ASSERT(tilesetDocument);
311 Q_ASSERT(mViewForTileset.contains(tilesetDocument));
312
313 if (tilesetDocument == mCurrentTilesetDocument)
314 setCurrentDocument(nullptr);
315
316 tilesetDocument->disconnect(this);
317
318 saveDocumentState(tilesetDocument);
319
320 TilesetView *view = mViewForTileset.take(tilesetDocument);
321
322 // remove first, to keep it valid while the current widget changes
323 mWidgetStack->removeWidget(view);
324 delete view;
325 }
326
setCurrentDocument(Document * document)327 void TilesetEditor::setCurrentDocument(Document *document)
328 {
329 TilesetDocument *tilesetDocument = qobject_cast<TilesetDocument*>(document);
330 Q_ASSERT(tilesetDocument || !document);
331
332 if (document && DocumentManager::instance()->currentEditor() == this)
333 DocumentManager::instance()->undoGroup()->setActiveStack(document->undoStack());
334
335 if (mCurrentTilesetDocument == tilesetDocument)
336 return;
337
338 TilesetView *tilesetView = nullptr;
339
340 if (document) {
341 tilesetView = mViewForTileset.value(tilesetDocument);
342 Q_ASSERT(tilesetView);
343
344 mWidgetStack->setCurrentWidget(tilesetView);
345 tilesetView->setEditWangSet(mWangDock->isVisible());
346 tilesetView->zoomable()->setComboBox(mZoomComboBox);
347 }
348
349 mPropertiesDock->setDocument(document);
350 mUndoDock->setStack(document ? document->undoStack() : nullptr);
351 mTileAnimationEditor->setTilesetDocument(tilesetDocument);
352 mTileCollisionDock->setTilesetDocument(tilesetDocument);
353 mWangDock->setDocument(document);
354
355 mCurrentTilesetDocument = tilesetDocument;
356
357 if (tilesetDocument) {
358 mDynamicWrappingToggle->setChecked(tilesetView->dynamicWrapping());
359
360 currentChanged(tilesetView->currentIndex());
361 selectionChanged();
362 }
363
364 updateAddRemoveActions();
365 }
366
currentDocument() const367 Document *TilesetEditor::currentDocument() const
368 {
369 return mCurrentTilesetDocument;
370 }
371
editorWidget() const372 QWidget *TilesetEditor::editorWidget() const
373 {
374 return mMainWindow;
375 }
376
toolBars() const377 QList<QToolBar *> TilesetEditor::toolBars() const
378 {
379 return QList<QToolBar*> {
380 mMainToolBar,
381 mTilesetToolBar
382 };
383 }
384
dockWidgets() const385 QList<QDockWidget *> TilesetEditor::dockWidgets() const
386 {
387 return QList<QDockWidget*> {
388 mPropertiesDock,
389 mUndoDock,
390 mTileCollisionDock,
391 mTemplatesDock,
392 mWangDock
393 };
394 }
395
statusBarWidgets() const396 QList<QWidget *> TilesetEditor::statusBarWidgets() const
397 {
398 return {
399 mStatusInfoLabel
400 };
401 }
402
permanentStatusBarWidgets() const403 QList<QWidget *> TilesetEditor::permanentStatusBarWidgets() const
404 {
405 return {
406 mZoomComboBox
407 };
408 }
409
enabledStandardActions() const410 Editor::StandardActions TilesetEditor::enabledStandardActions() const
411 {
412 StandardActions standardActions;
413
414 if (mCurrentTile && mTileCollisionDock->isVisible()) {
415 if (mTileCollisionDock->hasSelectedObjects())
416 standardActions |= CutAction | CopyAction | DeleteAction;
417
418 if (ClipboardManager::instance()->hasMap())
419 standardActions |= PasteAction | PasteInPlaceAction;
420 }
421
422 return standardActions;
423 }
424
performStandardAction(StandardAction action)425 void TilesetEditor::performStandardAction(StandardAction action)
426 {
427 switch (action) {
428 case CutAction:
429 mTileCollisionDock->cut();
430 break;
431 case CopyAction:
432 mTileCollisionDock->copy();
433 break;
434 case PasteAction:
435 mTileCollisionDock->paste();
436 break;
437 case PasteInPlaceAction:
438 mTileCollisionDock->pasteInPlace();
439 break;
440 case DeleteAction:
441 mTileCollisionDock->delete_();
442 break;
443 }
444 }
445
resetLayout()446 void TilesetEditor::resetLayout()
447 {
448 // Remove dock widgets (this also hides them)
449 const QList<QDockWidget*> dockWidgets = this->dockWidgets();
450 for (auto dockWidget : dockWidgets)
451 mMainWindow->removeDockWidget(dockWidget);
452
453 // Show Properties dock by default
454 mPropertiesDock->setVisible(true);
455
456 // Make sure all toolbars are visible
457 const QList<QToolBar*> toolBars = this->toolBars();
458 for (auto toolBar : toolBars)
459 toolBar->setVisible(true);
460
461 mMainWindow->addToolBar(mMainToolBar);
462 mMainWindow->addToolBar(mTilesetToolBar);
463
464 mMainWindow->addDockWidget(Qt::LeftDockWidgetArea, mPropertiesDock);
465 mMainWindow->addDockWidget(Qt::LeftDockWidgetArea, mUndoDock);
466 mMainWindow->addDockWidget(Qt::LeftDockWidgetArea, mTemplatesDock);
467 mMainWindow->tabifyDockWidget(mUndoDock, mTemplatesDock);
468
469 mMainWindow->addDockWidget(Qt::RightDockWidgetArea, mTileCollisionDock);
470 mMainWindow->addDockWidget(Qt::RightDockWidgetArea, mWangDock);
471 }
472
currentTilesetView() const473 TilesetView *TilesetEditor::currentTilesetView() const
474 {
475 return static_cast<TilesetView*>(mWidgetStack->currentWidget());
476 }
477
currentTileset() const478 Tileset *TilesetEditor::currentTileset() const
479 {
480 if (mCurrentTilesetDocument)
481 return mCurrentTilesetDocument->tileset().data();
482 return nullptr;
483 }
484
zoomable() const485 Zoomable *TilesetEditor::zoomable() const
486 {
487 if (auto view = currentTilesetView())
488 return view->zoomable();
489 return nullptr;
490 }
491
editCollisionAction() const492 QAction *TilesetEditor::editCollisionAction() const
493 {
494 return mTileCollisionDock->toggleViewAction();
495 }
496
editWangSetsAction() const497 QAction *TilesetEditor::editWangSetsAction() const
498 {
499 return mWangDock->toggleViewAction();
500 }
501
currentWidgetChanged()502 void TilesetEditor::currentWidgetChanged()
503 {
504 if (!mWidgetStack->currentWidget())
505 setCurrentDocument(nullptr);
506 }
507
selectionChanged()508 void TilesetEditor::selectionChanged()
509 {
510 TilesetView *view = currentTilesetView();
511 if (!view)
512 return;
513
514 updateAddRemoveActions();
515
516 const QItemSelectionModel *s = view->selectionModel();
517 const QModelIndexList indexes = s->selection().indexes();
518 if (indexes.isEmpty())
519 return;
520
521 const TilesetModel *model = view->tilesetModel();
522 QList<Tile*> selectedTiles;
523
524 for (const QModelIndex &index : indexes)
525 if (Tile *tile = model->tileAt(index))
526 selectedTiles.append(tile);
527
528 QScopedValueRollback<bool> settingSelectedTiles(mSettingSelectedTiles, true);
529 mCurrentTilesetDocument->setSelectedTiles(selectedTiles);
530 }
531
currentChanged(const QModelIndex & index)532 void TilesetEditor::currentChanged(const QModelIndex &index)
533 {
534 if (!index.isValid())
535 return;
536
537 auto model = static_cast<const TilesetModel*>(index.model());
538 setCurrentTile(model->tileAt(index));
539 }
540
indexPressed(const QModelIndex & index)541 void TilesetEditor::indexPressed(const QModelIndex &index)
542 {
543 TilesetView *view = currentTilesetView();
544 if (Tile *tile = view->tilesetModel()->tileAt(index))
545 mCurrentTilesetDocument->setCurrentObject(tile);
546 }
547
saveDocumentState(TilesetDocument * tilesetDocument) const548 void TilesetEditor::saveDocumentState(TilesetDocument *tilesetDocument) const
549 {
550 TilesetView *view = mViewForTileset.value(tilesetDocument);
551 if (!view)
552 return;
553
554 const QString fileName = tilesetDocument->externalOrEmbeddedFileName();
555 Session::current().setFileStateValue(fileName, QLatin1String("scaleInEditor"), view->scale());
556
557 // Some cleanup for potentially old preferences from Tiled 1.3
558 auto preferences = Preferences::instance();
559 QString path = QLatin1String("TilesetEditor/TilesetScale/") +
560 tilesetDocument->tileset()->name();
561 preferences->remove(path);
562 }
563
restoreDocumentState(TilesetDocument * tilesetDocument) const564 void TilesetEditor::restoreDocumentState(TilesetDocument *tilesetDocument) const
565 {
566 TilesetView *view = mViewForTileset.value(tilesetDocument);
567 if (!view)
568 return;
569
570 const QString fileName = tilesetDocument->externalOrEmbeddedFileName();
571 const QVariantMap fileState = Session::current().fileState(fileName);
572
573 if (fileState.isEmpty()) {
574 // Compatibility with Tiled 1.3
575 const Tileset *tileset = tilesetDocument->tileset().data();
576 const QString path = QLatin1String("TilesetEditor/TilesetScale/") + tileset->name();
577 const qreal scale = Preferences::instance()->value(path, 1).toReal();
578 view->zoomable()->setScale(scale);
579 return;
580 }
581
582 bool ok;
583 const qreal scale = fileState.value(QLatin1String("scaleInEditor")).toReal(&ok);
584 if (scale > 0 && ok)
585 view->zoomable()->setScale(scale);
586
587 if (fileState.contains(QLatin1String("dynamicWrapping"))) {
588 const bool dynamicWrapping = fileState.value(QLatin1String("dynamicWrapping")).toBool();
589 view->setDynamicWrapping(dynamicWrapping);
590 }
591 }
592
tilesetChanged()593 void TilesetEditor::tilesetChanged()
594 {
595 auto *tilesetDocument = static_cast<TilesetDocument*>(sender());
596 auto *tilesetView = mViewForTileset.value(tilesetDocument);
597 auto *model = tilesetView->tilesetModel();
598
599 if (tilesetDocument == mCurrentTilesetDocument)
600 setCurrentTile(nullptr); // It may be gone
601
602 tilesetView->updateBackgroundColor();
603 model->tilesetChanged();
604 }
605
selectedTilesChanged()606 void TilesetEditor::selectedTilesChanged()
607 {
608 if (mSettingSelectedTiles)
609 return;
610
611 if (mCurrentTilesetDocument != sender())
612 return;
613
614 TilesetView *tilesetView = currentTilesetView();
615 const TilesetModel *model = tilesetView->tilesetModel();
616
617 QItemSelection tileSelection;
618
619 for (Tile *tile : mCurrentTilesetDocument->selectedTiles()) {
620 const QModelIndex modelIndex = model->tileIndex(tile);
621 tileSelection.select(modelIndex, modelIndex);
622 }
623
624 QItemSelectionModel *selectionModel = tilesetView->selectionModel();
625 selectionModel->select(tileSelection, QItemSelectionModel::SelectCurrent);
626 if (!tileSelection.isEmpty()) {
627 selectionModel->setCurrentIndex(tileSelection.first().topLeft(),
628 QItemSelectionModel::NoUpdate);
629 }
630 }
631
updateTilesetView(Tileset * tileset)632 void TilesetEditor::updateTilesetView(Tileset *tileset)
633 {
634 if (!mCurrentTilesetDocument)
635 return;
636 if (mCurrentTilesetDocument->tileset().data() != tileset)
637 return;
638
639 TilesetModel *model = currentTilesetView()->tilesetModel();
640 model->tilesetChanged();
641 }
642
setCurrentTile(Tile * tile)643 void TilesetEditor::setCurrentTile(Tile *tile)
644 {
645 if (mCurrentTile == tile)
646 return;
647
648 mCurrentTile = tile;
649 emit currentTileChanged(tile);
650
651 if (tile)
652 mCurrentTilesetDocument->setCurrentObject(tile);
653 }
654
retranslateUi()655 void TilesetEditor::retranslateUi()
656 {
657 mTilesetToolBar->setWindowTitle(tr("Tileset"));
658
659 mAddTiles->setText(tr("Add Tiles"));
660 mRemoveTiles->setText(tr("Remove Tiles"));
661 mRelocateTiles->setText(tr("Rearrange Tiles"));
662 mShowAnimationEditor->setText(tr("Tile Animation Editor"));
663 mDynamicWrappingToggle->setText(tr("Dynamically Wrap Tiles"));
664
665 mTileCollisionDock->toggleViewAction()->setShortcut((Qt::CTRL | Qt::SHIFT) + Qt::Key_O);
666 }
667
hasTileInTileset(const QUrl & imageSource,const Tileset & tileset)668 static bool hasTileInTileset(const QUrl &imageSource, const Tileset &tileset)
669 {
670 for (auto tile : tileset.tiles()) {
671 if (tile->imageSource() == imageSource)
672 return true;
673 }
674 return false;
675 }
676
openAddTilesDialog()677 void TilesetEditor::openAddTilesDialog()
678 {
679 const Session &session = Session::current();
680 const QString startLocation = session.lastPath(Session::ImageFile);
681 const QString filter = Utils::readableImageFormatsFilter();
682 const auto urls = QFileDialog::getOpenFileUrls(mMainWindow->window(),
683 tr("Add Tiles"),
684 QUrl::fromLocalFile(startLocation),
685 filter);
686
687 if (!urls.isEmpty())
688 addTiles(urls);
689 }
690
addTiles(const QList<QUrl> & urls)691 void TilesetEditor::addTiles(const QList<QUrl> &urls)
692 {
693 Tileset *tileset = currentTileset();
694 if (!tileset)
695 return;
696
697 struct LoadedFile {
698 QUrl imageSource;
699 QPixmap image;
700 };
701 QVector<LoadedFile> loadedFiles;
702
703 // If the tile is already in the tileset, warn user and confirm addition
704 bool dontAskAgain = false;
705 bool rememberOption = true;
706 for (const QUrl &url : urls) {
707 if (!(dontAskAgain && rememberOption) && hasTileInTileset(url, *tileset)) {
708 if (dontAskAgain)
709 continue;
710 QCheckBox *checkBox = new QCheckBox(tr("Apply this action to all tiles"));
711 QMessageBox warning(QMessageBox::Warning,
712 tr("Add Tiles"),
713 tr("Tile \"%1\" already exists in the tileset!").arg(url.toString()),
714 QMessageBox::Yes | QMessageBox::No,
715 mMainWindow->window());
716 warning.setDefaultButton(QMessageBox::Yes);
717 warning.setInformativeText(tr("Add anyway?"));
718 warning.setCheckBox(checkBox);
719 int warningBoxChoice = warning.exec();
720 dontAskAgain = checkBox->checkState() == Qt::Checked;
721 rememberOption = warningBoxChoice == QMessageBox::Yes;
722 if (!rememberOption)
723 continue;
724 }
725 const QPixmap image(url.toLocalFile());
726 if (!image.isNull()) {
727 loadedFiles.append(LoadedFile { url, image });
728 } else {
729 // todo: support lazy loading of selected remote files
730 QMessageBox warning(QMessageBox::Warning,
731 tr("Add Tiles"),
732 tr("Could not load \"%1\"!").arg(url.toString()),
733 QMessageBox::Ignore | QMessageBox::Cancel,
734 mMainWindow->window());
735 warning.setDefaultButton(QMessageBox::Ignore);
736
737 if (warning.exec() != QMessageBox::Ignore)
738 return;
739 }
740 }
741
742 if (loadedFiles.isEmpty())
743 return;
744
745 const QString lastLocalFile = urls.last().toLocalFile();
746 if (!lastLocalFile.isEmpty()) {
747 Session &session = Session::current();
748 session.setLastPath(Session::ImageFile, QFileInfo(lastLocalFile).path());
749 }
750
751 QList<Tile*> tiles;
752 tiles.reserve(loadedFiles.size());
753
754 for (LoadedFile &loadedFile : loadedFiles) {
755 Tile *newTile = new Tile(tileset->takeNextTileId(), tileset);
756 newTile->setImage(loadedFile.image);
757 newTile->setImageSource(loadedFile.imageSource);
758 tiles.append(newTile);
759 }
760
761 mCurrentTilesetDocument->undoStack()->push(new AddTiles(mCurrentTilesetDocument, tiles));
762 }
763
hasTileReferences(MapDocument * mapDocument,std::function<bool (const Cell &)> condition)764 static bool hasTileReferences(MapDocument *mapDocument,
765 std::function<bool(const Cell &)> condition)
766 {
767 for (Layer *layer : mapDocument->map()->layers()) {
768 if (TileLayer *tileLayer = layer->asTileLayer()) {
769 if (tileLayer->hasCell(condition))
770 return true;
771
772 } else if (ObjectGroup *objectGroup = layer->asObjectGroup()) {
773 for (MapObject *object : *objectGroup) {
774 if (condition(object->cell()))
775 return true;
776 }
777 }
778 }
779
780 return false;
781 }
782
removeTileReferences(MapDocument * mapDocument,std::function<bool (const Cell &)> condition)783 static void removeTileReferences(MapDocument *mapDocument,
784 std::function<bool(const Cell &)> condition)
785 {
786 QUndoStack *undoStack = mapDocument->undoStack();
787 undoStack->beginMacro(QCoreApplication::translate("Undo Commands", "Remove Tiles"));
788
789 QList<MapObject*> objectsToRemove;
790
791 LayerIterator it(mapDocument->map());
792 while (Layer *layer = it.next()) {
793 switch (layer->layerType()) {
794 case Layer::TileLayerType: {
795 auto tileLayer = static_cast<TileLayer*>(layer);
796 const QRegion refs = tileLayer->region(condition);
797 if (!refs.isEmpty())
798 undoStack->push(new EraseTiles(mapDocument, tileLayer, refs));
799 break;
800 }
801 case Layer::ObjectGroupType: {
802 auto objectGroup = static_cast<ObjectGroup*>(layer);
803 for (MapObject *object : *objectGroup) {
804 if (condition(object->cell()))
805 objectsToRemove.append(object);
806 }
807 break;
808 }
809 case Layer::ImageLayerType:
810 case Layer::GroupLayerType:
811 break;
812 }
813 }
814
815 if (!objectsToRemove.isEmpty())
816 undoStack->push(new RemoveMapObjects(mapDocument, objectsToRemove));
817
818 undoStack->endMacro();
819 }
820
removeTiles()821 void TilesetEditor::removeTiles()
822 {
823 TilesetView *view = currentTilesetView();
824 if (!view)
825 return;
826 if (!view->selectionModel()->hasSelection())
827 return;
828
829 const QModelIndexList indexes = view->selectionModel()->selectedIndexes();
830 const TilesetModel *model = view->tilesetModel();
831 QList<Tile*> tiles;
832
833 for (const QModelIndex &index : indexes)
834 if (Tile *tile = model->tileAt(index))
835 tiles.append(tile);
836
837 auto matchesAnyTile = [&tiles] (const Cell &cell) {
838 if (Tile *tile = cell.tile())
839 return tiles.contains(tile);
840 return false;
841 };
842
843 QList<MapDocument *> mapsUsingTiles;
844 for (MapDocument *mapDocument : mCurrentTilesetDocument->mapDocuments())
845 if (hasTileReferences(mapDocument, matchesAnyTile))
846 mapsUsingTiles.append(mapDocument);
847
848 // If the tileset is in use, warn the user and confirm removal
849 if (!mapsUsingTiles.isEmpty()) {
850 QMessageBox warning(QMessageBox::Warning,
851 tr("Remove Tiles"),
852 tr("Tiles to be removed are in use by open maps!"),
853 QMessageBox::Yes | QMessageBox::No,
854 mMainWindow->window());
855 warning.setDefaultButton(QMessageBox::Yes);
856 warning.setInformativeText(tr("Remove all references to these tiles?"));
857
858 if (warning.exec() != QMessageBox::Yes)
859 return;
860 }
861
862 for (MapDocument *mapDocument : mapsUsingTiles)
863 removeTileReferences(mapDocument, matchesAnyTile);
864
865 mCurrentTilesetDocument->undoStack()->push(new RemoveTiles(mCurrentTilesetDocument, tiles));
866
867 // todo: make sure any current brushes are no longer referring to removed tiles
868 setCurrentTile(nullptr);
869 }
870
setRelocateTiles(bool relocateTiles)871 void TilesetEditor::setRelocateTiles(bool relocateTiles)
872 {
873 if (TilesetView *view = currentTilesetView())
874 view->setRelocateTiles(relocateTiles);
875
876 if (relocateTiles) {
877 mWangDock->setVisible(false);
878 mTileCollisionDock->setVisible(false);
879 }
880 }
881
setEditCollision(bool editCollision)882 void TilesetEditor::setEditCollision(bool editCollision)
883 {
884 if (editCollision) {
885 if (mTileCollisionDock->hasSelectedObjects())
886 mPropertiesDock->setDocument(mTileCollisionDock->dummyMapDocument());
887 mRelocateTiles->setChecked(false);
888 mWangDock->setVisible(false);
889 } else {
890 mPropertiesDock->setDocument(mCurrentTilesetDocument);
891 }
892 }
893
hasSelectedCollisionObjectsChanged()894 void TilesetEditor::hasSelectedCollisionObjectsChanged()
895 {
896 if (mTileCollisionDock->hasSelectedObjects())
897 mPropertiesDock->setDocument(mTileCollisionDock->dummyMapDocument());
898 else
899 mPropertiesDock->setDocument(mCurrentTilesetDocument);
900
901 emit enabledStandardActionsChanged();
902 }
903
setEditWang(bool editWang)904 void TilesetEditor::setEditWang(bool editWang)
905 {
906 if (TilesetView *view = currentTilesetView())
907 view->setEditWangSet(editWang);
908
909 if (editWang) {
910 mRelocateTiles->setChecked(false);
911 mTileCollisionDock->setVisible(false);
912 }
913 }
914
currentWangSetChanged(WangSet * wangSet)915 void TilesetEditor::currentWangSetChanged(WangSet *wangSet)
916 {
917 TilesetView *view = currentTilesetView();
918 if (!view)
919 return;
920
921 view->setWangSet(wangSet);
922 }
923
currentWangIdChanged(WangId wangId)924 void TilesetEditor::currentWangIdChanged(WangId wangId)
925 {
926 TilesetView *view = currentTilesetView();
927 if (!view)
928 return;
929
930 view->setWangId(wangId);
931 }
932
wangColorChanged(int color)933 void TilesetEditor::wangColorChanged(int color)
934 {
935 if (TilesetView *view = currentTilesetView())
936 view->setWangColor(color);
937 }
938
addWangSet(WangSet::Type type)939 void TilesetEditor::addWangSet(WangSet::Type type)
940 {
941 Tileset *tileset = currentTileset();
942 if (!tileset)
943 return;
944
945 WangSet *wangSet = new WangSet(tileset, QString(), type);
946 wangSet->setName(tr("Unnamed Set"));
947 wangSet->setColorCount(1);
948
949 mCurrentTilesetDocument->undoStack()->push(new AddWangSet(mCurrentTilesetDocument,
950 wangSet));
951
952 mWangDock->editWangSetName(wangSet);
953 }
954
duplicateWangSet()955 void TilesetEditor::duplicateWangSet()
956 {
957 Tileset *tileset = currentTileset();
958 if (!tileset)
959 return;
960
961 WangSet *wangSet = mWangDock->currentWangSet();
962 if (!wangSet)
963 return;
964
965 WangSet *duplicate = wangSet->clone(tileset);
966 duplicate->setName(QCoreApplication::translate("Tiled::MapDocument", "Copy of %1").arg(duplicate->name()));
967
968 mCurrentTilesetDocument->undoStack()->push(new AddWangSet(mCurrentTilesetDocument,
969 duplicate));
970
971 mWangDock->editWangSetName(duplicate);
972 }
973
removeWangSet()974 void TilesetEditor::removeWangSet()
975 {
976 WangSet *wangSet = mWangDock->currentWangSet();
977 if (!wangSet)
978 return;
979
980 mCurrentTilesetDocument->undoStack()->push(new RemoveWangSet(mCurrentTilesetDocument,
981 wangSet));
982 }
983
setWangSetImage(Tile * tile)984 void TilesetEditor::setWangSetImage(Tile *tile)
985 {
986 WangSet *wangSet = mWangDock->currentWangSet();
987 if (!wangSet)
988 return;
989
990 mCurrentTilesetDocument->undoStack()->push(new SetWangSetImage(mCurrentTilesetDocument,
991 wangSet,
992 tile->id()));
993 }
994
setWangColorImage(Tile * tile,int index)995 void TilesetEditor::setWangColorImage(Tile *tile, int index)
996 {
997 WangSet *wangSet = mWangDock->currentWangSet();
998 WangColor *wangColor = wangSet->colorAt(index).data();
999 mCurrentTilesetDocument->undoStack()->push(new ChangeWangColorImage(mCurrentTilesetDocument,
1000 wangColor,
1001 tile->id()));
1002 }
1003
setWangColorColor(WangColor * wangColor,const QColor & color)1004 void TilesetEditor::setWangColorColor(WangColor *wangColor, const QColor &color)
1005 {
1006 mCurrentTilesetDocument->undoStack()->push(new ChangeWangColorColor(mCurrentTilesetDocument,
1007 wangColor,
1008 color));
1009 }
1010
onAnimationEditorClosed()1011 void TilesetEditor::onAnimationEditorClosed()
1012 {
1013 mShowAnimationEditor->setChecked(false);
1014 }
1015
updateAddRemoveActions()1016 void TilesetEditor::updateAddRemoveActions()
1017 {
1018 bool isCollection = false;
1019 bool hasSelection = false;
1020
1021 if (Tileset *tileset = currentTileset()) {
1022 isCollection = tileset->isCollection();
1023 hasSelection = currentTilesetView()->selectionModel()->hasSelection();
1024 }
1025
1026 mAddTiles->setEnabled(isCollection);
1027 mRemoveTiles->setEnabled(isCollection && hasSelection);
1028 }
1029
1030 } // namespace Tiled
1031
1032 #include "tileseteditor.moc"
1033 #include "moc_tileseteditor.cpp"
1034