1 /*
2  * templatesdock.cpp
3  * Copyright 2017, Thorbjørn Lindeijer <thorbjorn@lindeijer.nl>
4  * Copyright 2017, Mohamed Thabet <thabetx@gmail.com>
5  *
6  * This file is part of Tiled.
7  *
8  * This program is free software; you can redistribute it and/or modify it
9  * under the terms of the GNU General Public License as published by the Free
10  * Software Foundation; either version 2 of the License, or (at your option)
11  * any later version.
12  *
13  * This program is distributed in the hope that it will be useful, but WITHOUT
14  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
15  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
16  * more details.
17  *
18  * You should have received a copy of the GNU General Public License along with
19  * this program. If not, see <http://www.gnu.org/licenses/>.
20  */
21 
22 #include "templatesdock.h"
23 
24 #include "documentmanager.h"
25 #include "editpolygontool.h"
26 #include "mapdocumentactionhandler.h"
27 #include "mapscene.h"
28 #include "mapview.h"
29 #include "objectgroup.h"
30 #include "objectselectiontool.h"
31 #include "propertiesdock.h"
32 #include "replacetileset.h"
33 #include "session.h"
34 #include "templatemanager.h"
35 #include "tilesetmanager.h"
36 #include "tilesetdocument.h"
37 #include "tmxmapformat.h"
38 #include "toolmanager.h"
39 #include "utils.h"
40 
41 #include <QAction>
42 #include <QBoxLayout>
43 #include <QFileDialog>
44 #include <QHeaderView>
45 #include <QLabel>
46 #include <QMenu>
47 #include <QMessageBox>
48 #include <QMimeData>
49 #include <QPushButton>
50 #include <QSplitter>
51 #include <QToolBar>
52 #include <QUndoStack>
53 
54 using namespace Tiled;
55 
56 // This references created dummy documents, to make sure they are shared if the
57 // same template is open in the MapEditor and the TilesetEditor.
58 QHash<ObjectTemplate*, QWeakPointer<MapDocument>> TemplatesDock::ourDummyDocuments;
59 bool TemplatesDock::ourEmittingChanged;
60 
TemplatesDock(QWidget * parent)61 TemplatesDock::TemplatesDock(QWidget *parent)
62     : QDockWidget(parent)
63     , mUndoAction(new QAction(this))
64     , mRedoAction(new QAction(this))
65     , mMapScene(new MapScene(this))
66     , mMapView(new MapView(this, MapView::NoStaticContents))
67     , mToolManager(new ToolManager(this))
68 {
69     setObjectName(QLatin1String("TemplatesDock"));
70 
71     // Prevent dropping a template into the editing view
72     mMapView->setAcceptDrops(false);
73     mMapView->setScene(mMapScene);
74 
75     // But accept drops on the dock
76     setAcceptDrops(true);
77 
78     mMapView->setResizeAnchor(QGraphicsView::AnchorViewCenter);
79     mMapView->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded);
80     mMapView->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
81 
82     mUndoAction->setIcon(QIcon(QLatin1String(":/images/16/edit-undo.png")));
83     Utils::setThemeIcon(mUndoAction, "edit-undo");
84     connect(mUndoAction, &QAction::triggered, this, &TemplatesDock::undo);
85 
86     mRedoAction->setIcon(QIcon(QLatin1String(":/images/16/edit-redo.png")));
87     Utils::setThemeIcon(mRedoAction, "edit-redo");
88     connect(mRedoAction, &QAction::triggered, this, &TemplatesDock::redo);
89 
90     // Initially disabled until a change happens
91     mUndoAction->setDisabled(true);
92     mRedoAction->setDisabled(true);
93 
94     QToolBar *editingToolBar = new QToolBar;
95     editingToolBar->setFloatable(false);
96     editingToolBar->setMovable(false);
97     editingToolBar->setIconSize(Utils::smallIconSize());
98 
99     auto objectSelectionTool = new ObjectSelectionTool(this);
100     auto editPolygonTool = new EditPolygonTool(this);
101 
102     // Assign empty shortcuts and don't register actions for these tools, to
103     // avoid collisions with the map editor and tile collision editor.
104     objectSelectionTool->setShortcut(QKeySequence());
105     editPolygonTool->setShortcut(QKeySequence());
106     mToolManager->setRegisterActions(false);
107 
108     editingToolBar->addAction(mUndoAction);
109     editingToolBar->addAction(mRedoAction);
110     editingToolBar->addSeparator();
111     editingToolBar->addAction(mToolManager->registerTool(objectSelectionTool));
112     editingToolBar->addAction(mToolManager->registerTool(editPolygonTool));
113 
114     mFixTilesetButton = new QPushButton(this);
115     connect(mFixTilesetButton, &QPushButton::clicked, this, &TemplatesDock::fixTileset);
116     mFixTilesetButton->setVisible(false);
117 
118     mDescriptionLabel = new QLabel;
119     mDescriptionLabel->setWordWrap(true);
120     mDescriptionLabel->setVisible(false);
121 
122     auto toolsLayout = new QHBoxLayout;
123     toolsLayout->addWidget(editingToolBar);
124     toolsLayout->addWidget(mFixTilesetButton);
125 
126     auto *editorLayout = new QVBoxLayout;
127     editorLayout->addLayout(toolsLayout);
128     editorLayout->addWidget(mDescriptionLabel);
129     editorLayout->addWidget(mMapView);
130     editorLayout->setContentsMargins(0, 0, 0, 0);
131     editorLayout->setSpacing(0);
132 
133     auto *widget = new QWidget;
134     widget->setLayout(editorLayout);
135 
136     setWidget(widget);
137     retranslateUi();
138 
139     connect(mToolManager, &ToolManager::selectedToolChanged,
140             mMapScene, &MapScene::setSelectedTool);
141 
142     connect(TemplateManager::instance(), &TemplateManager::objectTemplateChanged,
143             this, &TemplatesDock::objectTemplateChanged);
144 
145     setFocusPolicy(Qt::ClickFocus);
146     mMapView->setFocusProxy(this);
147 }
148 
~TemplatesDock()149 TemplatesDock::~TemplatesDock()
150 {
151     mMapScene->setSelectedTool(nullptr);
152 
153     if (mDummyMapDocument)
154         mDummyMapDocument->undoStack()->disconnect(this);
155 }
156 
setTile(Tile * tile)157 void TemplatesDock::setTile(Tile *tile)
158 {
159     mToolManager->setTile(tile);
160 }
161 
openTemplate(const QString & path)162 void TemplatesDock::openTemplate(const QString &path)
163 {
164     bringToFront();
165     setTemplate(TemplateManager::instance()->loadObjectTemplate(path));
166 }
167 
tryOpenTemplate(const QString & filePath)168 bool TemplatesDock::tryOpenTemplate(const QString &filePath)
169 {
170     auto objectTemplate = TemplateManager::instance()->loadObjectTemplate(filePath);
171     if (objectTemplate->object()) {
172         setTemplate(objectTemplate);
173         return true;
174     }
175     return false;
176 }
177 
bringToFront()178 void TemplatesDock::bringToFront()
179 {
180     show();
181     raise();
182     setFocus();
183 }
184 
readObjectTemplate(const QMimeData * mimeData)185 static ObjectTemplate *readObjectTemplate(const QMimeData *mimeData)
186 {
187     const auto urls = mimeData->urls();
188     if (urls.size() != 1)
189         return nullptr;
190 
191     const QString fileName = urls.first().toLocalFile();
192     if (fileName.isEmpty())
193         return nullptr;
194 
195     const QFileInfo info(fileName);
196     if (info.isDir())
197         return nullptr;
198 
199     auto objectTemplate = TemplateManager::instance()->loadObjectTemplate(info.absoluteFilePath());
200     return objectTemplate->object() ? objectTemplate : nullptr;
201 }
202 
dragEnterEvent(QDragEnterEvent * event)203 void TemplatesDock::dragEnterEvent(QDragEnterEvent *event)
204 {
205     if (!readObjectTemplate(event->mimeData()))
206         return;
207 
208     event->acceptProposedAction();
209 }
210 
dropEvent(QDropEvent * event)211 void TemplatesDock::dropEvent(QDropEvent *event)
212 {
213     if (auto objectTemplate = readObjectTemplate(event->mimeData()))
214         setTemplate(objectTemplate);
215 }
216 
setTemplate(ObjectTemplate * objectTemplate)217 void TemplatesDock::setTemplate(ObjectTemplate *objectTemplate)
218 {
219     if (mObjectTemplate == objectTemplate)
220         return;
221 
222     mObjectTemplate = objectTemplate;
223     refreshDummyObject();
224 
225     emit currentTemplateChanged(mObjectTemplate);
226 }
227 
refreshDummyObject()228 void TemplatesDock::refreshDummyObject()
229 {
230     mMapScene->setSelectedTool(nullptr);
231     MapDocumentPtr previousDocument = mDummyMapDocument;
232 
233     mMapView->setEnabled(mObjectTemplate);
234 
235     if (mObjectTemplate && mObjectTemplate->object()) {
236         mDummyMapDocument = ourDummyDocuments.value(mObjectTemplate);
237 
238         if (!mDummyMapDocument) {
239             // TODO: Isometric template objects are currently not supported
240             Map::Parameters mapParameters;
241 
242             // Setting sizes to 1 makes it render a one-pixel square (the map
243             // border), which serves as a somewhat hacky origin indicator.
244             mapParameters.width = 1;
245             mapParameters.height = 1;
246             mapParameters.tileWidth = 1;
247             mapParameters.tileHeight = 1;
248 
249             auto map = std::make_unique<Map>(mapParameters);
250 
251             MapObject *dummyObject = mObjectTemplate->object()->clone();
252             dummyObject->markAsTemplateBase();
253 
254             if (Tileset *tileset = dummyObject->cell().tileset()) {
255                 map->addTileset(tileset->sharedPointer());
256                 dummyObject->setPosition({-dummyObject->width() / 2, dummyObject->height() / 2});
257             } else {
258                 dummyObject->setPosition({-dummyObject->width() / 2, -dummyObject->height()  /2});
259             }
260 
261             ObjectGroup *objectGroup = new ObjectGroup;
262             objectGroup->addObject(dummyObject);
263 
264             map->addLayer(objectGroup);
265 
266             mDummyMapDocument = MapDocumentPtr::create(std::move(map));
267             mDummyMapDocument->setAllowHidingObjects(false);
268             mDummyMapDocument->switchCurrentLayer(objectGroup);
269 
270             ourDummyDocuments.insert(mObjectTemplate, mDummyMapDocument);
271         }
272 
273         mDummyMapDocument->setCurrentObject(dummyObject());
274 
275         mUndoAction->setEnabled(mDummyMapDocument->undoStack()->canUndo());
276         mRedoAction->setEnabled(mDummyMapDocument->undoStack()->canRedo());
277 
278         connect(mDummyMapDocument->undoStack(), &QUndoStack::indexChanged,
279                 this, &TemplatesDock::applyChanges);
280 
281         checkTileset();
282     } else {
283         mDummyMapDocument.reset();
284     }
285 
286     mMapScene->setMapDocument(mDummyMapDocument.data());
287     mToolManager->setMapDocument(mDummyMapDocument.data());
288     mPropertiesDock->setDocument(mDummyMapDocument.data());
289 
290     mMapScene->setSelectedTool(mToolManager->selectedTool());
291 
292     if (previousDocument)
293         previousDocument->undoStack()->disconnect(this);
294 }
295 
checkTileset()296 void TemplatesDock::checkTileset()
297 {
298     if (!mObjectTemplate || !mObjectTemplate->tileset()) {
299         mFixTilesetButton->setVisible(false);
300         mDescriptionLabel->setVisible(false);
301         return;
302     }
303 
304     auto templateName = QFileInfo(mObjectTemplate->fileName()).fileName();
305     auto tileset = mObjectTemplate->tileset();
306 
307     if (tileset->imageStatus() == LoadingError) {
308         mFixTilesetButton->setVisible(true);
309         mFixTilesetButton->setText(tr("Open Tileset"));
310         mFixTilesetButton->setToolTip(tileset->imageSource().fileName());
311 
312         mDescriptionLabel->setVisible(true);
313         mDescriptionLabel->setText(tr("%1: Couldn't find \"%2\"").arg(templateName,
314                                                                       tileset->imageSource().fileName()));
315         mDescriptionLabel->setToolTip(tileset->imageSource().fileName());
316     } else if (!tileset->fileName().isEmpty() && tileset->status() == LoadingError) {
317         mFixTilesetButton->setVisible(true);
318         mFixTilesetButton->setText(tr("Locate Tileset"));
319         mFixTilesetButton->setToolTip(tileset->fileName());
320 
321         mDescriptionLabel->setVisible(true);
322         mDescriptionLabel->setText(tr("%1: Couldn't find \"%2\"").arg(templateName,
323                                                                       tileset->fileName()));
324         mDescriptionLabel->setToolTip(tileset->fileName());
325     } else {
326         mFixTilesetButton->setVisible(false);
327         mDescriptionLabel->setVisible(false);
328     }
329 }
330 
objectTemplateChanged(ObjectTemplate * objectTemplate)331 void TemplatesDock::objectTemplateChanged(ObjectTemplate *objectTemplate)
332 {
333     if (ourEmittingChanged)
334         return;
335 
336     // Apparently the template was changed externally
337     ourDummyDocuments.remove(objectTemplate);
338 
339     if (mObjectTemplate == objectTemplate)
340         refreshDummyObject();
341 }
342 
undo()343 void TemplatesDock::undo()
344 {
345     if (mDummyMapDocument) {
346         mDummyMapDocument->undoStack()->undo();
347         emit mDummyMapDocument->selectedObjectsChanged();
348     }
349 }
350 
redo()351 void TemplatesDock::redo()
352 {
353     if (mDummyMapDocument) {
354         mDummyMapDocument->undoStack()->redo();
355         emit mDummyMapDocument->selectedObjectsChanged();
356     }
357 }
358 
applyChanges()359 void TemplatesDock::applyChanges()
360 {
361     mObjectTemplate->setObject(dummyObject());
362 
363     // Write out the template file
364     mObjectTemplate->save();
365 
366     mUndoAction->setEnabled(mDummyMapDocument->undoStack()->canUndo());
367     mRedoAction->setEnabled(mDummyMapDocument->undoStack()->canRedo());
368 
369     checkTileset();
370 
371     ourEmittingChanged = true;
372     emit TemplateManager::instance()->objectTemplateChanged(mObjectTemplate);
373     ourEmittingChanged = false;
374 }
375 
focusInEvent(QFocusEvent * event)376 void TemplatesDock::focusInEvent(QFocusEvent *event)
377 {
378     Q_UNUSED(event)
379     mPropertiesDock->setDocument(mDummyMapDocument.data());
380 }
381 
focusOutEvent(QFocusEvent * event)382 void TemplatesDock::focusOutEvent(QFocusEvent *event)
383 {
384     Q_UNUSED(event)
385 
386     if (hasFocus() || !mDummyMapDocument)
387         return;
388 
389     mDummyMapDocument->setSelectedObjects(QList<MapObject*>());
390 }
391 
retranslateUi()392 void TemplatesDock::retranslateUi()
393 {
394     setWindowTitle(tr("Template Editor"));
395 }
396 
fixTileset()397 void TemplatesDock::fixTileset()
398 {
399     if (!mObjectTemplate)
400         return;
401 
402     auto tileset = mObjectTemplate->tileset();
403     if (!tileset)
404         return;
405 
406     if (tileset->imageStatus() == LoadingError) {
407         // This code opens a new document even if there is a tileset document
408         auto tilesetDocument = DocumentManager::instance()->findTilesetDocument(tileset);
409 
410         if (!tilesetDocument) {
411             auto newTilesetDocument = TilesetDocumentPtr::create(tileset);
412             tilesetDocument = newTilesetDocument.data();
413             DocumentManager::instance()->addDocument(newTilesetDocument);
414         } else {
415             DocumentManager::instance()->openTileset(tileset);
416         }
417 
418         connect(tilesetDocument, &TilesetDocument::tilesetChanged,
419                 this, &TemplatesDock::checkTileset, Qt::UniqueConnection);
420     } else if (!tileset->fileName().isEmpty() && tileset->status() == LoadingError) {
421         FormatHelper<TilesetFormat> helper(FileFormat::Read, tr("All Files (*)"));
422 
423         Session &session = Session::current();
424         QString start = session.lastPath(Session::ExternalTileset);
425         QString fileName = QFileDialog::getOpenFileName(this, tr("Locate External Tileset"),
426                                                         start,
427                                                         helper.filter());
428 
429         if (!fileName.isEmpty()) {
430             session.setLastPath(Session::ExternalTileset, QFileInfo(fileName).path());
431 
432             QString error;
433             auto newTileset = TilesetManager::instance()->loadTileset(fileName, &error);
434             if (!newTileset || newTileset->status() == LoadingError) {
435                 QMessageBox::critical(window(), tr("Error Reading Tileset"), error);
436                 return;
437             }
438             // Replace with the first (and only) tileset.
439             mDummyMapDocument->undoStack()->push(new ReplaceTileset(mDummyMapDocument.data(), 0, newTileset));
440 
441             emit templateTilesetReplaced();
442         }
443     }
444 }
445 
dummyObject() const446 MapObject *TemplatesDock::dummyObject() const
447 {
448     if (!mDummyMapDocument)
449         return nullptr;
450 
451     return mDummyMapDocument->map()->layerAt(0)->asObjectGroup()->objectAt(0);
452 }
453 
454 #include "moc_templatesdock.cpp"
455