1 /*
2 * abstractworldtool.cpp
3 * Copyright 2019, Nils Kuebler <nils-kuebler@web.de>
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 "abstractworldtool.h"
22 #include "actionmanager.h"
23 #include "documentmanager.h"
24 #include "mainwindow.h"
25 #include "map.h"
26 #include "mapdocument.h"
27 #include "mapeditor.h"
28 #include "maprenderer.h"
29 #include "mapscene.h"
30 #include "mapview.h"
31 #include "selectionrectangle.h"
32 #include "tile.h"
33 #include "utils.h"
34 #include "worlddocument.h"
35 #include "worldmanager.h"
36
37 #include <QAction>
38 #include <QFileDialog>
39 #include <QKeyEvent>
40 #include <QMenu>
41 #include <QMessageBox>
42 #include <QToolBar>
43 #include <QToolButton>
44 #include <QUndoStack>
45 #include <QtMath>
46
47 #include "qtcompat_p.h"
48
49 namespace Tiled {
50
51 class AddMapCommand : public QUndoCommand
52 {
53 public:
AddMapCommand(const QString & worldName,const QString & mapName,const QRect & rect)54 AddMapCommand(const QString &worldName, const QString &mapName, const QRect &rect)
55 : mWorldName(worldName)
56 , mMapName(mapName)
57 , mRect(rect)
58 {
59 }
60
undo()61 void undo() override
62 {
63 WorldManager::instance().removeMap(mMapName);
64 }
65
redo()66 void redo() override
67 {
68 WorldManager::instance().addMap(mWorldName, mMapName, mRect);
69 }
70
71 private:
72 QString mWorldName;
73 QString mMapName;
74 QRect mRect;
75 };
76
77 class RemoveMapCommand : public QUndoCommand
78 {
79 public:
RemoveMapCommand(const QString & mapName)80 RemoveMapCommand(const QString &mapName)
81 : mMapName(mapName)
82 {
83 const WorldManager &manager = WorldManager::instance();
84 const World *world = manager.worldForMap(mMapName);
85 mPreviousRect = world->mapRect(mMapName);
86 mWorldName = world->fileName;
87 }
88
undo()89 void undo() override
90 {
91 WorldManager::instance().addMap(mWorldName, mMapName, mPreviousRect);
92 }
93
redo()94 void redo() override
95 {
96 // ensure we're switching to a differnet map in case the current map is removed
97 DocumentManager *manager = DocumentManager::instance();
98 if (manager->currentDocument() && manager->currentDocument()->fileName() == mMapName) {
99 const World *world = WorldManager::instance().worldForMap(mMapName);
100 for (World::MapEntry &entry : world->allMaps())
101 if (entry.fileName != mMapName) {
102 manager->switchToDocument(entry.fileName);
103 break;
104 }
105 }
106 WorldManager::instance().removeMap(mMapName);
107 }
108
109 private:
110 QString mWorldName;
111 QString mMapName;
112 QRect mPreviousRect;
113 };
114
115
AbstractWorldTool(Id id,const QString & name,const QIcon & icon,const QKeySequence & shortcut,QObject * parent)116 AbstractWorldTool::AbstractWorldTool(Id id,
117 const QString &name,
118 const QIcon &icon,
119 const QKeySequence &shortcut,
120 QObject *parent)
121 : AbstractTool(id, name, icon, shortcut, parent)
122 , mSelectionRectangle(new SelectionRectangle)
123 {
124 mSelectionRectangle->setVisible(false);
125
126 WorldManager &worldManager = WorldManager::instance();
127 connect(&worldManager, &WorldManager::worldsChanged, this, &AbstractWorldTool::updateEnabledState);
128
129 QIcon addAnotherMapToWorldIcon(QLatin1String(":images/24/world-map-add-other.png"));
130 mAddAnotherMapToWorldAction = new QAction(this);
131 mAddAnotherMapToWorldAction->setIcon(addAnotherMapToWorldIcon);
132 mAddAnotherMapToWorldAction->setShortcut(Qt::SHIFT + Qt::Key_A);
133 ActionManager::registerAction(mAddAnotherMapToWorldAction, "AddAnotherMap");
134 connect(mAddAnotherMapToWorldAction, &QAction::triggered, this, &AbstractWorldTool::addAnotherMapToWorldAtCenter);
135
136 QIcon addMapToWorldIcon(QLatin1String(":images/24/world-map-add-this.png"));
137 mAddMapToWorldAction = new QAction(this);
138 mAddMapToWorldAction->setIcon(addMapToWorldIcon);
139 mAddMapToWorldAction->setShortcut(Qt::SHIFT + Qt::Key_A);
140 ActionManager::registerAction(mAddMapToWorldAction, "AddMap");
141
142 QIcon removeMapFromWorldIcon(QLatin1String(":images/24/world-map-remove-this.png"));
143 mRemoveMapFromWorldAction = new QAction(this);
144 mRemoveMapFromWorldAction->setIcon(removeMapFromWorldIcon);
145 mRemoveMapFromWorldAction->setShortcut(Qt::SHIFT + Qt::Key_D);
146 ActionManager::registerAction(mRemoveMapFromWorldAction, "RemoveMap");
147 connect(mRemoveMapFromWorldAction, &QAction::triggered, this, &AbstractWorldTool::removeCurrentMapFromWorld);
148 }
149
150 AbstractWorldTool::~AbstractWorldTool() = default;
151
activate(MapScene * scene)152 void AbstractWorldTool::activate(MapScene *scene)
153 {
154 scene->addItem(mSelectionRectangle.get());
155 connect(scene, &MapScene::sceneRefreshed, this, &AbstractWorldTool::updateSelectionRectangle);
156 AbstractTool::activate(scene);
157 }
158
deactivate(MapScene * scene)159 void AbstractWorldTool::deactivate(MapScene *scene)
160 {
161 scene->removeItem(mSelectionRectangle.get());
162 disconnect(scene, &MapScene::sceneRefreshed, this, &AbstractWorldTool::updateSelectionRectangle);
163 AbstractTool::deactivate(scene);
164 }
165
mouseLeft()166 void AbstractWorldTool::mouseLeft()
167 {
168 setStatusInfo(QString());
169 }
170
mouseMoved(const QPointF & pos,Qt::KeyboardModifiers)171 void AbstractWorldTool::mouseMoved(const QPointF &pos,
172 Qt::KeyboardModifiers)
173 {
174 setTargetMap(mapAt(pos));
175
176 // Take into account the offset of the current layer
177 QPointF offsetPos = pos;
178 if (Layer *layer = currentLayer())
179 offsetPos -= mapScene()->absolutePositionForLayer(*layer);
180
181 const QPoint pixelPos = offsetPos.toPoint();
182 const QPointF tilePosF = mapDocument()->renderer()->screenToTileCoords(offsetPos);
183 const int x = qFloor(tilePosF.x());
184 const int y = qFloor(tilePosF.y());
185 setStatusInfo(QStringLiteral("%1, %2 (%3, %4)").arg(x).arg(y).arg(pixelPos.x()).arg(pixelPos.y()));
186 }
187
mousePressed(QGraphicsSceneMouseEvent * event)188 void AbstractWorldTool::mousePressed(QGraphicsSceneMouseEvent *event)
189 {
190 setTargetMap(mapAt(event->scenePos()));
191
192 if (event->button() == Qt::RightButton)
193 showContextMenu(event);
194 }
195
languageChanged()196 void AbstractWorldTool::languageChanged()
197 {
198 mAddAnotherMapToWorldAction->setText(tr("Add another map to the current world"));
199 mAddMapToWorldAction->setText(tr("Add the current map to a loaded world"));
200 mRemoveMapFromWorldAction->setText(tr("Remove the current map from the current world"));
201 }
202
updateEnabledState()203 void AbstractWorldTool::updateEnabledState()
204 {
205 const bool hasWorlds = !WorldManager::instance().worlds().isEmpty();
206 const World *world = constWorld(mapDocument());
207 setEnabled(mapDocument() && hasWorlds && (world == nullptr || world->canBeModified()));
208
209 // update toolbar actions
210 mAddMapToWorldAction->setEnabled(hasWorlds && world == nullptr);
211 mRemoveMapFromWorldAction->setEnabled(world != nullptr);
212 mAddAnotherMapToWorldAction->setEnabled(world != nullptr);
213 }
214
mapAt(const QPointF & pos) const215 MapDocument *AbstractWorldTool::mapAt(const QPointF &pos) const
216 {
217 const QList<QGraphicsItem *> &items = mapScene()->items(pos);
218
219 for (QGraphicsItem *item : items) {
220 if (!item->isEnabled())
221 continue;
222
223 auto mapItem = qgraphicsitem_cast<MapItem*>(item);
224 if (mapItem)
225 return mapItem->mapDocument();
226 }
227 return nullptr;
228 }
229
mapCanBeMoved(MapDocument * mapDocument) const230 bool AbstractWorldTool::mapCanBeMoved(MapDocument *mapDocument) const
231 {
232 if (!mapDocument)
233 return false;
234 const World *world = constWorld(mapDocument);
235 return world != nullptr && world->canBeModified();
236 }
237
238
mapRect(MapDocument * mapDocument) const239 QRect AbstractWorldTool::mapRect(MapDocument *mapDocument) const
240 {
241 QRect rect = mapDocument->renderer()->mapBoundingRect();
242 if (const World *world = constWorld(mapDocument))
243 rect.translate(world->mapRect(mapDocument->fileName()).topLeft());
244 return rect;
245 }
246
constWorld(MapDocument * mapDocument) const247 const World *AbstractWorldTool::constWorld(MapDocument *mapDocument) const
248 {
249 if (!mapDocument)
250 return nullptr;
251 return WorldManager::instance().worldForMap(mapDocument->fileName());
252 }
253
254 /**
255 * Shows the context menu for adding/removing maps from worlds.
256 */
showContextMenu(QGraphicsSceneMouseEvent * event)257 void AbstractWorldTool::showContextMenu(QGraphicsSceneMouseEvent *event)
258 {
259 MapDocument *currentDocument = mapDocument();
260 MapDocument *targetDocument = targetMap();
261 const World *currentWorld = constWorld(currentDocument);
262 const World *targetWorld = constWorld(targetDocument);
263
264 const auto screenPos = event->screenPos();
265
266 QMenu menu;
267 if (currentWorld) {
268 QPoint insertPos = event->scenePos().toPoint();
269 insertPos += mapRect(currentDocument).topLeft();
270
271 menu.addAction(QIcon(QLatin1String(":images/24/world-map-add-other.png")),
272 tr("Add a Map to World \"%2\"")
273 .arg(currentWorld->displayName()),
274 this, [=] { addAnotherMapToWorld(insertPos); });
275
276 if (targetDocument != nullptr && targetDocument != currentDocument) {
277 const QString targetFilename = targetDocument->fileName();
278 menu.addAction(QIcon(QLatin1String(":images/24/world-map-remove-this.png")),
279 tr("Remove \"%1\" from World \"%2\"")
280 .arg(targetDocument->displayName())
281 .arg(targetWorld->displayName()),
282 this, [=] { removeFromWorld(targetFilename); });
283 }
284 } else {
285 for (const World *world : WorldManager::instance().worlds()) {
286 if (!world->canBeModified())
287 continue;
288
289 menu.addAction(tr("Add \"%1\" to World \"%2\"")
290 .arg(currentDocument->displayName())
291 .arg(world->displayName()),
292 this, [=] { addToWorld(world); });
293 }
294 }
295
296 menu.exec(screenPos);
297 }
298
addAnotherMapToWorldAtCenter()299 void AbstractWorldTool::addAnotherMapToWorldAtCenter()
300 {
301 DocumentManager *manager = DocumentManager::instance();
302 MapView *view = manager->viewForDocument(mapDocument());
303 const QRectF viewRect { view->viewport()->rect() };
304 const QRectF sceneViewRect = view->viewportTransform().inverted().mapRect(viewRect);
305 addAnotherMapToWorld(sceneViewRect.center().toPoint());
306 }
307
addAnotherMapToWorld(QPoint insertPos)308 void AbstractWorldTool::addAnotherMapToWorld(QPoint insertPos)
309 {
310 MapDocument *map = mapDocument();
311 const World *world = constWorld(map);
312 if (!world)
313 return;
314
315 const QDir dir = QFileInfo(map->fileName()).dir();
316 const QString lastPath = QDir::cleanPath(dir.absolutePath());
317 QString filter = tr("All Files (*)");
318 FormatHelper<MapFormat> helper(FileFormat::ReadWrite, filter);
319
320 QString fileName = QFileDialog::getOpenFileName(MainWindow::instance(), tr("Load Map"), lastPath,
321 helper.filter());
322 if (fileName.isEmpty())
323 return;
324
325 const World *constWorldForSelectedMap = WorldManager::instance().worldForMap(fileName);
326 if (constWorldForSelectedMap) {
327 DocumentManager::instance()->openFile(fileName);
328 return;
329 }
330
331 QString error;
332 DocumentPtr document = DocumentManager::instance()->loadDocument(fileName, nullptr, &error);
333
334 if (!document) {
335 QMessageBox::critical(MainWindow::instance(),
336 tr("Error Opening File"),
337 tr("Error opening '%1':\n%2").arg(fileName, error));
338 return;
339 }
340
341 const QRect rect { snapPoint(insertPos, map), QSize(0, 0) };
342
343 undoStack()->push(new AddMapCommand(world->fileName, fileName, rect));
344 }
345
removeCurrentMapFromWorld()346 void AbstractWorldTool::removeCurrentMapFromWorld()
347 {
348 removeFromWorld(mapDocument()->fileName());
349 }
350
removeFromWorld(const QString & mapFileName)351 void AbstractWorldTool::removeFromWorld(const QString &mapFileName)
352 {
353 undoStack()->push(new RemoveMapCommand(mapFileName));
354 }
355
addToWorld(const World * world)356 void AbstractWorldTool::addToWorld(const World *world)
357 {
358 MapDocument *document = mapDocument();
359 QRect rect = document->renderer()->mapBoundingRect();
360
361 // Position the map alongside the last map by default
362 if (!world->maps.isEmpty()) {
363 const QRect &lastWorldRect = world->maps.last().rect;
364 rect.moveTo(lastWorldRect.right() + 1, lastWorldRect.top());
365 }
366
367 QUndoStack *undoStack = DocumentManager::instance()->ensureWorldDocument(world->fileName)->undoStack();
368 undoStack->push(new AddMapCommand(world->fileName, document->fileName(), rect));
369 }
370
undoStack()371 QUndoStack *AbstractWorldTool::undoStack()
372 {
373 const World *world = constWorld(mapDocument());
374 if (!world)
375 return nullptr;
376 return DocumentManager::instance()->ensureWorldDocument(world->fileName)->undoStack();
377 }
378
populateToolBar(QToolBar * toolBar)379 void AbstractWorldTool::populateToolBar(QToolBar *toolBar)
380 {
381 toolBar->addAction(mAddAnotherMapToWorldAction);
382 toolBar->addAction(mAddMapToWorldAction);
383 toolBar->addAction(mRemoveMapFromWorldAction);
384
385 auto addMapToWorldButton = qobject_cast<QToolButton*>(toolBar->widgetForAction(mAddMapToWorldAction));
386 auto addToWorldMenu = new QMenu(addMapToWorldButton);
387
388 connect(addToWorldMenu, &QMenu::aboutToShow, [=] {
389 addToWorldMenu->clear();
390
391 for (const World *world : WorldManager::instance().worlds()) {
392 if (!world->canBeModified())
393 continue;
394
395 addToWorldMenu->addAction(tr("Add \"%1\" to World \"%2\"")
396 .arg(mapDocument()->displayName())
397 .arg(world->displayName()),
398 this, [=] { addToWorld(world); });
399 }
400 });
401
402 addMapToWorldButton->setPopupMode(QToolButton::InstantPopup);
403 addMapToWorldButton->setMenu(addToWorldMenu);
404
405 // Workaround to make the shortcut for opening the menu work
406 connect(mAddMapToWorldAction, &QAction::triggered, addMapToWorldButton, [=] {
407 addMapToWorldButton->setDown(true);
408 addMapToWorldButton->showMenu();
409 });
410 }
411
snapPoint(QPoint point,MapDocument * document) const412 QPoint AbstractWorldTool::snapPoint(QPoint point, MapDocument *document) const
413 {
414 point.setX(point.x() - point.x() % document->map()->tileWidth());
415 point.setY(point.y() - point.y() % document->map()->tileHeight());
416 return point;
417 }
418
setTargetMap(MapDocument * mapDocument)419 void AbstractWorldTool::setTargetMap(MapDocument *mapDocument)
420 {
421 mTargetMap = mapDocument;
422 updateSelectionRectangle();
423 }
424
updateSelectionRectangle()425 void AbstractWorldTool::updateSelectionRectangle()
426 {
427 if (auto item = mapScene()->mapItem(mTargetMap)) {
428 auto rect = mapRect(mTargetMap);
429 rect.moveTo(item->pos().toPoint());
430
431 mSelectionRectangle->setVisible(true);
432 mSelectionRectangle->setRectangle(rect);
433 } else {
434 mSelectionRectangle->setVisible(false);
435 }
436 }
437
438 } // namespace Tiled
439
440 #include "moc_abstractworldtool.cpp"
441