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