diff --git a/NEWS.md b/NEWS.md index 28104d3e98..0f1c08079c 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,6 @@ ### Unreleased +* Added support for setting custom properties on the project (#2903) * Removed Space and Ctrl+Space shortcuts from Layers view to avoid conflict with panning (#3672) * Scripting: Added API for editing tile layers using terrain sets (with a-morphous, #3758) * Fixed object preview position with parallax factor on group layer (#3669) diff --git a/docs/scripting-doc/index.d.ts b/docs/scripting-doc/index.d.ts index a5f4253a25..ddacea6caf 100644 --- a/docs/scripting-doc/index.d.ts +++ b/docs/scripting-doc/index.d.ts @@ -1091,7 +1091,7 @@ declare class TiledObject { * * @since 1.10.1 */ -declare class Project { +declare class Project extends TiledObject { /** * A project-specific directory where you can put Tiled extensions. * diff --git a/src/libtiled/logginginterface.cpp b/src/libtiled/logginginterface.cpp index 0bb8d9fb4c..b63674112d 100644 --- a/src/libtiled/logginginterface.cpp +++ b/src/libtiled/logginginterface.cpp @@ -187,6 +187,8 @@ SelectCustomProperty::SelectCustomProperty(QString fileName, // not so helpful... would need WangSet index as well id = static_cast(object)->colorIndex(); break; + case Object::ProjectType: + break; } } diff --git a/src/libtiled/object.h b/src/libtiled/object.h index 7a37dbb148..b346e16466 100644 --- a/src/libtiled/object.h +++ b/src/libtiled/object.h @@ -41,13 +41,14 @@ class TILEDSHARED_EXPORT Object public: // Keep values synchronized with ClassPropertyType::ClassUsageFlag enum TypeId { - LayerType = 0x02, - MapObjectType = 0x04, - MapType = 0x08, - TilesetType = 0x10, - TileType = 0x20, - WangSetType = 0x40, - WangColorType = 0x80, + LayerType = 0x002, + MapObjectType = 0x004, + MapType = 0x008, + TilesetType = 0x010, + TileType = 0x020, + WangSetType = 0x040, + WangColorType = 0x080, + ProjectType = 0x100, }; explicit Object(TypeId typeId, const QString &className = QString()) diff --git a/src/libtiled/propertytype.cpp b/src/libtiled/propertytype.cpp index 3f219d33cb..5200877d16 100644 --- a/src/libtiled/propertytype.cpp +++ b/src/libtiled/propertytype.cpp @@ -308,6 +308,7 @@ static const struct { { ClassPropertyType::TilesetClass, QLatin1String("tileset") }, { ClassPropertyType::WangColorClass, QLatin1String("wangcolor") }, { ClassPropertyType::WangSetClass, QLatin1String("wangset") }, + { ClassPropertyType::ProjectClass, QLatin1String("project") }, }; QJsonObject ClassPropertyType::toJson(const ExportContext &context) const diff --git a/src/libtiled/propertytype.h b/src/libtiled/propertytype.h index 1959a071d5..99969713fa 100644 --- a/src/libtiled/propertytype.h +++ b/src/libtiled/propertytype.h @@ -135,18 +135,18 @@ class TILEDSHARED_EXPORT ClassPropertyType final : public PropertyType { public: enum ClassUsageFlag { - PropertyValueType = 0x01, + PropertyValueType = 0x001, // Keep values synchronized with Object::TypeId - LayerClass = 0x02, - MapObjectClass = 0x04, - MapClass = 0x08, - TilesetClass = 0x10, - TileClass = 0x20, - WangSetClass = 0x40, - WangColorClass = 0x80, - - AnyUsage = 0xFF, + LayerClass = 0x002, + MapObjectClass = 0x004, + MapClass = 0x008, + TilesetClass = 0x010, + TileClass = 0x020, + WangSetClass = 0x040, + WangColorClass = 0x080, + ProjectClass = 0x100, + AnyUsage = 0xFFF, AnyObjectClass = AnyUsage & ~PropertyValueType, }; diff --git a/src/tiled/automappingmanager.cpp b/src/tiled/automappingmanager.cpp index f36078f838..b84070d0f4 100644 --- a/src/tiled/automappingmanager.cpp +++ b/src/tiled/automappingmanager.cpp @@ -325,7 +325,7 @@ void AutomappingManager::refreshRulesFile(const QString &ruleFileOverride) } if (rulesFile.isEmpty() || !QFileInfo::exists(rulesFile)) { - auto &project = ProjectManager::instance()->project(); + const auto &project = ProjectManager::instance()->project(); if (!project.mAutomappingRulesFile.isEmpty()) rulesFile = project.mAutomappingRulesFile; } diff --git a/src/tiled/commanddialog.cpp b/src/tiled/commanddialog.cpp index 9b94f829a5..1e2d56e4a7 100644 --- a/src/tiled/commanddialog.cpp +++ b/src/tiled/commanddialog.cpp @@ -51,7 +51,7 @@ CommandDialog::CommandDialog(QWidget *parent) mUi->tabWidget->addTab(mGlobalCommandsEdit, tr("Global Commands")); mUi->tabWidget->addTab(mProjectCommandsEdit, tr("Project Commands")); - auto &project = ProjectManager::instance()->project(); + const auto &project = ProjectManager::instance()->project(); mUi->tabWidget->setTabEnabled(1, !project.fileName().isEmpty()); Utils::restoreGeometry(this); diff --git a/src/tiled/commandmanager.cpp b/src/tiled/commandmanager.cpp index 9f43bd8e14..3625a171e0 100644 --- a/src/tiled/commandmanager.cpp +++ b/src/tiled/commandmanager.cpp @@ -109,7 +109,7 @@ const QVector &CommandManager::globalCommands() const const QVector &CommandManager::projectCommands() const { - auto &project = ProjectManager::instance()->project(); + const auto &project = ProjectManager::instance()->project(); return project.mCommands; } diff --git a/src/tiled/document.h b/src/tiled/document.h index 7e7b20fa97..5adfd9930c 100644 --- a/src/tiled/document.h +++ b/src/tiled/document.h @@ -56,7 +56,8 @@ class Document : public QObject, enum DocumentType { MapDocumentType, TilesetDocumentType, - WorldDocumentType + WorldDocumentType, + ProjectDocumentType }; Document(DocumentType type, diff --git a/src/tiled/documentmanager.cpp b/src/tiled/documentmanager.cpp index c6c62edd16..f8f30f9af3 100644 --- a/src/tiled/documentmanager.cpp +++ b/src/tiled/documentmanager.cpp @@ -243,6 +243,7 @@ DocumentManager::DocumentManager(QObject *parent) break; } case Document::WorldDocumentType: + case Document::ProjectDocumentType: break; } diff --git a/src/tiled/editableobject.cpp b/src/tiled/editableobject.cpp index edd1c9f5ea..b9b5fc5795 100644 --- a/src/tiled/editableobject.cpp +++ b/src/tiled/editableobject.cpp @@ -112,6 +112,7 @@ static Map *mapForObject(Object *object) case Object::TileType: case Object::WangSetType: case Object::WangColorType: + case Object::ProjectType: break; } return nullptr; diff --git a/src/tiled/editableproject.cpp b/src/tiled/editableproject.cpp index 832568ce35..77033d5d8a 100644 --- a/src/tiled/editableproject.cpp +++ b/src/tiled/editableproject.cpp @@ -21,32 +21,45 @@ #include "editableproject.h" +#include "projectdocument.h" + namespace Tiled { -EditableProject::EditableProject(Project *project, QObject *parent) - : QObject(parent) - , mProject(project) +EditableProject::EditableProject(ProjectDocument *projectDocument, QObject *parent) + : EditableAsset(projectDocument, &projectDocument->project(), parent) { } QString EditableProject::extensionsPath() const { - return mProject->mExtensionsPath; + return project()->mExtensionsPath; } QString EditableProject::automappingRulesFile() const { - return mProject->mAutomappingRulesFile; + return project()->mAutomappingRulesFile; } QString EditableProject::fileName() const { - return mProject->fileName(); + return project()->fileName(); } QStringList EditableProject::folders() const { - return mProject->folders(); + return project()->folders(); +} + +bool EditableProject::isReadOnly() const +{ + return false; +} + +QSharedPointer EditableProject::createDocument() +{ + // We don't currently support opening a project in a tab, which this + // function is meant for. + return nullptr; } } // namespace Tiled diff --git a/src/tiled/editableproject.h b/src/tiled/editableproject.h index 781e88a938..2006370ed8 100644 --- a/src/tiled/editableproject.h +++ b/src/tiled/editableproject.h @@ -21,13 +21,16 @@ #pragma once +#include "editableasset.h" #include "project.h" #include namespace Tiled { -class EditableProject : public QObject +class ProjectDocument; + +class EditableProject final : public EditableAsset { Q_OBJECT @@ -37,17 +40,24 @@ class EditableProject : public QObject Q_PROPERTY(QStringList folders READ folders) public: - EditableProject(Project *project, QObject *parent = nullptr); + EditableProject(ProjectDocument *projectDocument, QObject *parent = nullptr); + bool isReadOnly() const override; QString extensionsPath() const; QString automappingRulesFile() const; QString fileName() const; QStringList folders() const; -private: - Project *mProject; + Project *project() const; + + QSharedPointer createDocument() override; }; +inline Project *EditableProject::project() const +{ + return static_cast(object()); +} + } // namespace Tiled Q_DECLARE_METATYPE(Tiled::EditableProject*) diff --git a/src/tiled/exporthelper.cpp b/src/tiled/exporthelper.cpp index 1c1a153e29..69597c60ba 100644 --- a/src/tiled/exporthelper.cpp +++ b/src/tiled/exporthelper.cpp @@ -206,6 +206,7 @@ void ExportHelper::resolveProperties(Object *object) const resolveProperties(color.data()); break; case Object::WangColorType: + case Object::ProjectType: break; } diff --git a/src/tiled/libtilededitor.qbs b/src/tiled/libtilededitor.qbs index f6021fa756..91e3149177 100644 --- a/src/tiled/libtilededitor.qbs +++ b/src/tiled/libtilededitor.qbs @@ -390,6 +390,8 @@ DynamicLibrary { "project.h", "projectdock.cpp", "projectdock.h", + "projectdocument.cpp", + "projectdocument.h", "projectmanager.cpp", "projectmanager.h", "projectmodel.cpp", @@ -399,6 +401,8 @@ DynamicLibrary { "projectpropertiesdialog.ui", "propertiesdock.cpp", "propertiesdock.h", + "propertieswidget.cpp", + "propertieswidget.h", "propertybrowser.cpp", "propertybrowser.h", "propertytypeseditor.cpp", diff --git a/src/tiled/mainwindow.cpp b/src/tiled/mainwindow.cpp index 22d3b2bd19..5bd46c38b1 100644 --- a/src/tiled/mainwindow.cpp +++ b/src/tiled/mainwindow.cpp @@ -56,6 +56,7 @@ #include "newtilesetdialog.h" #include "offsetmapdialog.h" #include "projectdock.h" +#include "projectdocument.h" #include "projectmanager.h" #include "projectpropertiesdialog.h" #include "propertytypeseditor.h" @@ -991,8 +992,11 @@ void MainWindow::initializeSession() const auto &session = Session::current(); // Restore associated project if applicable - Project project; - bool projectLoaded = !session.project.isEmpty() && project.load(session.project); + std::unique_ptr project; + if (!session.project.isEmpty()) + project = Project::load(session.project); + + const bool projectLoaded = project != nullptr; if (projectLoaded) { ProjectManager::instance()->setProject(std::move(project)); @@ -1379,9 +1383,9 @@ bool MainWindow::closeAllFiles() bool MainWindow::openProjectFile(const QString &fileName) { - Project project; + auto project = Project::load(fileName); - if (!project.load(fileName)) { + if (!project) { QMessageBox::critical(window(), tr("Error Opening Project"), tr("An error occurred while opening the project.")); @@ -1413,10 +1417,10 @@ void MainWindow::newProject() fileName.append(QStringLiteral(".tiled-project")); } - Project project; - project.addFolder(QFileInfo(fileName).path()); + auto project = std::make_unique(); + project->addFolder(QFileInfo(fileName).path()); - if (!project.save(fileName)) { + if (!project->save(fileName)) { QMessageBox::critical(window(), tr("Error Saving Project"), tr("An error occurred while saving the project.")); @@ -1435,10 +1439,10 @@ bool MainWindow::closeProject() if (project.fileName().isEmpty()) return true; - return switchProject(Project{}); + return switchProject(nullptr); } -bool MainWindow::switchProject(Project project) +bool MainWindow::switchProject(std::unique_ptr project) { auto prefs = Preferences::instance(); emit prefs->aboutToSwitchSession(); @@ -1448,11 +1452,15 @@ bool MainWindow::switchProject(Project project) WorldManager::instance().unloadAllWorlds(); - auto &session = Session::switchCurrent(Session::defaultFileNameForProject(project.fileName())); + if (project) { + auto &session = Session::switchCurrent(Session::defaultFileNameForProject(project->fileName())); - if (!project.fileName().isEmpty()) { - session.setProject(project.fileName()); - prefs->addRecentProject(project.fileName()); + if (!project->fileName().isEmpty()) { + session.setProject(project->fileName()); + prefs->addRecentProject(project->fileName()); + } + } else { + Session::switchCurrent(Session::defaultFileName()); } ProjectManager::instance()->setProject(std::move(project)); @@ -1487,6 +1495,8 @@ void MainWindow::restoreSession() void MainWindow::projectProperties() { Project &project = ProjectManager::instance()->project(); + if (project.fileName().length() == 0) + return; if (ProjectPropertiesDialog(project, this).exec() == QDialog::Accepted) { project.save(); diff --git a/src/tiled/mainwindow.h b/src/tiled/mainwindow.h index 36c5c105f2..cfea95ee81 100644 --- a/src/tiled/mainwindow.h +++ b/src/tiled/mainwindow.h @@ -131,7 +131,7 @@ class TILED_EDITOR_EXPORT MainWindow : public QMainWindow bool openProjectFile(const QString &fileName); void newProject(); bool closeProject(); - bool switchProject(Project project); + bool switchProject(std::unique_ptr project); void restoreSession(); void projectProperties(); diff --git a/src/tiled/project.cpp b/src/tiled/project.cpp index 45ae98ffad..82907ab83b 100644 --- a/src/tiled/project.cpp +++ b/src/tiled/project.cpp @@ -19,7 +19,6 @@ */ #include "project.h" -#include "preferences.h" #include "properties.h" #include "savefile.h" @@ -45,7 +44,8 @@ static QString absolute(const QDir &dir, const QString &fileName) } Project::Project() - : mPropertyTypes(SharedPropertyTypes::create()) + : Object(Object::ProjectType) + , mPropertyTypes(SharedPropertyTypes::create()) { } @@ -75,13 +75,15 @@ bool Project::save(const QString &fileName) commands.append(QJsonObject::fromVariantHash(command.toVariant())); const QJsonArray propertyTypes = mPropertyTypes->toJson(dir.path()); - + const ExportContext context(*mPropertyTypes, dir.path()); + const QJsonArray projectProperties = propertiesToJson(properties(), context); QJsonObject project { { QStringLiteral("propertyTypes"), propertyTypes }, { QStringLiteral("folders"), folders }, { QStringLiteral("extensionsPath"), relative(dir, extensionsPath) }, { QStringLiteral("automappingRulesFile"), dir.relativeFilePath(mAutomappingRulesFile) }, { QStringLiteral("commands"), commands }, + { QStringLiteral("properties"), projectProperties }, }; if (mCompatibilityVersion != Tiled_Latest) @@ -103,42 +105,48 @@ bool Project::save(const QString &fileName) return true; } -bool Project::load(const QString &fileName) +std::unique_ptr Project::load(const QString &fileName) { QFile file(fileName); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) - return false; + return nullptr; QJsonParseError error; const QByteArray json = file.readAll(); const QJsonDocument document(QJsonDocument::fromJson(json, &error)); if (error.error != QJsonParseError::NoError) - return false; + return nullptr; - mFileName = fileName; + auto project = std::make_unique(); + project->mFileName = fileName; const QDir dir = QFileInfo(fileName).dir(); - const QJsonObject project = document.object(); + const QJsonObject projectJson = document.object(); + + project->mExtensionsPath = absolute(dir, projectJson.value(QLatin1String("extensionsPath")).toString(QStringLiteral("extensions"))); + project->mObjectTypesFile = absolute(dir, projectJson.value(QLatin1String("objectTypesFile")).toString()); + project->mAutomappingRulesFile = absolute(dir, projectJson.value(QLatin1String("automappingRulesFile")).toString()); - mExtensionsPath = absolute(dir, project.value(QLatin1String("extensionsPath")).toString(QStringLiteral("extensions"))); - mObjectTypesFile = absolute(dir, project.value(QLatin1String("objectTypesFile")).toString()); - mAutomappingRulesFile = absolute(dir, project.value(QLatin1String("automappingRulesFile")).toString()); + project->mPropertyTypes->loadFromJson(projectJson.value(QLatin1String("propertyTypes")).toArray(), dir.path()); - mPropertyTypes->loadFromJson(project.value(QLatin1String("propertyTypes")).toArray(), dir.path()); + const QString projectPropertiesKey = QLatin1String("properties"); + if (projectJson.contains(projectPropertiesKey)) { + const ExportContext context(*project->mPropertyTypes, dir.path()); + const Properties loadedProperties = propertiesFromJson(projectJson.value(projectPropertiesKey).toArray(), context); + project->setProperties(loadedProperties); + } - mFolders.clear(); - const QJsonArray folders = project.value(QLatin1String("folders")).toArray(); + const QJsonArray folders = projectJson.value(QLatin1String("folders")).toArray(); for (const QJsonValue &folderValue : folders) - mFolders.append(QDir::cleanPath(dir.absoluteFilePath(folderValue.toString()))); + project->mFolders.append(QDir::cleanPath(dir.absoluteFilePath(folderValue.toString()))); - mCommands.clear(); - const QJsonArray commands = project.value(QLatin1String("commands")).toArray(); + const QJsonArray commands = projectJson.value(QLatin1String("commands")).toArray(); for (const QJsonValue &commandValue : commands) - mCommands.append(Command::fromVariant(commandValue.toVariant())); + project->mCommands.append(Command::fromVariant(commandValue.toVariant())); - mCompatibilityVersion = static_cast(project.value(QLatin1String("compatibilityVersion")).toInt(Tiled_Latest)); + project->mCompatibilityVersion = static_cast(projectJson.value(QLatin1String("compatibilityVersion")).toInt(Tiled_Latest)); - return true; + return project; } void Project::addFolder(const QString &folder) diff --git a/src/tiled/project.h b/src/tiled/project.h index 0756c1dc87..172e52e301 100644 --- a/src/tiled/project.h +++ b/src/tiled/project.h @@ -21,17 +21,18 @@ #pragma once #include "command.h" -#include "properties.h" -#include "propertytype.h" +#include "object.h" #include "tiled.h" #include #include #include +#include + namespace Tiled { -class Project +class Project : public Object { public: Project(); @@ -39,7 +40,8 @@ class Project const QString &fileName() const; bool save(); bool save(const QString &fileName); - bool load(const QString &fileName); + + static std::unique_ptr load(const QString &fileName); void addFolder(const QString &folder); void removeFolder(int index); diff --git a/src/tiled/projectdocument.cpp b/src/tiled/projectdocument.cpp new file mode 100644 index 0000000000..26849d85f4 --- /dev/null +++ b/src/tiled/projectdocument.cpp @@ -0,0 +1,82 @@ +/* + * projectdocument.cpp + * Copyright 2023, Chris Boehm AKA dogboydog + * Copyright 2023, Thorbjørn Lindeijer + * + * This file is part of Tiled. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ + +#include "projectdocument.h" + +#include "editableproject.h" + +#include + +namespace Tiled { + +ProjectDocument::ProjectDocument(std::unique_ptr project, QObject *parent) + : Document(ProjectDocumentType, project->fileName(), parent) +{ + mProject = std::move(project); + mCurrentObject = mProject.get(); + + connect(undoStack(), &QUndoStack::indexChanged, + this, [this] { mProject->save(); }); +} + +QString ProjectDocument::displayName() const +{ + return mProject->fileName(); +} + +bool ProjectDocument::save(const QString &/* fileName */, QString */* error */) +{ + return mProject->save(); +} + +FileFormat *ProjectDocument::writerFormat() const +{ + return nullptr; +} + +void ProjectDocument::setExportFormat(FileFormat *) +{ + // do nothing +} + +FileFormat *ProjectDocument::exportFormat() const +{ + return nullptr; +} + +QString ProjectDocument::lastExportFileName() const +{ + return mProject->fileName(); +} + +void ProjectDocument::setLastExportFileName(const QString &/* fileName */) +{ + // do nothing +} + +std::unique_ptr ProjectDocument::createEditable() +{ + return std::make_unique(this, this); +} + +} // namespace Tiled + +#include "moc_projectdocument.cpp" diff --git a/src/tiled/projectdocument.h b/src/tiled/projectdocument.h new file mode 100644 index 0000000000..310bd83c22 --- /dev/null +++ b/src/tiled/projectdocument.h @@ -0,0 +1,53 @@ +/* + * projectdocument.h + * Copyright 2023, Chris Boehm AKA dogboydog + * Copyright 2023, Thorbjørn Lindeijer + * + * This file is part of Tiled. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ + +#pragma once + +#include "document.h" +#include "project.h" + +namespace Tiled { + +class ProjectDocument : public Document +{ + Q_OBJECT + +public: + ProjectDocument(std::unique_ptr project, QObject *parent = nullptr); + + QString displayName() const override; + FileFormat *writerFormat() const override; + bool save(const QString &fileName, QString *error) override; + void setExportFormat(FileFormat *format) override; + FileFormat *exportFormat() const override; + QString lastExportFileName() const override; + void setLastExportFileName(const QString &fileName) override; + std::unique_ptr createEditable() override; + + Project &project() { return *mProject; } + +private: + std::unique_ptr mProject; +}; + +using ProjectDocumentPtr = QSharedPointer; + +} // namespace Tiled diff --git a/src/tiled/projectmanager.cpp b/src/tiled/projectmanager.cpp index 14a4e111d5..d7ff950be0 100644 --- a/src/tiled/projectmanager.cpp +++ b/src/tiled/projectmanager.cpp @@ -40,11 +40,11 @@ ProjectManager::ProjectManager(QObject *parent) /** * Replaces the current project with the given \a project. */ -void ProjectManager::setProject(Project _project) +void ProjectManager::setProject(std::unique_ptr _project) { mProjectModel->setProject(std::move(_project)); - auto &project = mProjectModel->project(); // _project was moved + auto &project = mProjectModel->project(); // Automatically import object types if they are referenced by the project if (!project.mObjectTypesFile.isEmpty()) { @@ -71,6 +71,11 @@ Project &ProjectManager::project() return mProjectModel->project(); } +EditableAsset *ProjectManager::editableProject() +{ + return mProjectModel->editableProject(); +} + } // namespace Tiled #include "moc_projectmanager.cpp" diff --git a/src/tiled/projectmanager.h b/src/tiled/projectmanager.h index 5704b86bf1..50af4707ad 100644 --- a/src/tiled/projectmanager.h +++ b/src/tiled/projectmanager.h @@ -26,6 +26,7 @@ namespace Tiled { +class EditableAsset; class ProjectModel; /** @@ -42,8 +43,9 @@ class ProjectManager : public QObject static ProjectManager *instance(); - void setProject(Project project); + void setProject(std::unique_ptr project); Project &project(); + EditableAsset *editableProject(); ProjectModel *projectModel(); diff --git a/src/tiled/projectmodel.cpp b/src/tiled/projectmodel.cpp index e9b63fb304..ae74058bbc 100644 --- a/src/tiled/projectmodel.cpp +++ b/src/tiled/projectmodel.cpp @@ -141,37 +141,57 @@ ProjectModel::~ProjectModel() mScanningThread.wait(); } -void ProjectModel::setProject(Project project) +void ProjectModel::setProject(std::unique_ptr project) { if (mUpdateNameFiltersTimer.isActive()) updateNameFilters(); beginResetModel(); - mProject = std::move(project); + if (project) + mProjectDocument = std::make_unique(std::move(project)); + else + mProjectDocument.reset(); + mFolders.clear(); mFoldersPendingScan.clear(); - for (const QString &folder : mProject.folders()) { + const auto &folders = this->project().folders(); + for (const QString &folder : folders) { mFolders.push_back(std::make_unique(folder)); scheduleFolderScan(folder); } mWatcher.clear(); - mWatcher.addPaths(mProject.folders()); + mWatcher.addPaths(folders); endResetModel(); } +Project &ProjectModel::project() +{ + return mProjectDocument ? mProjectDocument->project() : mEmptyProject; +} + +EditableAsset *ProjectModel::editableProject() +{ + return mProjectDocument ? mProjectDocument->editable() : nullptr; +} + void ProjectModel::addFolder(const QString &folder) { - const int row = int(mProject.folders().size()); + if (!mProjectDocument) + return; + + const int row = int(project().folders().size()); beginInsertRows(QModelIndex(), row, row); - mProject.addFolder(folder); + project().addFolder(folder); + mFolders.push_back(std::make_unique(folder)); mWatcher.addPath(folder); + scheduleFolderScan(folder); endInsertRows(); @@ -181,6 +201,9 @@ void ProjectModel::addFolder(const QString &folder) void ProjectModel::removeFolder(int row) { + if (!mProjectDocument) + return; + const QString folder = mFolders.at(row)->filePath; QStringList watchedFilePaths; @@ -188,9 +211,12 @@ void ProjectModel::removeFolder(int row) collectDirectories(*mFolders.at(row), watchedFilePaths); beginRemoveRows(QModelIndex(), row, row); - mProject.removeFolder(row); + + project().removeFolder(row); + mFolders.erase(mFolders.begin() + row); mWatcher.removePaths(watchedFilePaths); + endRemoveRows(); emit folderRemoved(folder); diff --git a/src/tiled/projectmodel.h b/src/tiled/projectmodel.h index 94396cf4e2..dce87d465d 100644 --- a/src/tiled/projectmodel.h +++ b/src/tiled/projectmodel.h @@ -21,7 +21,7 @@ #pragma once #include "filesystemwatcher.h" -#include "project.h" +#include "projectdocument.h" #include #include @@ -56,8 +56,9 @@ class ProjectModel : public QAbstractItemModel void updateNameFilters(); - void setProject(Project project); + void setProject(std::unique_ptr project); Project &project(); + EditableAsset *editableProject(); void addFolder(const QString &folder); void removeFolder(int row); @@ -114,7 +115,8 @@ class ProjectModel : public QAbstractItemModel void scheduleFolderScan(const QString &folder); void folderScanned(FolderEntry *entry); - Project mProject; + std::unique_ptr mProjectDocument; + Project mEmptyProject; QFileIconProvider mFileIconProvider; QStringList mNameFilters; QTimer mUpdateNameFiltersTimer; @@ -127,10 +129,4 @@ class ProjectModel : public QAbstractItemModel FileSystemWatcher mWatcher; }; - -inline Project &ProjectModel::project() -{ - return mProject; -} - } // namespace Tiled diff --git a/src/tiled/projectpropertiesdialog.cpp b/src/tiled/projectpropertiesdialog.cpp index f3f3f94e96..304abb8d85 100644 --- a/src/tiled/projectpropertiesdialog.cpp +++ b/src/tiled/projectpropertiesdialog.cpp @@ -23,7 +23,7 @@ #include "mapformat.h" #include "project.h" -#include "utils.h" +#include "projectdocument.h" #include "varianteditorfactory.h" #include "variantpropertymanager.h" @@ -35,9 +35,12 @@ ProjectPropertiesDialog::ProjectPropertiesDialog(Project &project, QWidget *pare : QDialog(parent) , ui(new Ui::ProjectPropertiesDialog) , mProject(project) + , mPropertiesProjectDocument(new ProjectDocument(std::make_unique(), this)) { ui->setupUi(this); + mPropertiesProjectDocument->project().setProperties(project.properties()); + auto variantPropertyManager = new VariantPropertyManager(this); auto variantEditorFactory = new VariantEditorFactory(this); auto groupPropertyManager = new QtGroupPropertyManager(this); @@ -79,6 +82,8 @@ ProjectPropertiesDialog::ProjectPropertiesDialog(Project &project, QWidget *pare ui->propertyBrowser->addProperty(generalGroupProperty); ui->propertyBrowser->addProperty(filesGroupProperty); + + ui->propertiesWidget->setDocument(mPropertiesProjectDocument); } ProjectPropertiesDialog::~ProjectPropertiesDialog() @@ -88,6 +93,7 @@ ProjectPropertiesDialog::~ProjectPropertiesDialog() void ProjectPropertiesDialog::accept() { + mProject.setProperties(mPropertiesProjectDocument->project().properties()); mProject.mCompatibilityVersion = mVersions.at(mCompatibilityVersionProperty->value().toInt()); mProject.mExtensionsPath = mExtensionPathProperty->value().toString(); mProject.mAutomappingRulesFile = mAutomappingRulesFileProperty->value().toString(); diff --git a/src/tiled/projectpropertiesdialog.h b/src/tiled/projectpropertiesdialog.h index 3c033c8f18..dbfd4e59d9 100644 --- a/src/tiled/projectpropertiesdialog.h +++ b/src/tiled/projectpropertiesdialog.h @@ -33,6 +33,7 @@ class ProjectPropertiesDialog; namespace Tiled { class Project; +class ProjectDocument; class ProjectPropertiesDialog : public QDialog { @@ -48,6 +49,7 @@ class ProjectPropertiesDialog : public QDialog Ui::ProjectPropertiesDialog *ui; Project &mProject; + ProjectDocument *mPropertiesProjectDocument; QList mVersions; QtVariantProperty *mCompatibilityVersionProperty; QtVariantProperty *mExtensionPathProperty; diff --git a/src/tiled/projectpropertiesdialog.ui b/src/tiled/projectpropertiesdialog.ui index 5194942dc4..899db3ac9c 100644 --- a/src/tiled/projectpropertiesdialog.ui +++ b/src/tiled/projectpropertiesdialog.ui @@ -6,8 +6,8 @@ 0 0 - 575 - 168 + 586 + 356 @@ -17,6 +17,21 @@ + + + + + 0 + 1 + + + + + + + + + @@ -36,6 +51,11 @@
QtGroupBoxPropertyBrowser
1 + + Tiled::PropertiesWidget + QTreeView +
propertieswidget.h
+
diff --git a/src/tiled/propertiesdock.cpp b/src/tiled/propertiesdock.cpp index 488312f1cb..36cde21f64 100644 --- a/src/tiled/propertiesdock.cpp +++ b/src/tiled/propertiesdock.cpp @@ -20,481 +20,39 @@ #include "propertiesdock.h" -#include "actionmanager.h" -#include "addpropertydialog.h" -#include "changeproperties.h" -#include "clipboardmanager.h" -#include "documentmanager.h" -#include "mapdocument.h" -#include "mapobject.h" -#include "propertybrowser.h" -#include "tile.h" -#include "tileset.h" -#include "utils.h" -#include "variantpropertymanager.h" +#include "propertieswidget.h" -#include #include -#include -#include -#include -#include -#include -#include -#include namespace Tiled { PropertiesDock::PropertiesDock(QWidget *parent) : QDockWidget(parent) - , mDocument(nullptr) - , mPropertyBrowser(new PropertyBrowser) + , mPropertiesWidget(new PropertiesWidget(this)) { setObjectName(QLatin1String("propertiesDock")); + setWidget(mPropertiesWidget); - mActionAddProperty = new QAction(this); - mActionAddProperty->setEnabled(false); - mActionAddProperty->setIcon(QIcon(QLatin1String(":/images/16/add.png"))); - connect(mActionAddProperty, &QAction::triggered, - this, &PropertiesDock::openAddPropertyDialog); - - mActionRemoveProperty = new QAction(this); - mActionRemoveProperty->setEnabled(false); - mActionRemoveProperty->setIcon(QIcon(QLatin1String(":/images/16/remove.png"))); - mActionRemoveProperty->setShortcuts(QKeySequence::Delete); - connect(mActionRemoveProperty, &QAction::triggered, - this, &PropertiesDock::removeProperties); - - mActionRenameProperty = new QAction(this); - mActionRenameProperty->setEnabled(false); - mActionRenameProperty->setIcon(QIcon(QLatin1String(":/images/16/rename.png"))); - connect(mActionRenameProperty, &QAction::triggered, - this, &PropertiesDock::renameProperty); - - Utils::setThemeIcon(mActionAddProperty, "add"); - Utils::setThemeIcon(mActionRemoveProperty, "remove"); - Utils::setThemeIcon(mActionRenameProperty, "rename"); - - QToolBar *toolBar = new QToolBar; - toolBar->setFloatable(false); - toolBar->setMovable(false); - toolBar->setIconSize(Utils::smallIconSize()); - toolBar->addAction(mActionAddProperty); - toolBar->addAction(mActionRemoveProperty); - toolBar->addAction(mActionRenameProperty); - - QWidget *widget = new QWidget(this); - QVBoxLayout *layout = new QVBoxLayout(widget); - layout->setContentsMargins(0, 0, 0, 0); - layout->setSpacing(0); - layout->addWidget(mPropertyBrowser); - layout->addWidget(toolBar); - widget->setLayout(layout); - - setWidget(widget); - - mPropertyBrowser->setContextMenuPolicy(Qt::CustomContextMenu); - connect(mPropertyBrowser, &PropertyBrowser::customContextMenuRequested, - this, &PropertiesDock::showContextMenu); - connect(mPropertyBrowser, &PropertyBrowser::selectedItemsChanged, - this, &PropertiesDock::updateActions); + connect(mPropertiesWidget, &PropertiesWidget::bringToFront, + this, &PropertiesDock::bringToFront); retranslateUi(); } void PropertiesDock::setDocument(Document *document) { - if (mDocument == document) - return; - - if (mDocument) - mDocument->disconnect(this); - - mDocument = document; - mPropertyBrowser->setDocument(document); - - if (document) { - connect(document, &Document::currentObjectChanged, - this, &PropertiesDock::currentObjectChanged); - connect(document, &Document::editCurrentObject, - this, &PropertiesDock::bringToFront); - - connect(document, &Document::propertyAdded, - this, &PropertiesDock::updateActions); - connect(document, &Document::propertyRemoved, - this, &PropertiesDock::updateActions); - - currentObjectChanged(document->currentObject()); - } else { - currentObjectChanged(nullptr); - } -} - -void PropertiesDock::bringToFront() -{ - show(); - raise(); - mPropertyBrowser->setFocus(); + mPropertiesWidget->setDocument(document); } void PropertiesDock::selectCustomProperty(const QString &name) { bringToFront(); - mPropertyBrowser->selectCustomProperty(name); -} - -static bool anyObjectHasProperty(const QList &objects, const QString &name) -{ - for (Object *obj : objects) { - if (obj->hasProperty(name)) - return true; - } - return false; -} - -void PropertiesDock::currentObjectChanged(Object *object) -{ - mPropertyBrowser->setObject(object); - - bool editingTileset = mDocument && mDocument->type() == Document::TilesetDocumentType; - bool isTileset = object && object->isPartOfTileset(); - bool enabled = object && (!isTileset || editingTileset); - - mPropertyBrowser->setEnabled(object); - mActionAddProperty->setEnabled(enabled); -} - -void PropertiesDock::updateActions() -{ - const QList items = mPropertyBrowser->selectedItems(); - bool allCustomProperties = !items.isEmpty() && mPropertyBrowser->allCustomPropertyItems(items); - bool editingTileset = mDocument && mDocument->type() == Document::TilesetDocumentType; - bool isTileset = mPropertyBrowser->object() && mPropertyBrowser->object()->isPartOfTileset(); - bool canModify = allCustomProperties && (!isTileset || editingTileset); - - // Disable remove and rename actions when none of the selected objects - // actually have the selected property (it may be inherited). - if (canModify) { - for (QtBrowserItem *item : items) { - if (!anyObjectHasProperty(mDocument->currentObjects(), item->property()->propertyName())) { - canModify = false; - break; - } - } - } - - mActionRemoveProperty->setEnabled(canModify); - mActionRenameProperty->setEnabled(canModify && items.size() == 1); -} - -void PropertiesDock::cutProperties() -{ - if (copyProperties()) - removeProperties(); -} - -bool PropertiesDock::copyProperties() -{ - Object *object = mPropertyBrowser->object(); - if (!object) - return false; - - Properties properties; - - const QList items = mPropertyBrowser->selectedItems(); - for (QtBrowserItem *item : items) { - if (!mPropertyBrowser->isCustomPropertyItem(item)) - return false; - - const QString name = item->property()->propertyName(); - const QVariant value = object->property(name); - if (!value.isValid()) - return false; - - properties.insert(name, value); - } - - ClipboardManager::instance()->setProperties(properties); - return true; -} - -void PropertiesDock::pasteProperties() -{ - auto clipboardManager = ClipboardManager::instance(); - - Properties pastedProperties = clipboardManager->properties(); - if (pastedProperties.isEmpty()) - return; - - const QList objects = mDocument->currentObjects(); - if (objects.isEmpty()) - return; - - QList commands; - - for (Object *object : objects) { - Properties properties = object->properties(); - mergeProperties(properties, pastedProperties); - - if (object->properties() != properties) { - commands.append(new ChangeProperties(mDocument, QString(), object, - properties)); - } - } - - if (!commands.isEmpty()) { - QUndoStack *undoStack = mDocument->undoStack(); - undoStack->beginMacro(tr("Paste Property/Properties", nullptr, - pastedProperties.size())); - - for (QUndoCommand *command : commands) - undoStack->push(command); - - undoStack->endMacro(); - } -} - -void PropertiesDock::openAddPropertyDialog() -{ - AddPropertyDialog dialog(mPropertyBrowser); - if (dialog.exec() == AddPropertyDialog::Accepted) - addProperty(dialog.propertyName(), dialog.propertyValue()); -} - -void PropertiesDock::addProperty(const QString &name, const QVariant &value) -{ - if (name.isEmpty()) - return; - Object *object = mDocument->currentObject(); - if (!object) - return; - - if (!object->hasProperty(name)) { - QUndoStack *undoStack = mDocument->undoStack(); - undoStack->push(new SetProperty(mDocument, - mDocument->currentObjects(), - name, value)); - } - - mPropertyBrowser->editCustomProperty(name); -} - -void PropertiesDock::removeProperties() -{ - Object *object = mDocument->currentObject(); - if (!object) - return; - - const QList items = mPropertyBrowser->selectedItems(); - if (items.isEmpty() || !mPropertyBrowser->allCustomPropertyItems(items)) - return; - - QStringList propertyNames; - for (QtBrowserItem *item : items) - propertyNames.append(item->property()->propertyName()); - - QUndoStack *undoStack = mDocument->undoStack(); - undoStack->beginMacro(tr("Remove Property/Properties", nullptr, - propertyNames.size())); - - for (const QString &name : propertyNames) { - undoStack->push(new RemoveProperty(mDocument, - mDocument->currentObjects(), - name)); - } - - undoStack->endMacro(); -} - -void PropertiesDock::renameProperty() -{ - QtBrowserItem *item = mPropertyBrowser->currentItem(); - if (!mPropertyBrowser->isCustomPropertyItem(item)) - return; - - const QString oldName = item->property()->propertyName(); - - QInputDialog *dialog = new QInputDialog(mPropertyBrowser); - dialog->setAttribute(Qt::WA_DeleteOnClose); - dialog->setInputMode(QInputDialog::TextInput); - dialog->setLabelText(tr("Name:")); - dialog->setTextValue(oldName); - dialog->setWindowTitle(tr("Rename Property")); - connect(dialog, &QInputDialog::textValueSelected, this, &PropertiesDock::renamePropertyTo); - dialog->open(); -} - -void PropertiesDock::renamePropertyTo(const QString &name) -{ - if (name.isEmpty()) - return; - - QtBrowserItem *item = mPropertyBrowser->currentItem(); - if (!item) - return; - - const QString oldName = item->property()->propertyName(); - if (oldName == name) - return; - - QUndoStack *undoStack = mDocument->undoStack(); - undoStack->push(new RenameProperty(mDocument, mDocument->currentObjects(), oldName, name)); -} - -void PropertiesDock::showContextMenu(const QPoint &pos) -{ - const Object *object = mDocument->currentObject(); - if (!object) - return; - - const QList items = mPropertyBrowser->selectedItems(); - const bool customPropertiesSelected = !items.isEmpty() && mPropertyBrowser->allCustomPropertyItems(items); - - bool currentObjectHasAllProperties = true; - QStringList propertyNames; - for (QtBrowserItem *item : items) { - const QString propertyName = item->property()->propertyName(); - propertyNames.append(propertyName); - - if (!object->hasProperty(propertyName)) - currentObjectHasAllProperties = false; - } - - QMenu contextMenu(mPropertyBrowser); - - if (customPropertiesSelected && propertyNames.size() == 1) { - const auto value = object->resolvedProperty(propertyNames.first()); - if (value.userType() == filePathTypeId()) { - const FilePath filePath = value.value(); - const QString localFile = filePath.url.toLocalFile(); - - if (!localFile.isEmpty()) { - Utils::addOpenContainingFolderAction(contextMenu, localFile); - - if (QFileInfo { localFile }.isFile()) - Utils::addOpenWithSystemEditorAction(contextMenu, localFile); - } - } else if (value.userType() == objectRefTypeId()) { - if (auto mapDocument = qobject_cast(mDocument)) { - const DisplayObjectRef objectRef(value.value(), mapDocument); - - contextMenu.addAction(tr("Go to Object"), [=] { - if (auto object = objectRef.object()) { - objectRef.mapDocument->setSelectedObjects({object}); - emit objectRef.mapDocument->focusMapObjectRequested(object); - } - })->setEnabled(objectRef.object()); - } - } - } - - if (!contextMenu.isEmpty()) - contextMenu.addSeparator(); - - QAction *cutAction = contextMenu.addAction(tr("Cu&t"), this, &PropertiesDock::cutProperties); - QAction *copyAction = contextMenu.addAction(tr("&Copy"), this, &PropertiesDock::copyProperties); - QAction *pasteAction = contextMenu.addAction(tr("&Paste"), this, &PropertiesDock::pasteProperties); - contextMenu.addSeparator(); - QMenu *convertMenu = nullptr; - - if (customPropertiesSelected) { - convertMenu = contextMenu.addMenu(tr("Convert To")); - contextMenu.addAction(mActionRemoveProperty); - contextMenu.addAction(mActionRenameProperty); - } else { - contextMenu.addAction(mActionAddProperty); - } - - cutAction->setShortcuts(QKeySequence::Cut); - cutAction->setIcon(QIcon(QLatin1String(":/images/16/edit-cut.png"))); - cutAction->setEnabled(customPropertiesSelected && currentObjectHasAllProperties); - copyAction->setShortcuts(QKeySequence::Copy); - copyAction->setIcon(QIcon(QLatin1String(":/images/16/edit-copy.png"))); - copyAction->setEnabled(customPropertiesSelected && currentObjectHasAllProperties); - pasteAction->setShortcuts(QKeySequence::Paste); - pasteAction->setIcon(QIcon(QLatin1String(":/images/16/edit-paste.png"))); - pasteAction->setEnabled(ClipboardManager::instance()->hasProperties()); - - Utils::setThemeIcon(cutAction, "edit-cut"); - Utils::setThemeIcon(copyAction, "edit-copy"); - Utils::setThemeIcon(pasteAction, "edit-paste"); - - if (convertMenu) { - const int convertTo[] = { - QMetaType::Bool, - QMetaType::QColor, - QMetaType::Double, - filePathTypeId(), - objectRefTypeId(), - QMetaType::Int, - QMetaType::QString - }; - - // todo: could include custom property types - - for (int toType : convertTo) { - bool someDifferentType = false; - bool allCanConvert = true; - - for (const QString &propertyName : propertyNames) { - QVariant propertyValue = object->property(propertyName); - - if (propertyValue.userType() != toType) - someDifferentType = true; - - if (!propertyValue.convert(toType)) { - allCanConvert = false; - break; - } - } - - if (someDifferentType && allCanConvert) { - QAction *action = convertMenu->addAction(typeToName(toType)); - action->setData(toType); - } - } - - convertMenu->setEnabled(!convertMenu->actions().isEmpty()); - } - - ActionManager::applyMenuExtensions(&contextMenu, MenuIds::propertiesViewProperties); - - const QPoint globalPos = mPropertyBrowser->mapToGlobal(pos); - const QAction *selectedItem = contextMenu.exec(globalPos); - - if (selectedItem && convertMenu && selectedItem->parentWidget() == convertMenu) { - QUndoStack *undoStack = mDocument->undoStack(); - undoStack->beginMacro(tr("Convert Property/Properties", nullptr, items.size())); - - for (const QString &propertyName : propertyNames) { - QVariant propertyValue = object->property(propertyName); - - int toType = selectedItem->data().toInt(); - propertyValue.convert(toType); - - undoStack->push(new SetProperty(mDocument, - mDocument->currentObjects(), - propertyName, propertyValue)); - } - - undoStack->endMacro(); - } + mPropertiesWidget->selectCustomProperty(name); } bool PropertiesDock::event(QEvent *event) { switch (event->type()) { - case QEvent::ShortcutOverride: { - QKeyEvent *keyEvent = static_cast(event); - if (keyEvent->matches(QKeySequence::Delete) || keyEvent->key() == Qt::Key_Backspace - || keyEvent->matches(QKeySequence::Cut) - || keyEvent->matches(QKeySequence::Copy) - || keyEvent->matches(QKeySequence::Paste)) { - event->accept(); - return true; - } - break; - } case QEvent::LanguageChange: retranslateUi(); break; @@ -505,32 +63,16 @@ bool PropertiesDock::event(QEvent *event) return QDockWidget::event(event); } -void PropertiesDock::keyPressEvent(QKeyEvent *event) +void PropertiesDock::bringToFront() { - if (event->matches(QKeySequence::Delete) || event->key() == Qt::Key_Backspace) { - removeProperties(); - } else if (event->matches(QKeySequence::Cut)) { - cutProperties(); - } else if (event->matches(QKeySequence::Copy)) { - copyProperties(); - } else if (event->matches(QKeySequence::Paste)) { - pasteProperties(); - } else { - QDockWidget::keyPressEvent(event); - } + show(); + raise(); + mPropertiesWidget->setFocus(); } void PropertiesDock::retranslateUi() { setWindowTitle(tr("Properties")); - - mActionAddProperty->setText(tr("Add Property")); - - mActionRemoveProperty->setText(tr("Remove")); - mActionRemoveProperty->setToolTip(tr("Remove Property")); - - mActionRenameProperty->setText(tr("Rename...")); - mActionRenameProperty->setToolTip(tr("Rename Property")); } } // namespace Tiled diff --git a/src/tiled/propertiesdock.h b/src/tiled/propertiesdock.h index 624a569198..c39532484d 100644 --- a/src/tiled/propertiesdock.h +++ b/src/tiled/propertiesdock.h @@ -21,17 +21,12 @@ #pragma once #include -#include - -class QtBrowserItem; namespace Tiled { -class Object; -class Tileset; - class Document; -class PropertyBrowser; + +class PropertiesWidget; class PropertiesDock : public QDockWidget { @@ -46,34 +41,16 @@ class PropertiesDock : public QDockWidget void setDocument(Document *document); public slots: - void bringToFront(); void selectCustomProperty(const QString &name); protected: bool event(QEvent *event) override; - void keyPressEvent(QKeyEvent *event) override; private: - void currentObjectChanged(Object *object); - void updateActions(); - - void cutProperties(); - bool copyProperties(); - void pasteProperties(); - void openAddPropertyDialog(); - void addProperty(const QString &name, const QVariant &value); - void removeProperties(); - void renameProperty(); - void renamePropertyTo(const QString &name); - void showContextMenu(const QPoint &pos); - + void bringToFront(); void retranslateUi(); - Document *mDocument; - PropertyBrowser *mPropertyBrowser; - QAction *mActionAddProperty; - QAction *mActionRemoveProperty; - QAction *mActionRenameProperty; + PropertiesWidget *mPropertiesWidget; }; } // namespace Tiled diff --git a/src/tiled/propertieswidget.cpp b/src/tiled/propertieswidget.cpp new file mode 100644 index 0000000000..e118088c2f --- /dev/null +++ b/src/tiled/propertieswidget.cpp @@ -0,0 +1,531 @@ +/* + * propertieswidget.cpp + * Copyright 2013-2023, Thorbjørn Lindeijer + * + * This file is part of Tiled. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ + +#include "propertieswidget.h" + +#include "actionmanager.h" +#include "addpropertydialog.h" +#include "changeproperties.h" +#include "clipboardmanager.h" +#include "mapdocument.h" +#include "mapobject.h" +#include "propertybrowser.h" +#include "utils.h" +#include "variantpropertymanager.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Tiled { + +PropertiesWidget::PropertiesWidget(QWidget *parent) + : QWidget{parent} + , mDocument(nullptr) + , mPropertyBrowser(new PropertyBrowser) +{ + mActionAddProperty = new QAction(this); + mActionAddProperty->setEnabled(false); + mActionAddProperty->setIcon(QIcon(QLatin1String(":/images/16/add.png"))); + connect(mActionAddProperty, &QAction::triggered, + this, &PropertiesWidget::openAddPropertyDialog); + + mActionRemoveProperty = new QAction(this); + mActionRemoveProperty->setEnabled(false); + mActionRemoveProperty->setIcon(QIcon(QLatin1String(":/images/16/remove.png"))); + mActionRemoveProperty->setShortcuts(QKeySequence::Delete); + connect(mActionRemoveProperty, &QAction::triggered, + this, &PropertiesWidget::removeProperties); + + mActionRenameProperty = new QAction(this); + mActionRenameProperty->setEnabled(false); + mActionRenameProperty->setIcon(QIcon(QLatin1String(":/images/16/rename.png"))); + connect(mActionRenameProperty, &QAction::triggered, + this, &PropertiesWidget::renameProperty); + + Utils::setThemeIcon(mActionAddProperty, "add"); + Utils::setThemeIcon(mActionRemoveProperty, "remove"); + Utils::setThemeIcon(mActionRenameProperty, "rename"); + + QToolBar *toolBar = new QToolBar; + toolBar->setFloatable(false); + toolBar->setMovable(false); + toolBar->setIconSize(Utils::smallIconSize()); + toolBar->addAction(mActionAddProperty); + toolBar->addAction(mActionRemoveProperty); + toolBar->addAction(mActionRenameProperty); + + QVBoxLayout *layout = new QVBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + layout->setSpacing(0); + layout->addWidget(mPropertyBrowser); + layout->addWidget(toolBar); + setLayout(layout); + + mPropertyBrowser->setContextMenuPolicy(Qt::CustomContextMenu); + connect(mPropertyBrowser, &PropertyBrowser::customContextMenuRequested, + this, &PropertiesWidget::showContextMenu); + connect(mPropertyBrowser, &PropertyBrowser::selectedItemsChanged, + this, &PropertiesWidget::updateActions); + + retranslateUi(); +} + +PropertiesWidget::~PropertiesWidget() +{ + // Disconnect to avoid crashing due to signals emitted during destruction + mPropertyBrowser->disconnect(this); +} + +void PropertiesWidget::setDocument(Document *document) +{ + if (mDocument == document) + return; + + if (mDocument) + mDocument->disconnect(this); + + mDocument = document; + mPropertyBrowser->setDocument(document); + + if (document) { + connect(document, &Document::currentObjectChanged, + this, &PropertiesWidget::currentObjectChanged); + connect(document, &Document::editCurrentObject, + this, &PropertiesWidget::bringToFront); + + connect(document, &Document::propertyAdded, + this, &PropertiesWidget::updateActions); + connect(document, &Document::propertyRemoved, + this, &PropertiesWidget::updateActions); + + currentObjectChanged(document->currentObject()); + } else { + currentObjectChanged(nullptr); + } +} + +void PropertiesWidget::selectCustomProperty(const QString &name) +{ + mPropertyBrowser->selectCustomProperty(name); +} + +static bool anyObjectHasProperty(const QList &objects, const QString &name) +{ + for (Object *obj : objects) { + if (obj->hasProperty(name)) + return true; + } + return false; +} + +void PropertiesWidget::currentObjectChanged(Object *object) +{ + mPropertyBrowser->setObject(object); + + bool editingTileset = mDocument && mDocument->type() == Document::TilesetDocumentType; + bool isTileset = object && object->isPartOfTileset(); + bool enabled = object && (!isTileset || editingTileset); + + mPropertyBrowser->setEnabled(object); + mActionAddProperty->setEnabled(enabled); +} + +void PropertiesWidget::updateActions() +{ + const QList items = mPropertyBrowser->selectedItems(); + bool allCustomProperties = !items.isEmpty() && mPropertyBrowser->allCustomPropertyItems(items); + bool editingTileset = mDocument && mDocument->type() == Document::TilesetDocumentType; + bool isTileset = mPropertyBrowser->object() && mPropertyBrowser->object()->isPartOfTileset(); + bool canModify = allCustomProperties && (!isTileset || editingTileset); + + // Disable remove and rename actions when none of the selected objects + // actually have the selected property (it may be inherited). + if (canModify) { + for (QtBrowserItem *item : items) { + if (!anyObjectHasProperty(mDocument->currentObjects(), item->property()->propertyName())) { + canModify = false; + break; + } + } + } + + mActionRemoveProperty->setEnabled(canModify); + mActionRenameProperty->setEnabled(canModify && items.size() == 1); +} + +void PropertiesWidget::cutProperties() +{ + if (copyProperties()) + removeProperties(); +} + +bool PropertiesWidget::copyProperties() +{ + Object *object = mPropertyBrowser->object(); + if (!object) + return false; + + Properties properties; + + const QList items = mPropertyBrowser->selectedItems(); + for (QtBrowserItem *item : items) { + if (!mPropertyBrowser->isCustomPropertyItem(item)) + return false; + + const QString name = item->property()->propertyName(); + const QVariant value = object->property(name); + if (!value.isValid()) + return false; + + properties.insert(name, value); + } + + ClipboardManager::instance()->setProperties(properties); + return true; +} + +void PropertiesWidget::pasteProperties() +{ + auto clipboardManager = ClipboardManager::instance(); + + Properties pastedProperties = clipboardManager->properties(); + if (pastedProperties.isEmpty()) + return; + + const QList objects = mDocument->currentObjects(); + if (objects.isEmpty()) + return; + + QList commands; + + for (Object *object : objects) { + Properties properties = object->properties(); + mergeProperties(properties, pastedProperties); + + if (object->properties() != properties) { + commands.append(new ChangeProperties(mDocument, QString(), object, + properties)); + } + } + + if (!commands.isEmpty()) { + QUndoStack *undoStack = mDocument->undoStack(); + undoStack->beginMacro(QCoreApplication::translate("Tiled::PropertiesDock", + "Paste Property/Properties", + nullptr, + pastedProperties.size())); + + for (QUndoCommand *command : commands) + undoStack->push(command); + + undoStack->endMacro(); + } +} + +void PropertiesWidget::openAddPropertyDialog() +{ + AddPropertyDialog dialog(mPropertyBrowser); + if (dialog.exec() == AddPropertyDialog::Accepted) + addProperty(dialog.propertyName(), dialog.propertyValue()); +} + +void PropertiesWidget::addProperty(const QString &name, const QVariant &value) +{ + if (name.isEmpty()) + return; + Object *object = mDocument->currentObject(); + if (!object) + return; + + if (!object->hasProperty(name)) { + QUndoStack *undoStack = mDocument->undoStack(); + undoStack->push(new SetProperty(mDocument, + mDocument->currentObjects(), + name, value)); + } + + mPropertyBrowser->editCustomProperty(name); +} + +void PropertiesWidget::removeProperties() +{ + Object *object = mDocument->currentObject(); + if (!object) + return; + + const QList items = mPropertyBrowser->selectedItems(); + if (items.isEmpty() || !mPropertyBrowser->allCustomPropertyItems(items)) + return; + + QStringList propertyNames; + for (QtBrowserItem *item : items) + propertyNames.append(item->property()->propertyName()); + + QUndoStack *undoStack = mDocument->undoStack(); + undoStack->beginMacro(QCoreApplication::translate("Tiled::PropertiesDock", + "Remove Property/Properties", + nullptr, + propertyNames.size())); + + for (const QString &name : propertyNames) { + undoStack->push(new RemoveProperty(mDocument, + mDocument->currentObjects(), + name)); + } + + undoStack->endMacro(); +} + +void PropertiesWidget::renameProperty() +{ + QtBrowserItem *item = mPropertyBrowser->currentItem(); + if (!mPropertyBrowser->isCustomPropertyItem(item)) + return; + + const QString oldName = item->property()->propertyName(); + + QInputDialog *dialog = new QInputDialog(mPropertyBrowser); + dialog->setAttribute(Qt::WA_DeleteOnClose); + dialog->setInputMode(QInputDialog::TextInput); + dialog->setLabelText(QCoreApplication::translate("Tiled::PropertiesDock", "Name:")); + dialog->setTextValue(oldName); + dialog->setWindowTitle(QCoreApplication::translate("Tiled::PropertiesDock", "Rename Property")); + connect(dialog, &QInputDialog::textValueSelected, this, &PropertiesWidget::renamePropertyTo); + dialog->open(); +} + +void PropertiesWidget::renamePropertyTo(const QString &name) +{ + if (name.isEmpty()) + return; + + QtBrowserItem *item = mPropertyBrowser->currentItem(); + if (!item) + return; + + const QString oldName = item->property()->propertyName(); + if (oldName == name) + return; + + QUndoStack *undoStack = mDocument->undoStack(); + undoStack->push(new RenameProperty(mDocument, mDocument->currentObjects(), oldName, name)); +} + +void PropertiesWidget::showContextMenu(const QPoint &pos) +{ + const Object *object = mDocument->currentObject(); + if (!object) + return; + + const QList items = mPropertyBrowser->selectedItems(); + const bool customPropertiesSelected = !items.isEmpty() && mPropertyBrowser->allCustomPropertyItems(items); + + bool currentObjectHasAllProperties = true; + QStringList propertyNames; + for (QtBrowserItem *item : items) { + const QString propertyName = item->property()->propertyName(); + propertyNames.append(propertyName); + + if (!object->hasProperty(propertyName)) + currentObjectHasAllProperties = false; + } + + QMenu contextMenu(mPropertyBrowser); + + if (customPropertiesSelected && propertyNames.size() == 1) { + const auto value = object->resolvedProperty(propertyNames.first()); + if (value.userType() == filePathTypeId()) { + const FilePath filePath = value.value(); + const QString localFile = filePath.url.toLocalFile(); + + if (!localFile.isEmpty()) { + Utils::addOpenContainingFolderAction(contextMenu, localFile); + + if (QFileInfo { localFile }.isFile()) + Utils::addOpenWithSystemEditorAction(contextMenu, localFile); + } + } else if (value.userType() == objectRefTypeId()) { + if (auto mapDocument = qobject_cast(mDocument)) { + const DisplayObjectRef objectRef(value.value(), mapDocument); + + contextMenu.addAction(QCoreApplication::translate("Tiled::PropertiesDock", "Go to Object"), [=] { + if (auto object = objectRef.object()) { + objectRef.mapDocument->setSelectedObjects({object}); + emit objectRef.mapDocument->focusMapObjectRequested(object); + } + })->setEnabled(objectRef.object()); + } + } + } + + if (!contextMenu.isEmpty()) + contextMenu.addSeparator(); + + QAction *cutAction = contextMenu.addAction(QCoreApplication::translate("Tiled::PropertiesDock", "Cu&t"), this, &PropertiesWidget::cutProperties); + QAction *copyAction = contextMenu.addAction(QCoreApplication::translate("Tiled::PropertiesDock", "&Copy"), this, &PropertiesWidget::copyProperties); + QAction *pasteAction = contextMenu.addAction(QCoreApplication::translate("Tiled::PropertiesDock", "&Paste"), this, &PropertiesWidget::pasteProperties); + contextMenu.addSeparator(); + QMenu *convertMenu = nullptr; + + if (customPropertiesSelected) { + convertMenu = contextMenu.addMenu(QCoreApplication::translate("Tiled::PropertiesDock", "Convert To")); + contextMenu.addAction(mActionRemoveProperty); + contextMenu.addAction(mActionRenameProperty); + } else { + contextMenu.addAction(mActionAddProperty); + } + + cutAction->setShortcuts(QKeySequence::Cut); + cutAction->setIcon(QIcon(QLatin1String(":/images/16/edit-cut.png"))); + cutAction->setEnabled(customPropertiesSelected && currentObjectHasAllProperties); + copyAction->setShortcuts(QKeySequence::Copy); + copyAction->setIcon(QIcon(QLatin1String(":/images/16/edit-copy.png"))); + copyAction->setEnabled(customPropertiesSelected && currentObjectHasAllProperties); + pasteAction->setShortcuts(QKeySequence::Paste); + pasteAction->setIcon(QIcon(QLatin1String(":/images/16/edit-paste.png"))); + pasteAction->setEnabled(ClipboardManager::instance()->hasProperties()); + + Utils::setThemeIcon(cutAction, "edit-cut"); + Utils::setThemeIcon(copyAction, "edit-copy"); + Utils::setThemeIcon(pasteAction, "edit-paste"); + + if (convertMenu) { + const int convertTo[] = { + QMetaType::Bool, + QMetaType::QColor, + QMetaType::Double, + filePathTypeId(), + objectRefTypeId(), + QMetaType::Int, + QMetaType::QString + }; + + // todo: could include custom property types + + for (int toType : convertTo) { + bool someDifferentType = false; + bool allCanConvert = true; + + for (const QString &propertyName : propertyNames) { + QVariant propertyValue = object->property(propertyName); + + if (propertyValue.userType() != toType) + someDifferentType = true; + + if (!propertyValue.convert(toType)) { + allCanConvert = false; + break; + } + } + + if (someDifferentType && allCanConvert) { + QAction *action = convertMenu->addAction(typeToName(toType)); + action->setData(toType); + } + } + + convertMenu->setEnabled(!convertMenu->actions().isEmpty()); + } + + ActionManager::applyMenuExtensions(&contextMenu, MenuIds::propertiesViewProperties); + + const QPoint globalPos = mPropertyBrowser->mapToGlobal(pos); + const QAction *selectedItem = contextMenu.exec(globalPos); + + if (selectedItem && convertMenu && selectedItem->parentWidget() == convertMenu) { + QUndoStack *undoStack = mDocument->undoStack(); + undoStack->beginMacro(QCoreApplication::translate("Tiled::PropertiesDock", "Convert Property/Properties", nullptr, items.size())); + + for (const QString &propertyName : propertyNames) { + QVariant propertyValue = object->property(propertyName); + + int toType = selectedItem->data().toInt(); + propertyValue.convert(toType); + + undoStack->push(new SetProperty(mDocument, + mDocument->currentObjects(), + propertyName, propertyValue)); + } + + undoStack->endMacro(); + } +} + +bool PropertiesWidget::event(QEvent *event) +{ + switch (event->type()) { + case QEvent::ShortcutOverride: { + QKeyEvent *keyEvent = static_cast(event); + if (keyEvent->matches(QKeySequence::Delete) || keyEvent->key() == Qt::Key_Backspace + || keyEvent->matches(QKeySequence::Cut) + || keyEvent->matches(QKeySequence::Copy) + || keyEvent->matches(QKeySequence::Paste)) { + event->accept(); + return true; + } + break; + } + case QEvent::LanguageChange: + retranslateUi(); + break; + default: + break; + } + + return QWidget::event(event); +} + +void PropertiesWidget::keyPressEvent(QKeyEvent *event) +{ + if (event->matches(QKeySequence::Delete) || event->key() == Qt::Key_Backspace) { + removeProperties(); + } else if (event->matches(QKeySequence::Cut)) { + cutProperties(); + } else if (event->matches(QKeySequence::Copy)) { + copyProperties(); + } else if (event->matches(QKeySequence::Paste)) { + pasteProperties(); + } else { + QWidget::keyPressEvent(event); + } +} + +void PropertiesWidget::retranslateUi() +{ + mActionAddProperty->setText(QCoreApplication::translate("Tiled::PropertiesDock", "Add Property")); + + mActionRemoveProperty->setText(QCoreApplication::translate("Tiled::PropertiesDock", "Remove")); + mActionRemoveProperty->setToolTip(QCoreApplication::translate("Tiled::PropertiesDock", "Remove Property")); + + mActionRenameProperty->setText(QCoreApplication::translate("Tiled::PropertiesDock", "Rename...")); + mActionRenameProperty->setToolTip(QCoreApplication::translate("Tiled::PropertiesDock", "Rename Property")); +} + +} // namespace Tiled + +#include "moc_propertieswidget.cpp" diff --git a/src/tiled/propertieswidget.h b/src/tiled/propertieswidget.h new file mode 100644 index 0000000000..022a520b2f --- /dev/null +++ b/src/tiled/propertieswidget.h @@ -0,0 +1,83 @@ +/* + * propertieswidget.h + * Copyright 2013-2023, Thorbjørn Lindeijer + * + * This file is part of Tiled. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ + +#pragma once + +#include + +namespace Tiled { + +class Object; + +class Document; +class PropertyBrowser; + +/** + * The PropertiesWidget combines the PropertyBrowser with some controls that + * allow adding and removing properties. It also implements cut, copy and paste + * actions and the context menu. + */ +class PropertiesWidget : public QWidget +{ + Q_OBJECT + +public: + explicit PropertiesWidget(QWidget *parent = nullptr); + ~PropertiesWidget() override; + + /** + * Sets the \a document on which this properties dock will act. + */ + void setDocument(Document *document); + +signals: + void bringToFront(); + +public slots: + void selectCustomProperty(const QString &name); + +protected: + bool event(QEvent *event) override; + void keyPressEvent(QKeyEvent *event) override; + +private: + void currentObjectChanged(Object *object); + void updateActions(); + + void cutProperties(); + bool copyProperties(); + void pasteProperties(); + void openAddPropertyDialog(); + void addProperty(const QString &name, const QVariant &value); + void removeProperties(); + void renameProperty(); + void renamePropertyTo(const QString &name); + void showContextMenu(const QPoint &pos); + + void retranslateUi(); + + Document *mDocument; + PropertyBrowser *mPropertyBrowser; + QAction *mActionAddProperty; + QAction *mActionRemoveProperty; + QAction *mActionRenameProperty; +}; + +} // namespace Tiled diff --git a/src/tiled/propertybrowser.cpp b/src/tiled/propertybrowser.cpp index f1f5636523..1b1850b873 100644 --- a/src/tiled/propertybrowser.cpp +++ b/src/tiled/propertybrowser.cpp @@ -488,6 +488,7 @@ static void addAutomappingProperties(Properties &properties, const Object *objec case Object::TileType: case Object::WangSetType: case Object::WangColorType: + case Object::ProjectType: break; } } @@ -695,6 +696,7 @@ void PropertyBrowser::valueChanged(QtProperty *property, const QVariant &val) case Object::TileType: applyTileValue(id, val); break; case Object::WangSetType: applyWangSetValue(id, val); break; case Object::WangColorType: applyWangColorValue(id, val); break; + case Object::ProjectType: break; } } @@ -1802,6 +1804,7 @@ void PropertyBrowser::addProperties() case Object::TileType: addTileProperties(); break; case Object::WangSetType: addWangSetProperties(); break; case Object::WangColorType: addWangColorProperties(); break; + case Object::ProjectType: break; } // Make sure certain properties are collapsed, to save space @@ -1841,7 +1844,8 @@ void PropertyBrowser::updateProperties() QScopedValueRollback updating(mUpdating, true); - mIdToProperty[ClassProperty]->setValue(mObject->className()); + if (auto classProperty = mIdToProperty.value(ClassProperty)) + classProperty->setValue(mObject->className()); switch (mObject->typeId()) { case Object::MapType: { @@ -2017,6 +2021,8 @@ void PropertyBrowser::updateProperties() mIdToProperty[WangColorProbabilityProperty]->setValue(wangColor->probability()); break; } + case Object::ProjectType: + break; } } diff --git a/src/tiled/propertytypeseditor.cpp b/src/tiled/propertytypeseditor.cpp index d0cae9c5e8..c6b4d19c15 100644 --- a/src/tiled/propertytypeseditor.cpp +++ b/src/tiled/propertytypeseditor.cpp @@ -250,7 +250,7 @@ PropertyTypesEditor::PropertyTypesEditor(QWidget *parent) Preferences *prefs = Preferences::instance(); - auto &project = ProjectManager::instance()->project(); + const auto &project = ProjectManager::instance()->project(); mPropertyTypesModel->setPropertyTypes(project.propertyTypes()); connect(prefs, &Preferences::propertyTypesChanged, @@ -412,7 +412,7 @@ void PropertyTypesEditor::propertyTypesChanged() if (mSettingPrefPropertyTypes) return; - auto &project = ProjectManager::instance()->project(); + const auto &project = ProjectManager::instance()->project(); mPropertyTypesModel->setPropertyTypes(project.propertyTypes()); selectedPropertyTypesChanged(); diff --git a/src/tiled/scriptmodule.cpp b/src/tiled/scriptmodule.cpp index 85c64b7e1e..a5f558eeff 100644 --- a/src/tiled/scriptmodule.cpp +++ b/src/tiled/scriptmodule.cpp @@ -214,11 +214,9 @@ QList ScriptModule::openAssets() const return assets; } -EditableProject *ScriptModule::project() +EditableAsset *ScriptModule::project() { - if (!mEditableProject) - mEditableProject = new EditableProject(&ProjectManager::instance()->project(), this); - return mEditableProject; + return ProjectManager::instance()->editableProject(); } TilesetEditor *ScriptModule::tilesetEditor() const diff --git a/src/tiled/scriptmodule.h b/src/tiled/scriptmodule.h index f7fdbbf3a8..c7f348cf6f 100644 --- a/src/tiled/scriptmodule.h +++ b/src/tiled/scriptmodule.h @@ -23,7 +23,6 @@ #include "id.h" #include "issuesdock.h" #include "properties.h" -#include "editableproject.h" #include #include @@ -70,7 +69,7 @@ class ScriptModule : public QObject Q_PROPERTY(Tiled::EditableAsset *activeAsset READ activeAsset WRITE setActiveAsset NOTIFY activeAssetChanged) Q_PROPERTY(QList openAssets READ openAssets) - Q_PROPERTY(Tiled::EditableProject *project READ project) + Q_PROPERTY(Tiled::EditableAsset *project READ project) Q_PROPERTY(Tiled::MapEditor *mapEditor READ mapEditor) Q_PROPERTY(Tiled::TilesetEditor *tilesetEditor READ tilesetEditor) @@ -100,7 +99,7 @@ class ScriptModule : public QObject QList openAssets() const; - EditableProject *project(); + EditableAsset *project(); TilesetEditor *tilesetEditor() const; MapEditor *mapEditor() const; @@ -176,7 +175,6 @@ public slots: std::map> mRegisteredTools; QStringList mScriptArguments; - EditableProject *mEditableProject = nullptr; }; inline bool ScriptModule::versionLessThan(const QString &a)