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