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