Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
* Added 'Collapse All' action and 'Only Expand to Current' mode to Project view (with rhythmcache, #4346)
* Added command variables %exportfile and %exportpath (#4476)
* Added a configurable world grid with snapping for maps (by Kanishka, #4534)
* Added drag-to-resize for maps in the world view (by Kanishka, #4545)
* Added drag-to-resize for maps in the world view, usable even when they are not part of a world (by Kanishka, #4545, #4562)
* Added an action to create a world containing the current map (by Kanishka, #4562)
* Made switching to the previously selected tool when pressing its shortcut again optional and off by default (by dogboydog, #4540)
* Persisted collapsed state of the properties groups in the session (#4561)
* Scripting: Added 'tiled.cell' function, 'cell.flags' property and 'TileLayerEdit.setCell' function (#4538)
Expand Down
73 changes: 69 additions & 4 deletions src/tiled/abstractworldtool.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,12 @@ AbstractWorldTool::AbstractWorldTool(Id id,
WorldManager &worldManager = WorldManager::instance();
connect(&worldManager, &WorldManager::worldsChanged, this, &AbstractWorldTool::updateEnabledState);

QIcon newWorldForMapIcon(QLatin1String(":images/24/world-map-add-other.png"));
mNewWorldForMapAction = new QAction(this);
mNewWorldForMapAction->setIcon(newWorldForMapIcon);
ActionManager::registerAction(mNewWorldForMapAction, "NewWorldForMap");
connect(mNewWorldForMapAction, &QAction::triggered, this, &AbstractWorldTool::createWorldForCurrentMap);

QIcon addAnotherMapToWorldIcon(QLatin1String(":images/24/world-map-add-other.png"));
mAddAnotherMapToWorldAction = new QAction(this);
mAddAnotherMapToWorldAction->setIcon(addAnotherMapToWorldIcon);
Expand Down Expand Up @@ -202,21 +208,46 @@ void AbstractWorldTool::languageChanged()

void AbstractWorldTool::languageChangedImpl()
{
mNewWorldForMapAction->setText(tr("Create a new world containing the current map"));
mAddAnotherMapToWorldAction->setText(tr("Add another map to the current world"));
mAddMapToWorldAction->setText(tr("Add the current map to a loaded world"));
mRemoveMapFromWorldAction->setText(tr("Remove the current map from the current world"));
}

void AbstractWorldTool::mapDocumentChanged(MapDocument *oldDocument,
MapDocument *newDocument)
{
// The enabled state of the actions depends on the map's file name
if (oldDocument)
disconnect(oldDocument, &Document::fileNameChanged,
this, &AbstractWorldTool::updateEnabledState);
if (newDocument)
connect(newDocument, &Document::fileNameChanged,
this, &AbstractWorldTool::updateEnabledState);
}

void AbstractWorldTool::updateEnabledState()
{
const bool hasWorlds = !WorldManager::instance().worlds().isEmpty();
const auto worldDocument = worldForMap(mapDocument());
setEnabled(mapDocument() && hasWorlds && (!worldDocument || worldDocument->world()->canBeModified()));
MapDocument *map = mapDocument();
const auto worldDocument = worldForMap(map);

// Maps that are not in a world can still be resized
setEnabled(mapCanBeResized(map));

// When the map is not in a world, only the action for creating a new
// world is shown, which guides the user to create one first
mNewWorldForMapAction->setVisible(!worldDocument);
mNewWorldForMapAction->setEnabled(map && !map->fileName().isEmpty() && !worldDocument);

// update toolbar actions
mAddAnotherMapToWorldAction->setVisible(worldDocument);
mAddAnotherMapToWorldAction->setEnabled(worldDocument);

mAddMapToWorldAction->setVisible(!worldDocument);
mAddMapToWorldAction->setEnabled(hasWorlds && !worldDocument);

mRemoveMapFromWorldAction->setVisible(worldDocument);
mRemoveMapFromWorldAction->setEnabled(worldDocument);
mAddAnotherMapToWorldAction->setEnabled(worldDocument);
}

MapDocument *AbstractWorldTool::mapAt(const QPointF &pos) const
Expand Down Expand Up @@ -273,6 +304,15 @@ bool AbstractWorldTool::mapCanBeMoved(MapDocument *mapDocument) const
return worldDocument && worldDocument->world()->canBeModified();
}

// Resizing only changes the map itself, so it also works without a world
bool AbstractWorldTool::mapCanBeResized(MapDocument *mapDocument) const
{
if (!mapDocument)
return false;
auto worldDocument = worldForMap(mapDocument);
return !worldDocument || worldDocument->world()->canBeModified();
}

QRect AbstractWorldTool::mapRect(MapDocument *mapDocument) const
{
auto rect = mapDocument->renderer()->mapBoundingRect();
Expand Down Expand Up @@ -316,6 +356,12 @@ void AbstractWorldTool::showContextMenu(QGraphicsSceneMouseEvent *event)
this, [=] { removeFromWorld(currentWorldDocument, targetFilename); });
}
} else {
menu.addAction(QIcon(QLatin1String(":images/24/world-map-add-other.png")),
tr("New World Containing \"%1\"")
.arg(mapDocument()->displayName()),
this, &AbstractWorldTool::createWorldForCurrentMap)
->setEnabled(!mapDocument()->fileName().isEmpty());

populateAddToWorldMenu(menu);
}

Expand Down Expand Up @@ -381,6 +427,24 @@ void AbstractWorldTool::addAnotherMapToWorld(QPoint insertPos)
undoStack->push(new AddMapCommand(worldDocument, fileName, rect));
}

// Asks for a file name and creates a new world containing the current map.
// Does nothing when the map is already part of a world, the user cancels or
// the world could not be saved.
void AbstractWorldTool::createWorldForCurrentMap()
{
MapDocument *map = mapDocument();
if (!map || map->fileName().isEmpty() || worldForMap(map))
return;

const QFileInfo fileInfo(map->fileName());
const QString suggestedFileName
= fileInfo.dir().filePath(fileInfo.completeBaseName() +
QStringLiteral(".world"));

if (auto worldDocument = MainWindow::instance()->createNewWorld(suggestedFileName))
addToWorld(worldDocument);
}

void AbstractWorldTool::removeCurrentMapFromWorld()
{
if (auto currentWorldDocument = worldForMap(mapDocument()))
Expand Down Expand Up @@ -424,6 +488,7 @@ QUndoStack *AbstractWorldTool::undoStack()

void AbstractWorldTool::populateToolBar(QToolBar *toolBar)
{
toolBar->addAction(mNewWorldForMapAction);
toolBar->addAction(mAddAnotherMapToWorldAction);
toolBar->addAction(mAddMapToWorldAction);
toolBar->addAction(mRemoveMapFromWorldAction);
Expand Down
6 changes: 6 additions & 0 deletions src/tiled/abstractworldtool.h
Original file line number Diff line number Diff line change
Expand Up @@ -91,10 +91,14 @@ class AbstractWorldTool : public AbstractTool
*/
void updateEnabledState() override;

void mapDocumentChanged(MapDocument *oldDocument,
MapDocument *newDocument) override;

MapDocument *mapAt(const QPointF &pos) const;

void addAnotherMapToWorldAtCenter();
void addAnotherMapToWorld(QPoint insertPos);
void createWorldForCurrentMap();
void removeCurrentMapFromWorld();
void removeFromWorld(WorldDocument *worldDocument, const QString &mapFileName);
void addToWorld(WorldDocument *worldDocument);
Expand All @@ -111,6 +115,7 @@ class AbstractWorldTool : public AbstractTool
void recenterView(const QPoint &offset);

bool mapCanBeMoved(MapDocument *mapDocument) const;
bool mapCanBeResized(MapDocument *mapDocument) const;
QRect mapRect(MapDocument *mapDocument) const;
WorldDocument *worldForMap(MapDocument *mapDocument) const;

Expand All @@ -123,6 +128,7 @@ class AbstractWorldTool : public AbstractTool

MapDocument *mTargetMap = nullptr;

QAction *mNewWorldForMapAction;
QAction *mAddAnotherMapToWorldAction;
QAction *mAddMapToWorldAction;
QAction *mRemoveMapFromWorldAction;
Expand Down
95 changes: 66 additions & 29 deletions src/tiled/mainwindow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -592,6 +592,12 @@ MainWindow::MainWindow(QWidget *parent, Qt::WindowFlags flags)
this, &MainWindow::addExternalTileset);
connect(mUi->actionAddAutomappingRulesTileset, &QAction::triggered,
this, &MainWindow::addAutomappingRulesTileset);

// Remember the loaded worlds before switching session or quitting
connect(preferences, &Preferences::aboutToSwitchSession, this, [this] {
mLoadedWorlds = WorldManager::instance().worldFileNames();
});

connect(mUi->actionLoadWorld, &QAction::triggered, this, [this] {
Session &session = Session::current();
QString lastPath = session.lastPath(Session::WorldFile);
Expand All @@ -608,8 +614,6 @@ MainWindow::MainWindow(QWidget *parent, Qt::WindowFlags flags)
QString errorString;
if (!WorldManager::instance().loadWorld(worldFile, &errorString))
QMessageBox::critical(this, tr("Error Loading World"), errorString);
else
mLoadedWorlds = WorldManager::instance().worldFileNames();
});
connect(mUi->menuUnloadWorld, &QMenu::aboutToShow, this, [this] {
mUi->menuUnloadWorld->clear();
Expand All @@ -624,7 +628,6 @@ MainWindow::MainWindow(QWidget *parent, Qt::WindowFlags flags)
return;

WorldManager::instance().unloadWorld(worldDocument);
mLoadedWorlds = WorldManager::instance().worldFileNames();
});
}
if (WorldManager::instance().worlds().count() >= 2) {
Expand All @@ -633,30 +636,7 @@ MainWindow::MainWindow(QWidget *parent, Qt::WindowFlags flags)
}
});
connect(mUi->actionNewWorld, &QAction::triggered, this, [this] {
Session &session = Session::current();
QString lastPath = session.lastPath(Session::WorldFile);
QString filter = tr("All Files (*)");
filter.append(QStringLiteral(";;"));
QString worldFilesFilter = tr("World files (*.world)");
filter.append(worldFilesFilter);
QString worldFile;

QFileDialog dialog(this, tr("New World"), lastPath, filter);
dialog.setAcceptMode(QFileDialog::AcceptSave);
dialog.selectNameFilter(worldFilesFilter);
dialog.setDefaultSuffix(QStringLiteral("world"));
if (dialog.exec() == QDialog::Accepted)
worldFile = dialog.selectedFiles().value(0);

if (worldFile.isEmpty())
return;

session.setLastPath(Session::WorldFile, QFileInfo(worldFile).path());
QString errorString;
if (!WorldManager::instance().addEmptyWorld(worldFile, &errorString))
QMessageBox::critical(this, tr("Error Creating World"), errorString);
else
mLoadedWorlds = WorldManager::instance().worldFileNames();
createNewWorld();
});
connect(mUi->menuSaveWorld, &QMenu::aboutToShow, this, [this] {
mUi->menuSaveWorld->clear();
Expand Down Expand Up @@ -1072,8 +1052,6 @@ bool MainWindow::openFile(const QString &fileName, FileFormat *fileFormat)
QMessageBox::critical(this, tr("Error Loading World"), errorString);
return false;
} else {
mLoadedWorlds = worldManager.worldFileNames();

Document *document = mDocumentManager->currentDocument();
if (document && document->type() == Document::MapDocumentType)
if (worldManager.worldForMap(document->fileName()) == worldDocument)
Expand Down Expand Up @@ -1316,6 +1294,65 @@ bool MainWindow::confirmSaveWorld(WorldDocument *worldDocument)
}
}

/**
* Asks for a file name and creates a new empty world. When \a
* suggestedFileName is empty, the last used world path is suggested.
*
* @return the created world, or null when the user canceled or the world
* could not be created
*/
WorldDocument *MainWindow::createNewWorld(const QString &suggestedFileName)
{
Session &session = Session::current();
const QString startingLocation = suggestedFileName.isEmpty()
? session.lastPath(Session::WorldFile)
: suggestedFileName;

QString filter = tr("All Files (*)");
filter.append(QStringLiteral(";;"));
QString worldFilesFilter = tr("World files (*.world)");
filter.append(worldFilesFilter);

QFileDialog dialog(this, tr("New World"), startingLocation, filter);
dialog.setAcceptMode(QFileDialog::AcceptSave);
dialog.selectNameFilter(worldFilesFilter);
dialog.setDefaultSuffix(QStringLiteral("world"));

QString worldFile;
if (dialog.exec() == QDialog::Accepted)
worldFile = dialog.selectedFiles().value(0);

if (worldFile.isEmpty())
return nullptr;

session.setLastPath(Session::WorldFile, QFileInfo(worldFile).path());

// When the selected world is already loaded, use it rather than
// reporting an error
if (auto worldDocument = WorldManager::instance().findWorld(worldFile)) {
if (!worldDocument->world()->canBeModified()) {
QMessageBox::critical(this, tr("Error Creating World"),
tr("World \"%1\" is already loaded and cannot be modified.")
.arg(worldDocument->displayName()));
return nullptr;
}

QMessageBox::information(this, tr("New World"),
tr("Using already loaded world \"%1\".")
.arg(worldDocument->displayName()));
return worldDocument.data();
}

QString errorString;
auto worldDocument = WorldManager::instance().addEmptyWorld(worldFile, &errorString);
if (!worldDocument) {
QMessageBox::critical(this, tr("Error Creating World"), errorString);
return nullptr;
}

return worldDocument.data();
}

void MainWindow::export_()
{
if (!exportDocument(mDocument)) {
Expand Down
2 changes: 2 additions & 0 deletions src/tiled/mainwindow.h
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@ class TILED_EDITOR_EXPORT MainWindow : public QMainWindow
*/
bool openFile(const QString &fileName, FileFormat *fileFormat = nullptr);

WorldDocument *createNewWorld(const QString &suggestedFileName = QString());

bool addRecentProjectsActions(QMenu *menu) const;

static MainWindow *instance();
Expand Down
23 changes: 13 additions & 10 deletions src/tiled/worldmanager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ void WorldManager::deleteInstance()
WorldDocumentPtr WorldManager::findWorld(const QString &fileName) const
{
const auto canonicalFilePath = QFileInfo(fileName).canonicalFilePath();
if (canonicalFilePath.isEmpty()) // file doesn't exist
return {};
for (auto &worldDocument : mWorldDocuments)
if (worldDocument->canonicalFilePath() == canonicalFilePath)
return worldDocument;
Expand All @@ -68,20 +70,21 @@ WorldDocumentPtr WorldManager::addEmptyWorld(const QString &fileName, QString *e

auto world = std::make_unique<World>();
world->fileName = fileName;
auto worldDocument = WorldDocumentPtr::create(std::move(world));

if (worldDocument->save(worldDocument->fileName(), errorString)) {
mWorldDocuments.append(worldDocument);
// Save before creating the document, so that the canonical file path is
// available when the document is constructed
if (!World::save(*world, errorString))
return {};

connect(worldDocument.data(), &WorldDocument::worldChanged,
this, [this] { emit worldsChanged(); });
auto worldDocument = WorldDocumentPtr::create(std::move(world));
mWorldDocuments.append(worldDocument);

emit worldLoaded(worldDocument.data());
emit worldsChanged();
return worldDocument;
}
connect(worldDocument.data(), &WorldDocument::worldChanged,
this, [this] { emit worldsChanged(); });

return {};
emit worldLoaded(worldDocument.data());
emit worldsChanged();
return worldDocument;
}

/**
Expand Down
Loading
Loading