From eb087028a443d821522db97df818b42160927466 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thorbj=C3=B8rn=20Lindeijer?= Date: Wed, 25 Mar 2026 16:39:20 +0100 Subject: [PATCH 01/21] Fix ability to change properties after deselecting current object (#4440) When deselecting the current object, its properties generally remain visible in the Properties view. But when changing any of the properties, the changes were only applied to "selected objects". Now, when there is still a MapObject set as the current object, changing its properties will apply to it regardless of whether it is still selected. (cherry picked from commit db0d1c47f5ad33bc71f35dfeb1cd870dfd6bf531) --- NEWS.md | 4 ++++ src/tiled/propertieswidget.cpp | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/NEWS.md b/NEWS.md index 0f95cff709..ab7521e61f 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,7 @@ +### Unreleased + +* Fixed ability to change properties after deselecting the current object (#4440) + ### Tiled 1.12.1 (25 March 2026) * Fixed Properties view flicker when switching between objects or files (#4460) diff --git a/src/tiled/propertieswidget.cpp b/src/tiled/propertieswidget.cpp index 155a25d7f8..5f03cde9fc 100644 --- a/src/tiled/propertieswidget.cpp +++ b/src/tiled/propertieswidget.cpp @@ -1946,7 +1946,7 @@ class MapObjectProperties : public ObjectProperties QUndoCommand *command = new ChangeMapObject(mapDocument(), mapObject(), property, value); - if (mapDocument()->selectedObjects().size() == 1) { + if (mapDocument()->selectedObjects().size() <= 1) { push(command); return; } From db21f4e2594178afddc1d7e42f5c4740aa75d8c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Huy=20V=C5=A9=20=28Josh=29?= Date: Wed, 25 Mar 2026 23:07:38 +0700 Subject: [PATCH 02/21] Reduce animated tile marker opacity during Wang editing (#4449) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Thorbjørn Lindeijer (cherry picked from commit ac6461c66885aaaa107b0fa26bc53a4e48e44298) --- NEWS.md | 1 + src/tiled/tilesetview.cpp | 18 +++++++++--------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/NEWS.md b/NEWS.md index ab7521e61f..1e3a038be0 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,6 @@ ### Unreleased +* Reduced animated tile marker opacity during terrain editing (by Huy Vũ, #4449) * Fixed ability to change properties after deselecting the current object (#4440) ### Tiled 1.12.1 (25 March 2026) diff --git a/src/tiled/tilesetview.cpp b/src/tiled/tilesetview.cpp index 41907c7543..a6cb444361 100644 --- a/src/tiled/tilesetview.cpp +++ b/src/tiled/tilesetview.cpp @@ -53,8 +53,6 @@ using namespace Tiled; -namespace { - static void setupTilesetGridTransform(const Tileset &tileset, QTransform &transform, QRect &targetRect) { if (tileset.orientation() == Tileset::Isometric) { @@ -77,6 +75,8 @@ static void setupTilesetGridTransform(const Tileset &tileset, QTransform &transf } } +namespace { + /** * The delegate for drawing tile items in the tileset view. */ @@ -95,7 +95,7 @@ class TileDelegate : public QAbstractItemDelegate const QModelIndex &index) const override; private: - static void drawFilmStrip(QPainter *painter, QRect targetRect); + static void drawFilmStrip(QPainter *painter, QRect targetRect, bool wangEditingActive); void drawWangOverlay(QPainter *painter, const Tile *tile, @@ -105,6 +105,8 @@ class TileDelegate : public QAbstractItemDelegate TilesetView *mTilesetView; }; +} // anonymous namespace + void TileDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const @@ -161,7 +163,7 @@ void TileDelegate::paint(QPainter *painter, // Overlay with film strip when animated if (mTilesetView->markAnimatedTiles() && tile->isAnimated()) - drawFilmStrip(painter, targetRect); + drawFilmStrip(painter, targetRect, mTilesetView->isEditWangSet()); const auto highlight = option.palette.highlight(); @@ -211,7 +213,7 @@ QSize TileDelegate::sizeHint(const QStyleOptionViewItem & /* option */, return QSize(extra, extra); } -void TileDelegate::drawFilmStrip(QPainter *painter, QRect targetRect) +void TileDelegate::drawFilmStrip(QPainter *painter, QRect targetRect, bool wangEditingActive) { painter->save(); @@ -224,7 +226,7 @@ void TileDelegate::drawFilmStrip(QPainter *painter, QRect targetRect) painter->scale(scale, scale); painter->translate(-18, 3); painter->rotate(-45); - painter->setOpacity(0.8); + painter->setOpacity(wangEditingActive ? 0.3 : 0.8); QRectF strip(0, 0, 32, 6); painter->fillRect(strip, Qt::black); @@ -278,8 +280,6 @@ void TileDelegate::drawWangOverlay(QPainter *painter, painter->restore(); } -} // anonymous namespace - TilesetView::TilesetView(QWidget *parent) : QTableView(parent) , mZoomable(new Zoomable(this)) @@ -504,7 +504,7 @@ void TilesetView::keyPressEvent(QKeyEvent *event) return; } - return QTableView::keyPressEvent(event); + QTableView::keyPressEvent(event); } void TilesetView::setRelocateTiles(bool enabled) From c244b895a6c8ce20724a7f2dbb86c13108ed6190 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thorbj=C3=B8rn=20Lindeijer?= Date: Wed, 22 Apr 2026 15:45:35 +0200 Subject: [PATCH 03/21] Fix Properties view getting stuck with updates disabled Regression introduced in 17a47f40107e3354b7103592e3261e55d22bc6df. When switching from a map to the tile collision editor and back while a collision object was selected, the Properties view could stop updating entirely. ScopedUpdatesDisabler unconditionally called setUpdatesEnabled(false) on the widget, but only called setUpdatesEnabled(true) when they were previously enabled. However, updates can be disabled due to them being disabled on a parent widget as well, in which case the updates could remain explicitly disabled for the child widget. Now we skip the disable when updates are already effectively disabled, so they will effectively re-enable as well. (cherry picked from commit 5b3bf7613d66b432313bd3aecf0f472415968d58) --- NEWS.md | 1 + src/tiled/propertieswidget.cpp | 10 +++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/NEWS.md b/NEWS.md index 1e3a038be0..7e04dd10d5 100644 --- a/NEWS.md +++ b/NEWS.md @@ -2,6 +2,7 @@ * Reduced animated tile marker opacity during terrain editing (by Huy Vũ, #4449) * Fixed ability to change properties after deselecting the current object (#4440) +* Fixed Properties view getting stuck with updates disabled (#4506) ### Tiled 1.12.1 (25 March 2026) diff --git a/src/tiled/propertieswidget.cpp b/src/tiled/propertieswidget.cpp index 5f03cde9fc..93b1d6fabb 100644 --- a/src/tiled/propertieswidget.cpp +++ b/src/tiled/propertieswidget.cpp @@ -76,15 +76,16 @@ class ScopedUpdatesDisabler { public: explicit ScopedUpdatesDisabler(QWidget *widget) - : mWidget(widget) - , mHadUpdatesEnabled(widget->updatesEnabled()) + // If updates are already effectively disabled, leave the widget alone + : mWidget(widget->updatesEnabled() ? widget : nullptr) { - widget->setUpdatesEnabled(false); + if (mWidget) + mWidget->setUpdatesEnabled(false); } ~ScopedUpdatesDisabler() { - if (!mHadUpdatesEnabled) + if (!mWidget) return; QTimer::singleShot(0, mWidget, [w = mWidget] { @@ -98,7 +99,6 @@ class ScopedUpdatesDisabler private: QWidget *mWidget; - bool mHadUpdatesEnabled; }; From 794aa21a16e6a36a09d3f3bc40216cfee5cc8b24 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Apr 2026 08:50:50 +0200 Subject: [PATCH 04/21] Bump softprops/action-gh-release from 2 to 3 (#4500) Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 2 to 3. - [Release notes](https://github.com/softprops/action-gh-release/releases) - [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md) - [Commits](https://github.com/softprops/action-gh-release/compare/v2...v3) --- updated-dependencies: - dependency-name: softprops/action-gh-release dependency-version: '3' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> (cherry picked from commit fa6254757c705ae1d1c9ef6ffbd086cd4a7eeabe) --- .github/workflows/packages.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/packages.yml b/.github/workflows/packages.yml index fe413131b3..4ef650e004 100644 --- a/.github/workflows/packages.yml +++ b/.github/workflows/packages.yml @@ -441,7 +441,7 @@ jobs: - name: Create release id: create_release - uses: softprops/action-gh-release@v2 + uses: softprops/action-gh-release@v3 with: name: Tiled ${{ needs.version.outputs.version }} draft: true From cce08a1c6bcaa95f1297e004dfbecbea09290abe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thorbj=C3=B8rn=20Lindeijer?= Date: Fri, 24 Apr 2026 11:34:09 +0200 Subject: [PATCH 05/21] Fix locale-aware parsing of numbers in expression-capable spin boxes (#4507) When the ability to evaluate expressions was added to the spin boxes, it bypassed the locale-aware number parsing normally performed by QSpinBox/QDoubleSpinBox. As a result, users in locales using a comma as decimal separator could no longer enter values like "3,14". Now the input is first parsed as a number using the widget's locale, and only falls back to expression evaluation when that fails. (cherry picked from commit fef53ef065cbcefbf49348c137f809264856b08c) --- NEWS.md | 1 + src/tiled/expressionspinbox.cpp | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/NEWS.md b/NEWS.md index 7e04dd10d5..e316e40a0b 100644 --- a/NEWS.md +++ b/NEWS.md @@ -3,6 +3,7 @@ * Reduced animated tile marker opacity during terrain editing (by Huy Vũ, #4449) * Fixed ability to change properties after deselecting the current object (#4440) * Fixed Properties view getting stuck with updates disabled (#4506) +* Fixed locale-aware parsing of numbers in expression-capable spin boxes ### Tiled 1.12.1 (25 March 2026) diff --git a/src/tiled/expressionspinbox.cpp b/src/tiled/expressionspinbox.cpp index 1526805fe8..ed2d5fbc65 100644 --- a/src/tiled/expressionspinbox.cpp +++ b/src/tiled/expressionspinbox.cpp @@ -48,6 +48,13 @@ ExpressionSpinBox::ExpressionSpinBox(QWidget *parent) int ExpressionSpinBox::valueFromText(const QString &text) const { + // First try locale-aware number parsing, so that input like "1,000" + // works as expected in locales using a thousands separator. + bool ok = false; + const int number = locale().toInt(text, &ok); + if (ok) + return number; + const QJSValue result = ExpressionEvaluator::evaluate(text); if (result.isNumber()) return result.toNumber(); @@ -94,6 +101,13 @@ ExpressionDoubleSpinBox::ExpressionDoubleSpinBox(QWidget *parent) double ExpressionDoubleSpinBox::valueFromText(const QString &text) const { + // First try locale-aware number parsing, so that input like "3,14" + // works as expected in locales using a comma as decimal separator. + bool ok = false; + const double number = locale().toDouble(text, &ok); + if (ok) + return number; + const QJSValue result = ExpressionEvaluator::evaluate(text); if (result.isNumber()) return result.toNumber(); From 3cfc24a80644f1be20e37f184cac30dd8c05f197 Mon Sep 17 00:00:00 2001 From: riebel Date: Fri, 24 Apr 2026 15:01:35 +0200 Subject: [PATCH 06/21] docs: Add pixi-tiledmap support details to documentation (#4499) Added information about pixi-tiledmap as a loader and renderer for Tiled Maps in Pixi.JS. (cherry picked from commit 1fb0747d56a314a72d97c6141d7160fd9a90d7c5) --- docs/reference/support-for-tmx-maps.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/reference/support-for-tmx-maps.rst b/docs/reference/support-for-tmx-maps.rst index cc94231357..87dd482de6 100644 --- a/docs/reference/support-for-tmx-maps.rst +++ b/docs/reference/support-for-tmx-maps.rst @@ -327,6 +327,10 @@ HTML5 (multiple engines) multi-canvas based game rendering library - `melonJS `__ A lightweight HTML5 game engine - `Panda 2 `__, a HTML5 Game Development Platform for Mac, Windows and Linux. Has `a plugin for rendering Tiled `__ maps, both orthogonal and isometric. +- `pixi-tiledmap `__ A + loader and renderer for Tiled Maps in `Pixi.JS `__ + v8+ written in TypeScript. JSON/XML support with no external deps, + full layer-type support, typed API and ESM/CJS dual output. - `Phaser `__ A fast, free and fun open source framework supporting both JavaScript and TypeScript (`Tiled tutorial `__) From a761307e2bca842b96fc3dbfae58f8d2fd39f9e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thorbj=C3=B8rn=20Lindeijer?= Date: Mon, 27 Apr 2026 17:09:26 +0200 Subject: [PATCH 07/21] Fixed Tile Animation Editor update after tileset image reload (#4508) When a tileset image was reloaded, the Tile Animation Editor didn't immediately repaint and failed to show any new tiles until switching tilesets. Now it updates immediately. Closes #3923 (cherry picked from commit 5fac4aafbcc20aa5e7637c9b99d2c01e2823988b) --- NEWS.md | 1 + src/tiled/tileanimationeditor.cpp | 12 ++++++++++++ src/tiled/tileanimationeditor.h | 1 + src/tiled/tilesetmodel.h | 2 -- 4 files changed, 14 insertions(+), 2 deletions(-) diff --git a/NEWS.md b/NEWS.md index e316e40a0b..62bc03898b 100644 --- a/NEWS.md +++ b/NEWS.md @@ -4,6 +4,7 @@ * Fixed ability to change properties after deselecting the current object (#4440) * Fixed Properties view getting stuck with updates disabled (#4506) * Fixed locale-aware parsing of numbers in expression-capable spin boxes +* Fixed Tile Animation Editor update after tileset image reload (#3923) ### Tiled 1.12.1 (25 March 2026) diff --git a/src/tiled/tileanimationeditor.cpp b/src/tiled/tileanimationeditor.cpp index 4050067cc0..fdc32ade24 100644 --- a/src/tiled/tileanimationeditor.cpp +++ b/src/tiled/tileanimationeditor.cpp @@ -30,6 +30,7 @@ #include "tiled.h" #include "tileset.h" #include "tilesetdocument.h" +#include "tilesetmanager.h" #include "utils.h" #include "zoomable.h" @@ -318,6 +319,9 @@ TileAnimationEditor::TileAnimationEditor(QWidget *parent) connect(mUi->setFrameTimeButton, &QAbstractButton::clicked, this, &TileAnimationEditor::setFrameTime); + connect(TilesetManager::instance(), &TilesetManager::tilesetImagesChanged, + this, &TileAnimationEditor::tilesetImagesChanged); + QShortcut *undoShortcut = new QShortcut(QKeySequence::Undo, this); QShortcut *redoShortcut = new QShortcut(QKeySequence::Redo, this); QShortcut *cutShortcut = new QShortcut(QKeySequence::Cut, mUi->frameList, nullptr, nullptr, Qt::WidgetShortcut); @@ -444,6 +448,14 @@ void TileAnimationEditor::tilesetChanged() model->tilesetChanged(); } +void TileAnimationEditor::tilesetImagesChanged(Tileset *tileset) +{ + if (mTilesetDocument && mTilesetDocument->tileset() == tileset) { + mUi->tilesetView->tilesetModel()->tilesetChanged(); + mUi->frameList->viewport()->update(); + } +} + void TileAnimationEditor::setDefaultFrameTime(int duration) { mFrameListModel->setDefaultFrameTime(duration); diff --git a/src/tiled/tileanimationeditor.h b/src/tiled/tileanimationeditor.h index fb6d744e67..fdeb2f8b7f 100644 --- a/src/tiled/tileanimationeditor.h +++ b/src/tiled/tileanimationeditor.h @@ -65,6 +65,7 @@ public slots: private: void framesEdited(); void tilesetChanged(); + void tilesetImagesChanged(Tileset *tileset); void tileAnimationChanged(Tile *tile); void currentObjectChanged(Object *object); diff --git a/src/tiled/tilesetmodel.h b/src/tiled/tilesetmodel.h index aa25ac4059..27f27432dd 100644 --- a/src/tiled/tilesetmodel.h +++ b/src/tiled/tilesetmodel.h @@ -62,8 +62,6 @@ class TilesetModel : public QAbstractListModel QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; - - /** * Returns a small size hint, to prevent the headers from affecting the * minimum width and height of the sections. From 2e8165ccbe3cd3d084188d34108f76eaa786862b Mon Sep 17 00:00:00 2001 From: Sid Date: Wed, 29 Apr 2026 21:24:42 +0530 Subject: [PATCH 08/21] Fixed runtime language switching in many editor widgets and models (#4411) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added missing LanguageChange / Preferences::languageChanged retranslation hooks in affected widgets, models, and action handlers, and refreshed dynamic property labels in PropertiesWidget. Closes #3443 Co-authored-by: Thorbjørn Lindeijer (cherry picked from commit c247b177a3dce6703210bb0c19828ffa6139d868) --- NEWS.md | 1 + src/tiled/brokenlinks.cpp | 40 +++++++++++++++++++++++++++--- src/tiled/brokenlinks.h | 6 +++++ src/tiled/commanddatamodel.cpp | 11 ++++++-- src/tiled/commanddatamodel.h | 2 ++ src/tiled/commandmanager.cpp | 2 -- src/tiled/commandmanager.h | 3 --- src/tiled/commandsedit.cpp | 11 ++++++++ src/tiled/commandsedit.h | 3 +++ src/tiled/filechangedwarning.cpp | 21 +++++++++++++--- src/tiled/filechangedwarning.h | 3 +++ src/tiled/fileedit.cpp | 27 +++++++++++++++----- src/tiled/fileedit.h | 4 +++ src/tiled/layerdock.cpp | 5 +++- src/tiled/layermodel.cpp | 5 ++++ src/tiled/layermodel.h | 1 + src/tiled/listedit.cpp | 18 +++++++++++++- src/tiled/listedit.h | 4 +++ src/tiled/mapobjectmodel.cpp | 5 ++++ src/tiled/mapobjectmodel.h | 1 + src/tiled/newversionbutton.cpp | 21 ++++++++++++++++ src/tiled/newversionbutton.h | 4 +++ src/tiled/objectrefedit.cpp | 19 ++++++++++++-- src/tiled/objectrefedit.h | 2 ++ src/tiled/objectsview.cpp | 5 +++- src/tiled/propertieswidget.cpp | 2 ++ src/tiled/shortcutsettingspage.cpp | 28 +++++++++++++++++++-- src/tiled/shortcutsettingspage.h | 1 + src/tiled/templatesdock.cpp | 10 ++++++++ src/tiled/templatesdock.h | 5 ++-- src/tiled/tilestampmodel.cpp | 5 ++++ src/tiled/tilestampmodel.h | 2 ++ src/tiled/tilestampsdock.cpp | 2 ++ 33 files changed, 250 insertions(+), 29 deletions(-) diff --git a/NEWS.md b/NEWS.md index 62bc03898b..d5d1bc2bbe 100644 --- a/NEWS.md +++ b/NEWS.md @@ -5,6 +5,7 @@ * Fixed Properties view getting stuck with updates disabled (#4506) * Fixed locale-aware parsing of numbers in expression-capable spin boxes * Fixed Tile Animation Editor update after tileset image reload (#3923) +* Fixed runtime language switching in many editor widgets and models (by SIDDHAARTHAA, #4411) ### Tiled 1.12.1 (25 March 2026) diff --git a/src/tiled/brokenlinks.cpp b/src/tiled/brokenlinks.cpp index ef0d8d4b9a..97ce10cecc 100644 --- a/src/tiled/brokenlinks.cpp +++ b/src/tiled/brokenlinks.cpp @@ -41,6 +41,7 @@ #include #include +#include #include #include #include @@ -101,6 +102,16 @@ BrokenLinksModel::BrokenLinksModel(QObject *parent) { } +void BrokenLinksModel::languageChanged() +{ + emit headerDataChanged(Qt::Horizontal, 0, columnCount() - 1); + + // link type column is affected by language change + const int rows = rowCount(); + if (rows > 0) + emit dataChanged(index(0, 2), index(rows - 1, 2)); +} + void BrokenLinksModel::setDocument(Document *document) { if (auto mapDocument = qobject_cast(mDocument)) { @@ -411,11 +422,9 @@ BrokenLinksWidget::BrokenLinksWidget(BrokenLinksModel *brokenLinksModel, QWidget Qt::Horizontal, this)) { - mTitleLabel->setText(tr("Some files could not be found")); - mDescriptionLabel->setText(tr("One or more referenced files could not be found. You can help locate them below.")); mDescriptionLabel->setWordWrap(true); - mLocateButton = mButtons->addButton(tr("Locate File..."), QDialogButtonBox::ActionRole); + mLocateButton = mButtons->addButton(QString(), QDialogButtonBox::ActionRole); mLocateButton->setEnabled(false); QFont font = mTitleLabel->font(); @@ -461,6 +470,29 @@ BrokenLinksWidget::BrokenLinksWidget(BrokenLinksModel *brokenLinksModel, QWidget // For some reason a model reset doesn't trigger the selectionChanged signal, // so we need to handle that explicitly. connect(brokenLinksModel, &BrokenLinksModel::modelReset, this, &BrokenLinksWidget::selectionChanged); + + retranslateUi(); +} + +void BrokenLinksWidget::changeEvent(QEvent *event) +{ + QWidget::changeEvent(event); + + if (event->type() == QEvent::LanguageChange) { + mBrokenLinksModel->languageChanged(); + retranslateUi(); + } +} + +void BrokenLinksWidget::retranslateUi() +{ + mTitleLabel->setText(tr("Some files could not be found")); + mDescriptionLabel->setText(tr("One or more referenced files could not be found. You can help locate them below.")); + + if (auto ignoreButton = mButtons->button(QDialogButtonBox::Ignore)) + ignoreButton->setText(tr("Ignore")); + + selectionChanged(); } void BrokenLinksWidget::clicked(QAbstractButton *button) @@ -512,6 +544,8 @@ void BrokenLinksWidget::selectionChanged() mLocateButton->setText(tr("Open Tileset...")); break; } + } else { + mLocateButton->setText(tr("Locate File...")); } } diff --git a/src/tiled/brokenlinks.h b/src/tiled/brokenlinks.h index 96660590bf..2d25993065 100644 --- a/src/tiled/brokenlinks.h +++ b/src/tiled/brokenlinks.h @@ -73,6 +73,8 @@ class BrokenLinksModel : public QAbstractListModel public: BrokenLinksModel(QObject *parent = nullptr); + void languageChanged(); + void setDocument(Document *document); Document *document() const; @@ -134,7 +136,11 @@ class BrokenLinksWidget : public QWidget signals: void ignore(); +protected: + void changeEvent(QEvent *event) override; + private: + void retranslateUi(); void clicked(QAbstractButton *button); void selectionChanged(); diff --git a/src/tiled/commanddatamodel.cpp b/src/tiled/commanddatamodel.cpp index 9cb055c13f..e9d9a96325 100644 --- a/src/tiled/commanddatamodel.cpp +++ b/src/tiled/commanddatamodel.cpp @@ -20,8 +20,6 @@ #include "commanddatamodel.h" -#include "preferences.h" - #include #include #include @@ -39,6 +37,15 @@ CommandDataModel::CommandDataModel(QObject *parent) { } +void CommandDataModel::languageChanged() +{ + emit headerDataChanged(Qt::Horizontal, 0, columnCount() - 1); + + const int rows = rowCount(); + if (rows > 0) + emit dataChanged(index(0, NameColumn), index(rows - 1, ShortcutColumn)); +} + void CommandDataModel::setCommands(const QVector &commands) { beginResetModel(); diff --git a/src/tiled/commanddatamodel.h b/src/tiled/commanddatamodel.h index 519d9c686e..918388b46e 100644 --- a/src/tiled/commanddatamodel.h +++ b/src/tiled/commanddatamodel.h @@ -38,6 +38,8 @@ class CommandDataModel : public QAbstractTableModel CommandDataModel(QObject *parent = nullptr); + void languageChanged(); + void setCommands(const QVector &commands); const QVector &commands() const; diff --git a/src/tiled/commandmanager.cpp b/src/tiled/commandmanager.cpp index 3625a171e0..6e56c94d7d 100644 --- a/src/tiled/commandmanager.cpp +++ b/src/tiled/commandmanager.cpp @@ -21,7 +21,6 @@ #include "commandmanager.h" -#include "commanddatamodel.h" #include "commanddialog.h" #include "logginginterface.h" #include "pluginmanager.h" @@ -37,7 +36,6 @@ namespace Tiled { CommandManager::CommandManager() - : mModel(new CommandDataModel(this)) { auto preferences = Preferences::instance(); diff --git a/src/tiled/commandmanager.h b/src/tiled/commandmanager.h index 65abcaf238..aee0b659d5 100644 --- a/src/tiled/commandmanager.h +++ b/src/tiled/commandmanager.h @@ -31,8 +31,6 @@ class QMenu; namespace Tiled { -class CommandDataModel; - class CommandManager : public QObject { Q_OBJECT @@ -63,7 +61,6 @@ public slots: void updateActions(); - CommandDataModel *mModel; QVector mCommands; QList mMenus; QList mActions; diff --git a/src/tiled/commandsedit.cpp b/src/tiled/commandsedit.cpp index d8c6317513..622a25b951 100644 --- a/src/tiled/commandsedit.cpp +++ b/src/tiled/commandsedit.cpp @@ -25,6 +25,7 @@ #include "session.h" #include "utils.h" +#include #include #include @@ -83,6 +84,16 @@ CommandsEdit::~CommandsEdit() delete mUi; } +void CommandsEdit::changeEvent(QEvent *event) +{ + QWidget::changeEvent(event); + + if (event->type() == QEvent::LanguageChange) { + mUi->retranslateUi(this); + mModel->languageChanged(); + } +} + const QVector &CommandsEdit::commands() const { return mModel->commands(); diff --git a/src/tiled/commandsedit.h b/src/tiled/commandsedit.h index 2bafff9b58..d8c92ff688 100644 --- a/src/tiled/commandsedit.h +++ b/src/tiled/commandsedit.h @@ -55,6 +55,9 @@ public slots: void browseExecutable(); void browseWorkingDirectory(); +protected: + void changeEvent(QEvent *event) override; + private: Ui::CommandsEdit *mUi; CommandDataModel *mModel; diff --git a/src/tiled/filechangedwarning.cpp b/src/tiled/filechangedwarning.cpp index eccd0e2940..8e4d2d2803 100644 --- a/src/tiled/filechangedwarning.cpp +++ b/src/tiled/filechangedwarning.cpp @@ -21,6 +21,7 @@ #include "filechangedwarning.h" #include +#include #include #include #include @@ -36,21 +37,33 @@ FileChangedWarning::FileChangedWarning(QWidget *parent) Qt::Horizontal, this)) { - mLabel->setText(tr("File change detected. Discard changes and reload the file?")); - QHBoxLayout *layout = new QHBoxLayout; layout->addWidget(mLabel); layout->addWidget(mButtons); layout->addStretch(1); setLayout(layout); - mButtons->button(QDialogButtonBox::Yes)->setText(tr("Reload")); - mButtons->button(QDialogButtonBox::No)->setText(tr("Ignore")); + retranslateUi(); connect(mButtons, &QDialogButtonBox::accepted, this, &FileChangedWarning::reload); connect(mButtons, &QDialogButtonBox::rejected, this, &FileChangedWarning::ignore); } +void FileChangedWarning::changeEvent(QEvent *event) +{ + QWidget::changeEvent(event); + + if (event->type() == QEvent::LanguageChange) + retranslateUi(); +} + +void FileChangedWarning::retranslateUi() +{ + mLabel->setText(tr("File change detected. Discard changes and reload the file?")); + mButtons->button(QDialogButtonBox::Yes)->setText(tr("Reload")); + mButtons->button(QDialogButtonBox::No)->setText(tr("Ignore")); +} + void FileChangedWarning::paintEvent(QPaintEvent *event) { QWidget::paintEvent(event); diff --git a/src/tiled/filechangedwarning.h b/src/tiled/filechangedwarning.h index 4f055002af..40cd6c481f 100644 --- a/src/tiled/filechangedwarning.h +++ b/src/tiled/filechangedwarning.h @@ -39,9 +39,12 @@ class FileChangedWarning : public QWidget void ignore(); protected: + void changeEvent(QEvent *event) override; void paintEvent(QPaintEvent *event) override; private: + void retranslateUi(); + QLabel *mLabel; QDialogButtonBox *mButtons; }; diff --git a/src/tiled/fileedit.cpp b/src/tiled/fileedit.cpp index 5e1b09e266..0442c069a5 100644 --- a/src/tiled/fileedit.cpp +++ b/src/tiled/fileedit.cpp @@ -23,6 +23,7 @@ #include "tiled.h" +#include #include #include #include @@ -33,6 +34,7 @@ namespace Tiled { FileEdit::FileEdit(QWidget *parent) : QWidget(parent) + , mButton(new QToolButton(this)) , mErrorTextColor(Qt::red) { QHBoxLayout *layout = new QHBoxLayout(this); @@ -44,12 +46,10 @@ FileEdit::FileEdit(QWidget *parent) mOkTextColor = mLineEdit->palette().color(QPalette::Active, QPalette::Text); - QToolButton *button = new QToolButton(this); - button->setText(QStringLiteral("…")); - button->setAutoRaise(true); - button->setToolTip(tr("Choose")); + mButton->setText(QStringLiteral("…")); + mButton->setAutoRaise(true); layout->addWidget(mLineEdit); - layout->addWidget(button); + layout->addWidget(mButton); setFocusProxy(mLineEdit); setFocusPolicy(Qt::StrongFocus); @@ -59,8 +59,23 @@ FileEdit::FileEdit(QWidget *parent) this, &FileEdit::textEdited); connect(mLineEdit, &QLineEdit::textChanged, this, &FileEdit::validate); - connect(button, &QAbstractButton::clicked, + connect(mButton, &QAbstractButton::clicked, this, &FileEdit::buttonClicked); + + retranslateUi(); +} + +void FileEdit::changeEvent(QEvent *event) +{ + QWidget::changeEvent(event); + + if (event->type() == QEvent::LanguageChange) + retranslateUi(); +} + +void FileEdit::retranslateUi() +{ + mButton->setToolTip(tr("Choose")); } void FileEdit::setFileUrl(const QUrl &url) diff --git a/src/tiled/fileedit.h b/src/tiled/fileedit.h index 7fcd006f3f..24bc5ba66b 100644 --- a/src/tiled/fileedit.h +++ b/src/tiled/fileedit.h @@ -25,6 +25,7 @@ #include class QLineEdit; +class QToolButton; namespace Tiled { @@ -59,17 +60,20 @@ class FileEdit : public QWidget void fileUrlChanged(const QUrl &url); protected: + void changeEvent(QEvent *event) override; void focusInEvent(QFocusEvent *e) override; void focusOutEvent(QFocusEvent *e) override; void keyPressEvent(QKeyEvent *e) override; void keyReleaseEvent(QKeyEvent *e) override; private: + void retranslateUi(); void textEdited(); void validate(); void buttonClicked(); QLineEdit *mLineEdit; + QToolButton *mButton; QString mFilter; bool mIsDirectory = false; QColor mOkTextColor; diff --git a/src/tiled/layerdock.cpp b/src/tiled/layerdock.cpp index f6dd6b3abc..4a04f8b135 100644 --- a/src/tiled/layerdock.cpp +++ b/src/tiled/layerdock.cpp @@ -354,7 +354,10 @@ void LayerView::layerRemoved(Layer *layer) bool LayerView::event(QEvent *event) { - if (event->type() == QEvent::ShortcutOverride) { + if (event->type() == QEvent::LanguageChange) { + if (mMapDocument) + mMapDocument->layerModel()->languageChanged(); + } else if (event->type() == QEvent::ShortcutOverride) { if (static_cast(event)->key() == Qt::Key_Tab) { if (indexWidget(currentIndex())) { event->accept(); diff --git a/src/tiled/layermodel.cpp b/src/tiled/layermodel.cpp index 0607add6ed..b74eeeb7d8 100644 --- a/src/tiled/layermodel.cpp +++ b/src/tiled/layermodel.cpp @@ -46,6 +46,11 @@ LayerModel::LayerModel(QObject *parent): mObjectGroupIcon.addFile(QLatin1String(":images/32/layer-object.png")); } +void LayerModel::languageChanged() +{ + emit headerDataChanged(Qt::Horizontal, 0, columnCount() - 1); +} + QModelIndex LayerModel::index(int row, int column, const QModelIndex &parent) const { // Top-level layer index diff --git a/src/tiled/layermodel.h b/src/tiled/layermodel.h index 8b837be5cc..483d3f6583 100644 --- a/src/tiled/layermodel.h +++ b/src/tiled/layermodel.h @@ -79,6 +79,7 @@ class LayerModel : public QAbstractItemModel MapDocument *mapDocument() const; void setMapDocument(MapDocument *mapDocument); + void languageChanged(); void insertLayer(GroupLayer *parentLayer, int index, Layer *layer); Layer *takeLayerAt(GroupLayer *parentLayer, int index); diff --git a/src/tiled/listedit.cpp b/src/tiled/listedit.cpp index 94fd6d0f45..b81f598499 100644 --- a/src/tiled/listedit.cpp +++ b/src/tiled/listedit.cpp @@ -24,6 +24,7 @@ #include "propertytypesmodel.h" #include "utils.h" +#include #include #include #include @@ -45,7 +46,6 @@ ListEdit::ListEdit(QWidget *parent) mAddButton = new QToolButton{this}; mAddButton->setIcon(QIcon(QStringLiteral(":/images/22/add.png"))); - mAddButton->setText(tr("Add")); mAddButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); mAddButton->setMenu(mAddMenu); mAddButton->setPopupMode(QToolButton::MenuButtonPopup); @@ -65,6 +65,8 @@ ListEdit::ListEdit(QWidget *parent) connect(mAddMenu, &QMenu::triggered, this, [this](QAction *action) { emit appendValue(action->data()); }); + + retranslateUi(); } void ListEdit::setValue(const QVariantList &value) @@ -79,6 +81,20 @@ QString ListEdit::valueText(const QVariantList &value) : tr("%1 items").arg(value.count()); } +void ListEdit::changeEvent(QEvent *event) +{ + QWidget::changeEvent(event); + + if (event->type() == QEvent::LanguageChange) + retranslateUi(); +} + +void ListEdit::retranslateUi() +{ + mAddButton->setText(tr("Add")); + mLabel->setText(valueText(mValue)); +} + void ListEdit::addButtonClicked() { if (mValue.isEmpty()) { diff --git a/src/tiled/listedit.h b/src/tiled/listedit.h index a379b4a4c2..05e86582c5 100644 --- a/src/tiled/listedit.h +++ b/src/tiled/listedit.h @@ -50,7 +50,11 @@ class ListEdit final : public QWidget signals: void appendValue(const QVariant &value); +protected: + void changeEvent(QEvent *event) override; + private: + void retranslateUi(); void addButtonClicked(); void populateAddMenu(); diff --git a/src/tiled/mapobjectmodel.cpp b/src/tiled/mapobjectmodel.cpp index 4d06a54ba8..a14d658586 100644 --- a/src/tiled/mapobjectmodel.cpp +++ b/src/tiled/mapobjectmodel.cpp @@ -83,6 +83,11 @@ MapObjectModel::MapObjectModel(QObject *parent) mObjectGroupIcon.addFile(QLatin1String(":images/32/layer-object.png")); } +void MapObjectModel::languageChanged() +{ + emit headerDataChanged(Qt::Horizontal, 0, ColumnCount - 1); +} + QModelIndex MapObjectModel::index(int row, int column, const QModelIndex &parent) const { diff --git a/src/tiled/mapobjectmodel.h b/src/tiled/mapobjectmodel.h index 9abe7fa2c5..20f3e85976 100644 --- a/src/tiled/mapobjectmodel.h +++ b/src/tiled/mapobjectmodel.h @@ -106,6 +106,7 @@ class MapObjectModel : public QAbstractItemModel void setMapDocument(MapDocument *mapDocument); MapDocument *mapDocument() const { return mMapDocument; } + void languageChanged(); void moveObjects(ObjectGroup *og, int from, int to, int count); diff --git a/src/tiled/newversionbutton.cpp b/src/tiled/newversionbutton.cpp index 2c7e916d19..f4e77f6eeb 100644 --- a/src/tiled/newversionbutton.cpp +++ b/src/tiled/newversionbutton.cpp @@ -25,6 +25,7 @@ #include "tiledapplication.h" #include "utils.h" +#include #include namespace Tiled { @@ -67,6 +68,26 @@ NewVersionButton::NewVersionButton(NewVersionButton::Visibility visibility, QWid }); } +void NewVersionButton::changeEvent(QEvent *event) +{ + QToolButton::changeEvent(event); + + if (event->type() == QEvent::LanguageChange) + retranslateUi(); +} + +void NewVersionButton::retranslateUi() +{ + const auto &checker = tiledApp()->newVersionChecker(); + + if (checker.isNewVersionAvailable()) + newVersionAvailable(checker.versionInfo()); + else if (!checker.errorString().isEmpty()) + errorStringChanged(checker.errorString()); + else + setText(tr("Up to date")); +} + void NewVersionButton::newVersionAvailable(const NewVersionChecker::VersionInfo &versionInfo) { setIcon(QIcon(QLatin1String("://images/scalable/software-update-available-symbolic.svg"))); diff --git a/src/tiled/newversionbutton.h b/src/tiled/newversionbutton.h index 46a2f968e9..59a346788f 100644 --- a/src/tiled/newversionbutton.h +++ b/src/tiled/newversionbutton.h @@ -39,7 +39,11 @@ class NewVersionButton : public QToolButton explicit NewVersionButton(QWidget *parent = nullptr); explicit NewVersionButton(Visibility visibility, QWidget *parent = nullptr); +protected: + void changeEvent(QEvent *event) override; + private: + void retranslateUi(); void newVersionAvailable(const NewVersionChecker::VersionInfo &versionInfo); void errorStringChanged(const QString &errorString); diff --git a/src/tiled/objectrefedit.cpp b/src/tiled/objectrefedit.cpp index f26a7060ce..c47327dafa 100644 --- a/src/tiled/objectrefedit.cpp +++ b/src/tiled/objectrefedit.cpp @@ -25,6 +25,7 @@ #include "mapobject.h" #include "utils.h" +#include #include #include #include @@ -50,7 +51,6 @@ ObjectRefEdit::ObjectRefEdit(QWidget *parent) mObjectDialogButton->setEnabled(false); mObjectDialogButton->setIconSize(Utils::smallIconSize()); mObjectDialogButton->setIcon(QIcon(QStringLiteral("://images/scalable/search-object-dialog.svg"))); - mObjectDialogButton->setToolTip(tr("Search Object")); mPickObjectButton->setText(QStringLiteral("Pick")); mPickObjectButton->setAutoRaise(true); @@ -59,7 +59,6 @@ ObjectRefEdit::ObjectRefEdit(QWidget *parent) mPickObjectButton->setFocusPolicy(Qt::StrongFocus); mPickObjectButton->setIconSize(Utils::smallIconSize()); mPickObjectButton->setIcon(QIcon(QStringLiteral("://images/scalable/select-object.svg"))); - mPickObjectButton->setToolTip(tr("Select Object on Map")); layout->setContentsMargins(0, 0, 0, 0); layout->setSpacing(0); @@ -72,6 +71,8 @@ ObjectRefEdit::ObjectRefEdit(QWidget *parent) connect(mObjectDialogButton, &QToolButton::clicked, this, &ObjectRefEdit::openObjectRefDialog); connect(mPickObjectButton, &QToolButton::toggled, this, &ObjectRefEdit::pickObjectOnMap); connect(mLineEdit, &QLineEdit::editingFinished, this, &ObjectRefEdit::onEditFinished); + + retranslateUi(); } ObjectRefEdit::~ObjectRefEdit() @@ -112,6 +113,20 @@ void ObjectRefEdit::keyPressEvent(QKeyEvent *event) QWidget::keyPressEvent(event); } +void ObjectRefEdit::changeEvent(QEvent *event) +{ + QWidget::changeEvent(event); + + if (event->type() == QEvent::LanguageChange) + retranslateUi(); +} + +void ObjectRefEdit::retranslateUi() +{ + mObjectDialogButton->setToolTip(tr("Search Object")); + mPickObjectButton->setToolTip(tr("Select Object on Map")); +} + void ObjectRefEdit::openObjectRefDialog() { if (!mValue.mapDocument) diff --git a/src/tiled/objectrefedit.h b/src/tiled/objectrefedit.h index 628fc640d0..eccaa3a4c9 100644 --- a/src/tiled/objectrefedit.h +++ b/src/tiled/objectrefedit.h @@ -44,9 +44,11 @@ class ObjectRefEdit : public QWidget void valueChanged(const DisplayObjectRef &value); protected: + void changeEvent(QEvent *event) override; void keyPressEvent(QKeyEvent *event) override; private: + void retranslateUi(); void openObjectRefDialog(); void pickObjectOnMap(bool pick); diff --git a/src/tiled/objectsview.cpp b/src/tiled/objectsview.cpp index a5760a0a29..1c3bbf67d7 100644 --- a/src/tiled/objectsview.cpp +++ b/src/tiled/objectsview.cpp @@ -185,7 +185,10 @@ void ObjectsView::restoreExpandedLayers() bool ObjectsView::event(QEvent *event) { - if (event->type() == QEvent::ShortcutOverride) { + if (event->type() == QEvent::LanguageChange) { + if (MapObjectModel *model = mapObjectModel()) + model->languageChanged(); + } else if (event->type() == QEvent::ShortcutOverride) { if (static_cast(event)->key() == Qt::Key_Tab) { if (indexWidget(currentIndex())) { event->accept(); diff --git a/src/tiled/propertieswidget.cpp b/src/tiled/propertieswidget.cpp index 93b1d6fabb..32c75f1ed7 100644 --- a/src/tiled/propertieswidget.cpp +++ b/src/tiled/propertieswidget.cpp @@ -3155,6 +3155,8 @@ void PropertiesWidget::keyPressEvent(QKeyEvent *event) void PropertiesWidget::retranslateUi() { + mCustomProperties->setName(QCoreApplication::translate("Tiled::CustomProperties", "Custom Properties")); + mActionAddProperty->setText(QCoreApplication::translate("Tiled::PropertiesDock", "Add Property")); mActionRemoveProperty->setText(QCoreApplication::translate("Tiled::PropertiesDock", "Remove")); diff --git a/src/tiled/shortcutsettingspage.cpp b/src/tiled/shortcutsettingspage.cpp index 0c6f746bb4..c6131087eb 100644 --- a/src/tiled/shortcutsettingspage.cpp +++ b/src/tiled/shortcutsettingspage.cpp @@ -31,6 +31,7 @@ #include #include #include +#include #include #include #include @@ -64,9 +65,10 @@ class ActionsModel : public QAbstractListModel explicit ActionsModel(QObject *parent = nullptr); void setVisible(bool visible); + void languageChanged(); - int rowCount(const QModelIndex &parent) const override; - int columnCount(const QModelIndex &parent) const override; + int rowCount(const QModelIndex &parent = {}) const override; + int columnCount(const QModelIndex &parent = {}) const override; QVariant data(const QModelIndex &index, int role) const override; bool setData(const QModelIndex &index, const QVariant &value, int role) override; Qt::ItemFlags flags(const QModelIndex &index) const override; @@ -103,6 +105,18 @@ void ActionsModel::setVisible(bool visible) refresh(); } +void ActionsModel::languageChanged() +{ + const int rows = rowCount(); + const int columns = columnCount(); + + emit headerDataChanged(Qt::Horizontal, 0, columns - 1); + + // name and shortcut might be affected by language change + if (rows > 0) + emit dataChanged(index(0, 1), index(rows - 1, columns - 1)); +} + void ActionsModel::refresh() { if (!mVisible) @@ -640,6 +654,16 @@ QSize ShortcutSettingsPage::sizeHint() const return size; } +void ShortcutSettingsPage::changeEvent(QEvent *event) +{ + QWidget::changeEvent(event); + + if (event->type() == QEvent::LanguageChange) { + ui->retranslateUi(this); + mActionsModel->languageChanged(); + } +} + void ShortcutSettingsPage::showEvent(QShowEvent *event) { mActionsModel->setVisible(true); diff --git a/src/tiled/shortcutsettingspage.h b/src/tiled/shortcutsettingspage.h index 458406d259..a5d63e48e8 100644 --- a/src/tiled/shortcutsettingspage.h +++ b/src/tiled/shortcutsettingspage.h @@ -44,6 +44,7 @@ class ShortcutSettingsPage : public QWidget QSize sizeHint() const override; protected: + void changeEvent(QEvent *event) override; void showEvent(QShowEvent *event) override; void hideEvent(QHideEvent *event) override; diff --git a/src/tiled/templatesdock.cpp b/src/tiled/templatesdock.cpp index 8a6ea65b45..45beae0454 100644 --- a/src/tiled/templatesdock.cpp +++ b/src/tiled/templatesdock.cpp @@ -39,6 +39,7 @@ #include #include +#include #include #include #include @@ -154,6 +155,14 @@ TemplatesDock::~TemplatesDock() mDummyMapDocument->undoStack()->disconnect(this); } +void TemplatesDock::changeEvent(QEvent *event) +{ + QDockWidget::changeEvent(event); + + if (event->type() == QEvent::LanguageChange) + retranslateUi(); +} + void TemplatesDock::setTile(Tile *tile) { mToolManager->setTile(tile); @@ -381,6 +390,7 @@ void TemplatesDock::focusInEvent(QFocusEvent *event) void TemplatesDock::retranslateUi() { setWindowTitle(tr("Template Editor")); + checkTileset(); } void TemplatesDock::fixTileset() diff --git a/src/tiled/templatesdock.h b/src/tiled/templatesdock.h index fc9fb6c748..da90bc3cbd 100644 --- a/src/tiled/templatesdock.h +++ b/src/tiled/templatesdock.h @@ -64,6 +64,7 @@ public slots: void bringToFront(); protected: + void changeEvent(QEvent *event) override; void dragEnterEvent(QDragEnterEvent *event) override; void dropEvent(QDropEvent *event) override; @@ -93,8 +94,8 @@ public slots: MapDocumentPtr mDummyMapDocument; MapScene *mMapScene; MapView *mMapView; - ObjectTemplate *mObjectTemplate; - PropertiesDock *mPropertiesDock; + ObjectTemplate *mObjectTemplate = nullptr; + PropertiesDock *mPropertiesDock = nullptr; ToolManager *mToolManager; static QHash> ourDummyDocuments; diff --git a/src/tiled/tilestampmodel.cpp b/src/tiled/tilestampmodel.cpp index add132ba27..609795b70a 100644 --- a/src/tiled/tilestampmodel.cpp +++ b/src/tiled/tilestampmodel.cpp @@ -29,6 +29,11 @@ TileStampModel::TileStampModel(QObject *parent) { } +void TileStampModel::languageChanged() +{ + emit headerDataChanged(Qt::Horizontal, 0, columnCount() - 1); +} + QModelIndex TileStampModel::index(int row, int column, const QModelIndex &parent) const { if (!hasIndex(row, column, parent)) diff --git a/src/tiled/tilestampmodel.h b/src/tiled/tilestampmodel.h index 0a0d005a18..f74484e407 100644 --- a/src/tiled/tilestampmodel.h +++ b/src/tiled/tilestampmodel.h @@ -38,6 +38,8 @@ class TileStampModel : public QAbstractItemModel public: TileStampModel(QObject *parent = nullptr); + void languageChanged(); + QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override; QModelIndex index(const TileStamp &stamp) const; diff --git a/src/tiled/tilestampsdock.cpp b/src/tiled/tilestampsdock.cpp index afce999b95..e19a3bda67 100644 --- a/src/tiled/tilestampsdock.cpp +++ b/src/tiled/tilestampsdock.cpp @@ -292,6 +292,8 @@ void TileStampsDock::ensureStampVisible(const TileStamp &stamp) void TileStampsDock::retranslateUi() { + mTileStampModel->languageChanged(); + setWindowTitle(tr("Tile Stamps")); mNewStamp->setText(tr("Add New Stamp")); From c50db4f1d2ed5020aacf208842bbe0a627a05c86 Mon Sep 17 00:00:00 2001 From: Mollah Hamza <142751532+MollahHamza@users.noreply.github.com> Date: Thu, 30 Apr 2026 14:14:03 +0600 Subject: [PATCH 09/21] Fix point object bounding box in multi-object selections (#4401) The objectBounds() function was using renderer->shape(object) for Point objects, which returns the visual pin shape rather than the actual point position. This caused the selection bounding box to be larger than expected, affecting snapping and rotation behavior. Fix by returning a zero-size rect at the point's screen position, consistent with how Point objects are treated geometrically. Fixes #3784 (cherry picked from commit 71441557e68de5b67b5853a6f6ab272d5f7805c3) --- NEWS.md | 1 + src/tiled/objectselectiontool.cpp | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/NEWS.md b/NEWS.md index d5d1bc2bbe..6ed308ac50 100644 --- a/NEWS.md +++ b/NEWS.md @@ -6,6 +6,7 @@ * Fixed locale-aware parsing of numbers in expression-capable spin boxes * Fixed Tile Animation Editor update after tileset image reload (#3923) * Fixed runtime language switching in many editor widgets and models (by SIDDHAARTHAA, #4411) +* Fixed multi-object selection bounding box for point objects (by Mollah Hamza, #4401) ### Tiled 1.12.1 (25 March 2026) diff --git a/src/tiled/objectselectiontool.cpp b/src/tiled/objectselectiontool.cpp index d2d53dbb67..b6b3383f7f 100644 --- a/src/tiled/objectselectiontool.cpp +++ b/src/tiled/objectselectiontool.cpp @@ -928,7 +928,8 @@ static QRectF objectBounds(const MapObject *object, return transform.map(screenPolygon).boundingRect(); } case MapObject::Point: { - return transform.mapRect(renderer->shape(object).boundingRect()); + const QPointF pos = renderer->pixelToScreenCoords(object->position()); + return transform.mapRect(QRectF(pos, QSizeF(0, 0))); } case MapObject::Polygon: case MapObject::Polyline: { From 8284b458ed2ae3c86a5af896dccdd0084b7f3745 Mon Sep 17 00:00:00 2001 From: Praagya garg Date: Thu, 30 Apr 2026 16:51:19 +0530 Subject: [PATCH 10/21] Added whitespace trimming for custom property names (#4486) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This also prevents whitespaces-only property names. Co-authored-by: Thorbjørn Lindeijer (cherry picked from commit 725993a74a1fc51096afafdfedb706b2f04fa4f2) --- NEWS.md | 1 + src/tiled/propertieswidget.cpp | 3 ++- src/tiled/variantmapproperty.cpp | 4 +++- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/NEWS.md b/NEWS.md index 6ed308ac50..13d0d97162 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,6 @@ ### Unreleased +* Added whitespace trimming for custom property names (with Praagya26, #4486) * Reduced animated tile marker opacity during terrain editing (by Huy Vũ, #4449) * Fixed ability to change properties after deselecting the current object (#4440) * Fixed Properties view getting stuck with updates disabled (#4506) diff --git a/src/tiled/propertieswidget.cpp b/src/tiled/propertieswidget.cpp index 32c75f1ed7..48f8c2e14e 100644 --- a/src/tiled/propertieswidget.cpp +++ b/src/tiled/propertieswidget.cpp @@ -2904,7 +2904,8 @@ void PropertiesWidget::renameProperty(const QString &name) dialog->setTextValue(name); dialog->setWindowTitle(QCoreApplication::translate("Tiled::PropertiesDock", "Rename Property")); - connect(dialog, &QInputDialog::textValueSelected, this, [=] (const QString &newName) { + connect(dialog, &QInputDialog::textValueSelected, this, [=] (const QString &text) { + const QString newName = text.trimmed(); if (newName.isEmpty()) return; if (newName == name) diff --git a/src/tiled/variantmapproperty.cpp b/src/tiled/variantmapproperty.cpp index 73dfa7d102..c6cf521f28 100644 --- a/src/tiled/variantmapproperty.cpp +++ b/src/tiled/variantmapproperty.cpp @@ -755,7 +755,9 @@ QWidget *AddValueProperty::createLabel(int level, QWidget *parent) nameEdit->setContentsMargins(margins); - connect(nameEdit, &QLineEdit::textChanged, this, &Property::setName); + connect(nameEdit, &QLineEdit::textChanged, this, [this](const QString &name) { + setName(name.trimmed()); + }); connect(nameEdit, &QLineEdit::returnPressed, this, [this] { if (!name().isEmpty()) emit addRequested(); From 3bba74e8421ae1d01545b8abb2dd6e70f550c6b3 Mon Sep 17 00:00:00 2001 From: PoonamMehan <116332299+PoonamMehan@users.noreply.github.com> Date: Thu, 30 Apr 2026 17:11:13 +0530 Subject: [PATCH 11/21] fix(ci): replace third-party action with native brew bump-cask-pr command (#4478) The `eugenesvk/action-homebrew-bump-cask` action had failed on the first test, see: https://github.com/mapeditor/tiled/pull/4391#issuecomment-4056400362 (cherry picked from commit e48776ac0a156340dac3e7f3c610ebd164c9b6fb) --- .github/workflows/merge-request-homebrew.yml | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/.github/workflows/merge-request-homebrew.yml b/.github/workflows/merge-request-homebrew.yml index 1e58a5cbe6..672b19ec96 100644 --- a/.github/workflows/merge-request-homebrew.yml +++ b/.github/workflows/merge-request-homebrew.yml @@ -6,7 +6,7 @@ on: workflow_dispatch: inputs: tag: - description: 'Release tag to bump (e.g. v1.11.2). Defaults to latest release.' + description: 'Release tag to bump (e.g. v1.11.2). Defaults to the triggering release tag.' required: false jobs: @@ -18,9 +18,12 @@ jobs: steps: - name: Update Homebrew cask - uses: eugenesvk/action-homebrew-bump-cask@6b066272df85c164f0496877387ecc8d989a194f # 3.8.6 - with: - token: ${{ secrets.HOMEBREW_BUMP_TOKEN }} - cask: tiled - org: mapeditor - tag: ${{ inputs.tag || github.ref }} + env: + HOMEBREW_GITHUB_API_TOKEN: ${{ secrets.HOMEBREW_BUMP_TOKEN }} + run: | + TAG="${{ inputs.tag || github.ref_name }}" + VERSION="${TAG#v}" + if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+ ]]; then + echo "Error: '$VERSION' is not a valid version. Provide a tag like v1.11.2." && exit 1 + fi + brew bump-cask-pr --version "$VERSION" --fork-org mapeditor --no-browse tiled From 89807f2b3a498c0535965dc26d2efebacd941eca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thorbj=C3=B8rn=20Lindeijer?= Date: Thu, 30 Apr 2026 18:01:02 +0200 Subject: [PATCH 12/21] Made indexes in the TilesetModel without tiles invalid (#4512) This way, those indexes can't be navigated to using the keyboard nor selected using the mouse. I've also added rendering of the focused tile in TileDelegate, which helps a little bit to give additional context in some cases like multi-selection or when toggling selection. Closes #3498 (cherry picked from commit 77fc9a259c10584a790c5e074a0708f5631e1975) --- NEWS.md | 1 + src/tiled/tilesetmodel.cpp | 11 +++++++++++ src/tiled/tilesetmodel.h | 2 ++ src/tiled/tilesetview.cpp | 23 +++++++++++++++++++---- 4 files changed, 33 insertions(+), 4 deletions(-) diff --git a/NEWS.md b/NEWS.md index 13d0d97162..e393a464cd 100644 --- a/NEWS.md +++ b/NEWS.md @@ -8,6 +8,7 @@ * Fixed Tile Animation Editor update after tileset image reload (#3923) * Fixed runtime language switching in many editor widgets and models (by SIDDHAARTHAA, #4411) * Fixed multi-object selection bounding box for point objects (by Mollah Hamza, #4401) +* Fixed non-tile locations in wrapping Tilesets view being selectable (#3498) ### Tiled 1.12.1 (25 March 2026) diff --git a/src/tiled/tilesetmodel.cpp b/src/tiled/tilesetmodel.cpp index 32f68c7961..749463f670 100644 --- a/src/tiled/tilesetmodel.cpp +++ b/src/tiled/tilesetmodel.cpp @@ -44,6 +44,17 @@ TilesetModel::TilesetModel(TilesetDocument *tilesetDocument, QObject *parent) this, &TilesetModel::tileChanged); } +/** + * Override to return an invalid index if the tile index is out of range. + */ +QModelIndex TilesetModel::index(int row, int column, const QModelIndex &parent) const +{ + const int tileIndex = column + row * columnCount(); + if (tileIndex >= mTileIds.size()) + return QModelIndex(); + return QAbstractListModel::index(row, column, parent); +} + int TilesetModel::rowCount(const QModelIndex &parent) const { if (parent.isValid()) diff --git a/src/tiled/tilesetmodel.h b/src/tiled/tilesetmodel.h index 27f27432dd..fd8c200ef6 100644 --- a/src/tiled/tilesetmodel.h +++ b/src/tiled/tilesetmodel.h @@ -45,6 +45,8 @@ class TilesetModel : public QAbstractListModel */ TilesetModel(TilesetDocument *tilesetDocument, QObject *parent = nullptr); + QModelIndex index(int row, int column = 0, const QModelIndex &parent = {}) const override; + /** * Returns the number of rows. */ diff --git a/src/tiled/tilesetview.cpp b/src/tiled/tilesetview.cpp index a6cb444361..d3a91a1005 100644 --- a/src/tiled/tilesetview.cpp +++ b/src/tiled/tilesetview.cpp @@ -111,8 +111,8 @@ void TileDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { - const TilesetModel *model = static_cast(index.model()); - const Tile *tile = model->tileAt(index); + auto model = static_cast(index.model()); + auto tile = model->tileAt(index); if (!tile) return; @@ -177,12 +177,27 @@ void TileDelegate::paint(QPainter *painter, if (mTilesetView->isEditWangSet()) drawWangOverlay(painter, tile, targetRect, index); + + // draw the focus rect + if (option.state & QStyle::State_HasFocus) { + auto style = option.widget->style(); + QStyleOptionFocusRect o; + o.QStyleOption::operator=(option); + o.rect = style->subElementRect(QStyle::SE_ItemViewItemFocusRect, &option); + o.state |= QStyle::State_KeyboardFocusChange; + o.state |= QStyle::State_Item; + QPalette::ColorGroup cg = (option.state & QStyle::State_Enabled) + ? QPalette::Normal : QPalette::Disabled; + o.backgroundColor = option.palette.color(cg, (option.state & QStyle::State_Selected) + ? QPalette::Highlight : QPalette::Window); + style->drawPrimitive(QStyle::PE_FrameFocusRect, &o, painter); + } } QSize TileDelegate::sizeHint(const QStyleOptionViewItem & /* option */, const QModelIndex &index) const { - const TilesetModel *m = static_cast(index.model()); + auto m = static_cast(index.model()); const int extra = mTilesetView->drawGrid() ? 1 : 0; const qreal scale = mTilesetView->scale(); @@ -444,7 +459,7 @@ void TilesetView::setMarkAnimatedTiles(bool enabled) bool TilesetView::event(QEvent *event) { if (event->type() == QEvent::Gesture) { - QGestureEvent *gestureEvent = static_cast(event); + auto gestureEvent = static_cast(event); if (QGesture *gesture = gestureEvent->gesture(Qt::PinchGesture)) mZoomable->handlePinchGesture(static_cast(gesture)); } else if (event->type() == QEvent::ShortcutOverride) { From b9ff20d6722456398aa5ec7a9adc2afd634d0f68 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 May 2026 07:03:32 +0200 Subject: [PATCH 13/21] Bump timheuer/base64-to-file from 1 to 2 (#4516) Bumps [timheuer/base64-to-file](https://github.com/timheuer/base64-to-file) from 1 to 2. - [Release notes](https://github.com/timheuer/base64-to-file/releases) - [Commits](https://github.com/timheuer/base64-to-file/compare/v1...v2) --- updated-dependencies: - dependency-name: timheuer/base64-to-file dependency-version: '2' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> (cherry picked from commit f511930c1b6f816b3ec5a98cd4e740205f4fcf2e) --- .github/workflows/packages.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/packages.yml b/.github/workflows/packages.yml index 4ef650e004..4dbfb53930 100644 --- a/.github/workflows/packages.yml +++ b/.github/workflows/packages.yml @@ -280,7 +280,7 @@ jobs: - name: Create Certificate File id: certificateFile if: env.SIGNING_ENABLED == 'true' - uses: timheuer/base64-to-file@v1 + uses: timheuer/base64-to-file@v2 with: fileName: 'certificate.p12' encodedString: ${{ secrets.MACOS_CERTIFICATE }} From 15240803e8635f63732725e1220653b8555167bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thorbj=C3=B8rn=20Lindeijer?= Date: Tue, 19 May 2026 17:09:00 +0200 Subject: [PATCH 14/21] tBIN plugin: Made it compile with QT_NO_CAST_FROM_ASCII (#4513) Fixes build errors against Qt 6.4 (Ubuntu 24.04) where QStringView comparisons with const char* literals cause compilation issues. Wrapped string literals in QLatin1String/QStringLiteral and replaced implicit const char* conversions with explicit QString::fromStdString / QLatin1Char. (cherry picked from commit d6417e46d68f088c08524e8c4a7a7f316a5d7a6b) --- src/plugins/tbin/tbin.qbs | 6 +- src/plugins/tbin/tbinplugin.cpp | 14 +- src/plugins/tbin/tidemapformat.cpp | 204 ++++++++++++++--------------- 3 files changed, 114 insertions(+), 110 deletions(-) diff --git a/src/plugins/tbin/tbin.qbs b/src/plugins/tbin/tbin.qbs index 38d7732f71..160b64075b 100644 --- a/src/plugins/tbin/tbin.qbs +++ b/src/plugins/tbin/tbin.qbs @@ -1,5 +1,9 @@ TiledPlugin { - cpp.defines: base.concat(["TBIN_LIBRARY"]) + cpp.defines: base.concat([ + "TBIN_LIBRARY", + "QT_NO_CAST_FROM_ASCII", + "QT_NO_CAST_TO_ASCII", + ]) files: [ "tbin_global.h", diff --git a/src/plugins/tbin/tbinplugin.cpp b/src/plugins/tbin/tbinplugin.cpp index d7b38a11f2..0e2a82988d 100644 --- a/src/plugins/tbin/tbinplugin.cpp +++ b/src/plugins/tbin/tbinplugin.cpp @@ -167,8 +167,8 @@ std::unique_ptr< Tiled::Map > TbinPlugin::fromTbin( const tbin::Map& tmap, const if (ttilesheet.margin.x != ttilesheet.margin.y) throw std::invalid_argument(QT_TR_NOOP("Tilesheet must have equal margins.")); - auto tileset = Tiled::Tileset::create(ttilesheet.id.c_str(), ttilesheet.tileSize.x, ttilesheet.tileSize.y, ttilesheet.spacing.x, ttilesheet.margin.x); - tileset->setImageSource(Tiled::toUrl(QString::fromStdString(ttilesheet.image).replace("\\", "/"), fileDir)); + auto tileset = Tiled::Tileset::create(QString::fromStdString(ttilesheet.id), ttilesheet.tileSize.x, ttilesheet.tileSize.y, ttilesheet.spacing.x, ttilesheet.margin.x); + tileset->setImageSource(Tiled::toUrl(QString::fromStdString(ttilesheet.image).replace(QLatin1Char('\\'), QLatin1Char('/')), fileDir)); tileset->loadImage(); tbinToTiledProperties(ttilesheet.props, *tileset); @@ -235,7 +235,7 @@ std::unique_ptr< Tiled::Map > TbinPlugin::fromTbin( const tbin::Map& tmap, const if (ttile.props.size() > 0) { - auto obj = std::make_unique("TileData"); + auto obj = std::make_unique(QStringLiteral("TileData")); obj->setPosition(QPointF(ix * tlayer.tileSize.x, iy * tlayer.tileSize.y)); obj->setSize(QSizeF(tlayer.tileSize.x, tlayer.tileSize.y)); tbinToTiledProperties(ttile.props, *obj); @@ -342,7 +342,7 @@ tbin::Map TbinPlugin::toTbin( const Tiled::Map* map, const QDir &fileDir ) for (const Tiled::SharedTileset& tilesheet : map->tilesets()) { tbin::TileSheet ttilesheet; ttilesheet.id = tilesheet->name().toStdString(); - ttilesheet.image = Tiled::toFileReference(tilesheet->imageSource(), fileDir).replace("/", "\\").toStdString(); + ttilesheet.image = Tiled::toFileReference(tilesheet->imageSource(), fileDir).replace(QLatin1Char('/'), QLatin1Char('\\')).toStdString(); ttilesheet.margin.x = ttilesheet.margin.y = tilesheet->margin(); ttilesheet.spacing.x = ttilesheet.spacing.y = tilesheet->tileSpacing(); ttilesheet.sheetSize.x = tilesheet->columnCount(); @@ -355,7 +355,7 @@ tbin::Map TbinPlugin::toTbin( const Tiled::Map* map, const QDir &fileDir ) for (auto tile : tilesheet->tiles()) { const auto &props = tile->properties(); for (auto it = props.begin(), it_end = props.end(); it != it_end; ++it) { - tilesetTileProperties.insert("@TileIndex@" + QString::number(tile->id()) + "@" + it.key(), it.value()); + tilesetTileProperties.insert(QLatin1String("@TileIndex@") + QString::number(tile->id()) + QLatin1Char('@') + it.key(), it.value()); } } tiledToTbinProperties(tilesetTileProperties, ttilesheet.props, fileDir); @@ -385,7 +385,7 @@ tbin::Map TbinPlugin::toTbin( const Tiled::Map* map, const QDir &fileDir ) ttile.staticData.tileIndex = -1; if (hasUnsupportedFlags(cell)) { - Tiled::ERROR("tBIN: Flipped and/or rotated tiles are not supported.", + Tiled::ERROR(QLatin1String("tBIN: Flipped and/or rotated tiles are not supported."), Tiled::JumpToTile { map, QPoint(ix + layer->x(), iy + layer->y()), layer }); } @@ -400,7 +400,7 @@ tbin::Map TbinPlugin::toTbin( const Tiled::Map* map, const QDir &fileDir ) for (Tiled::Frame frame : tile->frames()) { if (frame.duration != ttile.animatedData.frameInterval) { - Tiled::ERROR("tBIN: Frames with different duration are not supported.", + Tiled::ERROR(QLatin1String("tBIN: Frames with different duration are not supported."), Tiled::SelectTile { tile }); } diff --git a/src/plugins/tbin/tidemapformat.cpp b/src/plugins/tbin/tidemapformat.cpp index 44df5abb70..244ab3d9c6 100644 --- a/src/plugins/tbin/tidemapformat.cpp +++ b/src/plugins/tbin/tidemapformat.cpp @@ -61,7 +61,7 @@ std::unique_ptr TideMapFormat::read(const QString &fileName) tbin::Map tmap; const QXmlStreamAttributes mapAttrs = xml.attributes(); - tmap.id = mapAttrs.value( "Id" ).toString().toStdString(); + tmap.id = mapAttrs.value( QLatin1String("Id") ).toString().toStdString(); auto readProps = [&xml]() -> tbin::Properties { @@ -69,31 +69,31 @@ std::unique_ptr TideMapFormat::read(const QString &fileName) while ( xml.readNextStartElement() ) { - if ( xml.name() == "Property" ) + if ( xml.name() == QLatin1String("Property") ) { const QXmlStreamAttributes attrs = xml.attributes(); - const std::string key = attrs.value( "Key" ).toString().toStdString(); - const QString type = attrs.value( "Type" ).toString(); + const std::string key = attrs.value( QLatin1String("Key") ).toString().toStdString(); + const QString type = attrs.value( QLatin1String("Type") ).toString(); const QString value = xml.readElementText(); tbin::PropertyValue prop; - if ( type == "Boolean" ) + if ( type == QLatin1String("Boolean") ) { prop.type = tbin::PropertyValue::Bool; - prop.data.b = QString::compare( value, "true", Qt::CaseInsensitive ) == 0; + prop.data.b = QString::compare( value, QLatin1String("true"), Qt::CaseInsensitive ) == 0; } - if ( type == "Int32" ) + if ( type == QLatin1String("Int32") ) { prop.type = tbin::PropertyValue::Integer; prop.data.i = value.toInt(); } - if ( type == "Single" ) + if ( type == QLatin1String("Single") ) { prop.type = tbin::PropertyValue::Float; prop.data.f = value.toFloat(); } - if ( type == "String" ) + if ( type == QLatin1String("String") ) { prop.type = tbin::PropertyValue::String; prop.dataStr = value.toStdString(); @@ -111,11 +111,11 @@ std::unique_ptr TideMapFormat::read(const QString &fileName) { if ( xml.name() == QLatin1String( "Description" ) ) tmap.desc = xml.readElementText().toStdString(); - else if ( xml.name() == "TileSheets" ) + else if ( xml.name() == QLatin1String("TileSheets") ) { while ( xml.readNextStartElement() ) { - if ( xml.name() != "TileSheet" ) + if ( xml.name() != QLatin1String("TileSheet") ) { xml.skipCurrentElement(); continue; @@ -124,27 +124,27 @@ std::unique_ptr TideMapFormat::read(const QString &fileName) const QXmlStreamAttributes tsAttrs = xml.attributes(); tbin::TileSheet ts; - ts.id = tsAttrs.value( "Id" ).toString().toStdString(); + ts.id = tsAttrs.value( QLatin1String("Id") ).toString().toStdString(); while ( xml.readNextStartElement() ) { if ( xml.name() == QLatin1String( "Description" ) ) ts.desc = xml.readElementText().toStdString(); - else if ( xml.name() == "ImageSource" ) + else if ( xml.name() == QLatin1String("ImageSource") ) ts.image = xml.readElementText().toStdString(); - else if ( xml.name() == "Alignment" ) + else if ( xml.name() == QLatin1String("Alignment") ) { const QXmlStreamAttributes alignAttrs = xml.attributes(); - const QString sheetSizeStr = alignAttrs.value( "SheetSize" ).toString(); - const QString tileSizeStr = alignAttrs.value( "TileSize" ).toString(); - const QString marginStr = alignAttrs.value( "Margin" ).toString(); - const QString spacingStr = alignAttrs.value( "Spacing" ).toString(); + const QString sheetSizeStr = alignAttrs.value( QLatin1String("SheetSize") ).toString(); + const QString tileSizeStr = alignAttrs.value( QLatin1String("TileSize") ).toString(); + const QString marginStr = alignAttrs.value( QLatin1String("Margin") ).toString(); + const QString spacingStr = alignAttrs.value( QLatin1String("Spacing") ).toString(); - qsizetype sheetSizeSep = sheetSizeStr.indexOf( " x " ); - qsizetype tileSizeSep = tileSizeStr.indexOf( " x " ); - qsizetype marginSep = marginStr.indexOf( " x " ); - qsizetype spacingSep = spacingStr.indexOf( " x " ); + qsizetype sheetSizeSep = sheetSizeStr.indexOf( QLatin1String(" x ") ); + qsizetype tileSizeSep = tileSizeStr.indexOf( QLatin1String(" x ") ); + qsizetype marginSep = marginStr.indexOf( QLatin1String(" x ") ); + qsizetype spacingSep = spacingStr.indexOf( QLatin1String(" x ") ); ts.sheetSize = tbin::Vector2i( sheetSizeStr.mid( 0, sheetSizeSep ).toInt(), sheetSizeStr.mid( sheetSizeSep + 3 ).toInt() ); ts.tileSize = tbin::Vector2i( tileSizeStr.mid( 0, tileSizeSep ).toInt(), tileSizeStr.mid( tileSizeSep + 3 ).toInt() ); @@ -153,7 +153,7 @@ std::unique_ptr TideMapFormat::read(const QString &fileName) xml.skipCurrentElement(); } - else if (xml.name() == "Properties") + else if (xml.name() == QLatin1String("Properties")) ts.props = readProps(); else xml.skipCurrentElement(); } @@ -161,11 +161,11 @@ std::unique_ptr TideMapFormat::read(const QString &fileName) tmap.tilesheets.push_back( ts ); } } - else if ( xml.name() == "Layers" ) + else if ( xml.name() == QLatin1String("Layers") ) { while ( xml.readNextStartElement() ) { - if ( xml.name() != "Layer" ) + if ( xml.name() != QLatin1String("Layer") ) { xml.skipCurrentElement(); continue; @@ -173,30 +173,30 @@ std::unique_ptr TideMapFormat::read(const QString &fileName) const QXmlStreamAttributes layerAttrs = xml.attributes(); tbin::Layer layer; - layer.id = layerAttrs.value( "Id" ).toString().toStdString(); - auto vis = layerAttrs.value( "Visible" ).toString().toStdString(); - layer.visible = QString::compare( layerAttrs.value( "Visible" ).toString(), "true", Qt::CaseInsensitive ) == 0; + layer.id = layerAttrs.value( QLatin1String("Id") ).toString().toStdString(); + auto vis = layerAttrs.value( QLatin1String("Visible") ).toString().toStdString(); + layer.visible = QString::compare( layerAttrs.value( QLatin1String("Visible") ).toString(), QLatin1String("true"), Qt::CaseInsensitive ) == 0; while ( xml.readNextStartElement() ) { if ( xml.name() == QLatin1String( "Description" ) ) layer.desc = xml.readElementText().toStdString(); - else if ( xml.name() == "Dimensions" ) + else if ( xml.name() == QLatin1String("Dimensions") ) { const QXmlStreamAttributes alignAttrs = xml.attributes(); - const QString layerSizeStr = alignAttrs.value( "LayerSize" ).toString(); - const QString tileSizeStr = alignAttrs.value( "TileSize" ).toString(); + const QString layerSizeStr = alignAttrs.value( QLatin1String("LayerSize") ).toString(); + const QString tileSizeStr = alignAttrs.value( QLatin1String("TileSize") ).toString(); - qsizetype layerSizeSep = layerSizeStr.indexOf( " x " ); - qsizetype tileSizeSep = tileSizeStr.indexOf( " x " ); + qsizetype layerSizeSep = layerSizeStr.indexOf( QLatin1String(" x ") ); + qsizetype tileSizeSep = tileSizeStr.indexOf( QLatin1String(" x ") ); layer.layerSize = tbin::Vector2i( layerSizeStr.mid( 0, layerSizeSep ).toInt(), layerSizeStr.mid( layerSizeSep + 3 ).toInt() ); layer.tileSize = tbin::Vector2i( tileSizeStr.mid( 0, tileSizeSep ).toInt(), tileSizeStr.mid( tileSizeSep + 3 ).toInt() ); xml.skipCurrentElement(); } - else if ( xml.name() == "TileArray" ) + else if ( xml.name() == QLatin1String("TileArray") ) { tbin::Tile nullTile; nullTile.staticData.tileIndex = -1; @@ -208,8 +208,8 @@ std::unique_ptr TideMapFormat::read(const QString &fileName) tbin::Tile tile; tile.tilesheet = tilesheetToUse; - tile.staticData.tileIndex = tileAttrs.value( "Index" ).toString().toInt(); - tile.staticData.blendMode = tileAttrs.value( "BlendMode" ).toString() == "Alpha" ? 0 : 1; + tile.staticData.tileIndex = tileAttrs.value( QLatin1String("Index") ).toString().toInt(); + tile.staticData.blendMode = tileAttrs.value( QLatin1String("BlendMode") ).toString() == QLatin1String("Alpha") ? 0 : 1; // tIDE (the xTile tile editor) uses a self-closing element when there are no properties. // However, Qt seems to represent these simply as empty elements when reading. @@ -218,7 +218,7 @@ std::unique_ptr TideMapFormat::read(const QString &fileName) // for compatibility with xTile code. while ( xml.readNextStartElement() ) { - if (xml.name() == "Properties") + if (xml.name() == QLatin1String("Properties")) tile.props = readProps(); else xml.skipCurrentElement(); } @@ -230,7 +230,7 @@ std::unique_ptr TideMapFormat::read(const QString &fileName) tbin::Vector2i tilePos; while ( xml.readNextStartElement() ) { - if (xml.name() != "Row" ) + if (xml.name() != QLatin1String("Row") ) { xml.skipCurrentElement(); continue; @@ -238,43 +238,43 @@ std::unique_ptr TideMapFormat::read(const QString &fileName) while ( xml.readNextStartElement() ) { - if ( xml.name() == "TileSheet" ) + if ( xml.name() == QLatin1String("TileSheet") ) { const QXmlStreamAttributes tsAttrs = xml.attributes(); - lastTilesheet = tsAttrs.value( "Ref" ).toString().toStdString(); + lastTilesheet = tsAttrs.value( QLatin1String("Ref") ).toString().toStdString(); xml.skipCurrentElement(); } - else if ( xml.name() == "Static" ) + else if ( xml.name() == QLatin1String("Static") ) { layer.tiles[ tilePos.x + tilePos.y * layer.layerSize.x ] = readStaticTile( lastTilesheet ); tilePos.x += 1; } - else if ( xml.name() == "Animated" ) + else if ( xml.name() == QLatin1String("Animated") ) { const QXmlStreamAttributes tileAttrs = xml.attributes(); tbin::Tile tile; - tile.animatedData.frameInterval = tileAttrs.value( "Interval" ).toString().toInt(); + tile.animatedData.frameInterval = tileAttrs.value( QLatin1String("Interval") ).toString().toInt(); while ( xml.readNextStartElement() ) { - if (xml.name() == "Frames" ) + if (xml.name() == QLatin1String("Frames") ) { std::string lastTilesheet; // Shadowing the previous declaration intentionally, because we explicitly do not want to use/affect it while ( xml.readNextStartElement() ) { - if ( xml.name() == "TileSheet" ) + if ( xml.name() == QLatin1String("TileSheet") ) { const QXmlStreamAttributes tsAttrs = xml.attributes(); - lastTilesheet = tsAttrs.value( "Ref" ).toString().toStdString(); + lastTilesheet = tsAttrs.value( QLatin1String("Ref") ).toString().toStdString(); xml.skipCurrentElement(); } - else if (xml.name() == "Static") + else if (xml.name() == QLatin1String("Static")) tile.animatedData.frames.push_back( readStaticTile( lastTilesheet ) ); else xml.skipCurrentElement(); } } - else if (xml.name() == "Properties") + else if (xml.name() == QLatin1String("Properties")) tile.props = readProps(); else xml.skipCurrentElement(); } @@ -282,10 +282,10 @@ std::unique_ptr TideMapFormat::read(const QString &fileName) layer.tiles[ tilePos.x + tilePos.y * layer.layerSize.x ] = tile; tilePos.x += 1; } - else if ( xml.name() == "Null" ) + else if ( xml.name() == QLatin1String("Null") ) { const QXmlStreamAttributes tileAttrs = xml.attributes(); - tilePos.x += tileAttrs.value( "Count" ).toString().toInt(); + tilePos.x += tileAttrs.value( QLatin1String("Count") ).toString().toInt(); xml.skipCurrentElement(); } else xml.skipCurrentElement(); @@ -295,7 +295,7 @@ std::unique_ptr TideMapFormat::read(const QString &fileName) tilePos.y += 1; } } - else if (xml.name() == "Properties") + else if (xml.name() == QLatin1String("Properties")) layer.props = readProps(); else xml.skipCurrentElement(); } @@ -303,7 +303,7 @@ std::unique_ptr TideMapFormat::read(const QString &fileName) tmap.layers.push_back( layer ); } } - else if (xml.name() == "Properties") + else if (xml.name() == QLatin1String("Properties")) tmap.props = readProps(); else xml.skipCurrentElement(); } @@ -339,28 +339,28 @@ bool TideMapFormat::write(const Tiled::Map *map, const QString &fileName, Option auto writeProps = [&xml](const tbin::Properties& props) { - xml.writeStartElement( "Properties" ); + xml.writeStartElement( QStringLiteral("Properties") ); for ( const auto& prop : props ) { - xml.writeStartElement( "Property" ); - xml.writeAttribute( "Key", QString(prop.first.c_str()) ); + xml.writeStartElement( QStringLiteral("Property") ); + xml.writeAttribute( QStringLiteral("Key"), QString::fromStdString(prop.first) ); switch ( prop.second.type ) { case tbin::PropertyValue::Bool: - xml.writeAttribute( "Type", "Boolean" ); - xml.writeCDATA( prop.second.data.b ? "True" : "False" ); + xml.writeAttribute( QStringLiteral("Type"), QStringLiteral("Boolean") ); + xml.writeCDATA( prop.second.data.b ? QStringLiteral("True") : QStringLiteral("False") ); break; case tbin::PropertyValue::Integer: - xml.writeAttribute( "Type", "Int32" ); + xml.writeAttribute( QStringLiteral("Type"), QStringLiteral("Int32") ); xml.writeCDATA( QString::number( prop.second.data.i ) ); break; case tbin::PropertyValue::Float: - xml.writeAttribute( "Type", "Single" ); + xml.writeAttribute( QStringLiteral("Type"), QStringLiteral("Single") ); xml.writeCDATA( QString::number( prop.second.data.f ) ); break; case tbin::PropertyValue::String: - xml.writeAttribute( "Type", "String" ); - xml.writeCDATA( QString(prop.second.dataStr.c_str()) ); + xml.writeAttribute( QStringLiteral("Type"), QStringLiteral("String") ); + xml.writeCDATA( QString::fromStdString(prop.second.dataStr) ); break; default: throw std::invalid_argument( "Unknown property type" ); @@ -371,34 +371,34 @@ bool TideMapFormat::write(const Tiled::Map *map, const QString &fileName, Option }; xml.writeStartDocument(); - xml.writeStartElement( "Map" ); + xml.writeStartElement( QStringLiteral("Map") ); { - xml.writeAttribute( "Id", QString(tmap.id.c_str()) ); + xml.writeAttribute( QStringLiteral("Id"), QString::fromStdString(tmap.id) ); - xml.writeStartElement( "Description" ); - xml.writeCDATA( QString(tmap.desc.c_str()) ); + xml.writeStartElement( QStringLiteral("Description") ); + xml.writeCDATA( QString::fromStdString(tmap.desc) ); xml.writeEndElement(); - xml.writeStartElement("TileSheets"); + xml.writeStartElement(QStringLiteral("TileSheets")); { for ( const auto& ts : tmap.tilesheets ) { - xml.writeStartElement( "TileSheet" ); - xml.writeAttribute( "Id", QString(ts.id.c_str()) ); + xml.writeStartElement( QStringLiteral("TileSheet") ); + xml.writeAttribute( QStringLiteral("Id"), QString::fromStdString(ts.id) ); - xml.writeStartElement( "Description" ); - xml.writeCDATA( QString(ts.desc.c_str()) ); + xml.writeStartElement( QStringLiteral("Description") ); + xml.writeCDATA( QString::fromStdString(ts.desc) ); xml.writeEndElement(); - xml.writeStartElement( "ImageSource" ); - xml.writeCDATA( QString(ts.image.c_str()) ); + xml.writeStartElement( QStringLiteral("ImageSource") ); + xml.writeCDATA( QString::fromStdString(ts.image) ); xml.writeEndElement(); - xml.writeEmptyElement( "Alignment" ); - xml.writeAttribute( "SheetSize", QStringLiteral( "%1 x %2" ).arg( ts.sheetSize.x ).arg( ts.sheetSize.y ) ); - xml.writeAttribute( "TileSize", QStringLiteral( "%1 x %2" ).arg( ts.tileSize.x ).arg( ts.tileSize.y ) ); - xml.writeAttribute( "Margin", QStringLiteral( "%1 x %2" ).arg( ts.margin.x ).arg( ts.margin.y ) ); - xml.writeAttribute( "Spacing", QStringLiteral( "%1 x %2" ).arg( ts.spacing.x ).arg( ts.spacing.y ) ); + xml.writeEmptyElement( QStringLiteral("Alignment") ); + xml.writeAttribute( QStringLiteral("SheetSize"), QStringLiteral( "%1 x %2" ).arg( ts.sheetSize.x ).arg( ts.sheetSize.y ) ); + xml.writeAttribute( QStringLiteral("TileSize"), QStringLiteral( "%1 x %2" ).arg( ts.tileSize.x ).arg( ts.tileSize.y ) ); + xml.writeAttribute( QStringLiteral("Margin"), QStringLiteral( "%1 x %2" ).arg( ts.margin.x ).arg( ts.margin.y ) ); + xml.writeAttribute( QStringLiteral("Spacing"), QStringLiteral( "%1 x %2" ).arg( ts.spacing.x ).arg( ts.spacing.y ) ); writeProps( ts.props ); @@ -407,33 +407,33 @@ bool TideMapFormat::write(const Tiled::Map *map, const QString &fileName, Option } xml.writeEndElement(); - xml.writeStartElement("Layers"); + xml.writeStartElement(QStringLiteral("Layers")); { for ( const auto& layer : tmap.layers ) { - xml.writeStartElement( "Layer" ); - xml.writeAttribute( "Id", QString(layer.id.c_str()) ); - xml.writeAttribute( "Visible", layer.visible ? "True" : "False" ); + xml.writeStartElement( QStringLiteral("Layer") ); + xml.writeAttribute( QStringLiteral("Id"), QString::fromStdString(layer.id) ); + xml.writeAttribute( QStringLiteral("Visible"), layer.visible ? QStringLiteral("True") : QStringLiteral("False") ); - xml.writeStartElement( "Description" ); - xml.writeCDATA( QString(layer.desc.c_str()) ); + xml.writeStartElement( QStringLiteral("Description") ); + xml.writeCDATA( QString::fromStdString(layer.desc) ); xml.writeEndElement(); - xml.writeEmptyElement( "Dimensions" ); - xml.writeAttribute( "LayerSize", QStringLiteral( "%1 x %2" ).arg( layer.layerSize.x ).arg( layer.layerSize.y ) ); - xml.writeAttribute( "TileSize", QStringLiteral( "%1 x %2" ).arg( layer.tileSize.x ).arg( layer.tileSize.y ) ); + xml.writeEmptyElement( QStringLiteral("Dimensions") ); + xml.writeAttribute( QStringLiteral("LayerSize"), QStringLiteral( "%1 x %2" ).arg( layer.layerSize.x ).arg( layer.layerSize.y ) ); + xml.writeAttribute( QStringLiteral("TileSize"), QStringLiteral( "%1 x %2" ).arg( layer.tileSize.x ).arg( layer.tileSize.y ) ); auto writeStaticTile = [&xml, &writeProps](const tbin::Tile& tile) { if ( tile.props.size() == 0 ) - xml.writeEmptyElement( "Static" ); + xml.writeEmptyElement( QStringLiteral("Static") ); else - xml.writeStartElement( "Static" ); - xml.writeAttribute( "Index", QString::number( tile.staticData.tileIndex ) ); + xml.writeStartElement( QStringLiteral("Static") ); + xml.writeAttribute( QStringLiteral("Index"), QString::number( tile.staticData.tileIndex ) ); switch ( tile.staticData.blendMode ) { - case 0: xml.writeAttribute( "BlendMode", "Alpha" ); break; - case 1: xml.writeAttribute( "BlendMode", "Additive" ); break; + case 0: xml.writeAttribute( QStringLiteral("BlendMode"), QStringLiteral("Alpha") ); break; + case 1: xml.writeAttribute( QStringLiteral("BlendMode"), QStringLiteral("Additive") ); break; default: throw std::invalid_argument( "Unsupported tile blend mode" ); } if ( tile.props.size() > 0 ) @@ -442,18 +442,18 @@ bool TideMapFormat::write(const Tiled::Map *map, const QString &fileName, Option xml.writeEndElement(); } }; - xml.writeStartElement( "TileArray" ); + xml.writeStartElement( QStringLiteral("TileArray") ); std::string lastTilesheet; for ( int iy = 0; iy < layer.layerSize.y; ++iy ) { - xml.writeStartElement( "Row" ); + xml.writeStartElement( QStringLiteral("Row") ); for ( int ix = 0; ix < layer.layerSize.x; ++ix ) { tbin::Tile tile = layer.tiles[ ix + iy * layer.layerSize.x ]; if ( tile.tilesheet != "" && tile.tilesheet != lastTilesheet ) { - xml.writeEmptyElement( "TileSheet" ); - xml.writeAttribute( "Ref", QString(tile.tilesheet.c_str()) ); + xml.writeEmptyElement( QStringLiteral("TileSheet") ); + xml.writeAttribute( QStringLiteral("Ref"), QString::fromStdString(tile.tilesheet) ); lastTilesheet = tile.tilesheet; } @@ -470,8 +470,8 @@ bool TideMapFormat::write(const Tiled::Map *map, const QString &fileName, Option nullCount += 1; } - xml.writeEmptyElement( "Null" ); - xml.writeAttribute( "Count", QString::number( nullCount ) ); + xml.writeEmptyElement( QStringLiteral("Null") ); + xml.writeAttribute( QStringLiteral("Count"), QString::number( nullCount ) ); } else if ( tile.staticData.tileIndex != -1 ) { @@ -479,17 +479,17 @@ bool TideMapFormat::write(const Tiled::Map *map, const QString &fileName, Option } else if ( tile.animatedData.frames.size() > 0 ) { - xml.writeStartElement( "Animated" ); - xml.writeAttribute( "Interval", QString::number( tile.animatedData.frameInterval ) ); + xml.writeStartElement( QStringLiteral("Animated") ); + xml.writeAttribute( QStringLiteral("Interval"), QString::number( tile.animatedData.frameInterval ) ); - xml.writeStartElement( "Frames" ); + xml.writeStartElement( QStringLiteral("Frames") ); std::string lastTilesheet; // Shadowing the previous declaration intentionally, because we explicitly do not want to use/affect it for ( const auto& tile : tile.animatedData.frames ) { if ( tile.tilesheet != "" && tile.tilesheet != lastTilesheet ) { - xml.writeEmptyElement( "TileSheet" ); - xml.writeAttribute( "Ref", QString(tile.tilesheet.c_str()) ); + xml.writeEmptyElement( QStringLiteral("TileSheet") ); + xml.writeAttribute( QStringLiteral("Ref"), QString::fromStdString(tile.tilesheet) ); lastTilesheet = tile.tilesheet; } writeStaticTile( tile ); From 81157af6220b79018a7214d4cdf4b5835ee3d68b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thorbj=C3=B8rn=20Lindeijer?= Date: Thu, 21 May 2026 19:13:04 +0200 Subject: [PATCH 15/21] Fix copy/paste of list properties (#4514) and clean up RecursiveBehavior (#4517) Fix copy/paste of list properties and related clean-up Copying a list custom property and pasting it onto another object dropped the items, leaving them saved as `type="std::nullptr_t"` in TMX. The same bug applied to lists nested inside class properties and also affected storing list properties on projects or worlds. The clipboard, project files, and world files serialize through `propertiesToJson` / `propertiesFromJson`, which fed `QJsonValue::fromVariant` a tree containing `ExportValue` wrappers it was not programmed to handle. They now use a new `TypedListValues` `RecursiveBehavior` that produces `{type, propertytype, value}` maps for each list element (matching the JSON map format on disk) and decodes the same shape on read. A few cleanups while in the area: - Folded the JSON map converter helpers (`exportValuesToVariantMap`, `convertListValues`) into `ExportContext` so the same recursion logic is shared. - Replaced the half-baked `ListsAsExportValues` default with an explicit `NoRecursion` default. Callers that walk compound values themselves (the XML reader and writer, `PropertyTypes::toJson`) now use the default. The JSON helpers opt in to `TypedListValues`. - Made the XML writer mirror the XML reader, driving recursion through lists and class members itself, so `ExportValuesOnly` was no longer needed and was removed. - Added autotests for round-tripping list values to XML and JSON. Closes #4514 (cherry picked from commit c9ad67bc7dd44c9738f3482a301b7fcac3395e4d) --- NEWS.md | 1 + src/libtiled/mapreader.cpp | 2 +- src/libtiled/maptovariantconverter.cpp | 51 ++++------------ src/libtiled/maptovariantconverter.h | 2 - src/libtiled/mapwriter.cpp | 64 +++++++++----------- src/libtiled/properties.cpp | 83 ++++++++++++++++++++------ src/libtiled/properties.h | 22 ++++--- src/libtiled/propertytype.cpp | 51 ++++++++-------- src/libtiled/varianttomapconverter.cpp | 44 +++----------- src/libtiled/varianttomapconverter.h | 3 - src/libtiled/world.cpp | 6 +- src/tiled/project.cpp | 6 +- tests/properties/test_properties.cpp | 77 ++++++++++++++++++++++++ 13 files changed, 241 insertions(+), 171 deletions(-) diff --git a/NEWS.md b/NEWS.md index e393a464cd..90bb49f004 100644 --- a/NEWS.md +++ b/NEWS.md @@ -4,6 +4,7 @@ * Reduced animated tile marker opacity during terrain editing (by Huy Vũ, #4449) * Fixed ability to change properties after deselecting the current object (#4440) * Fixed Properties view getting stuck with updates disabled (#4506) +* Fixed copy/paste of list properties losing item types (#4514) * Fixed locale-aware parsing of numbers in expression-capable spin boxes * Fixed Tile Animation Editor update after tileset image reload (#3923) * Fixed runtime language switching in many editor widgets and models (by SIDDHAARTHAA, #4411) diff --git a/src/libtiled/mapreader.cpp b/src/libtiled/mapreader.cpp index bb35f84299..99513a8ab7 100644 --- a/src/libtiled/mapreader.cpp +++ b/src/libtiled/mapreader.cpp @@ -1433,7 +1433,7 @@ Properties MapReaderPrivate::readProperties() Q_ASSERT(xml.isStartElement() && xml.name() == QLatin1String("properties")); Properties properties; - const ExportContext context(mPath.path()); + const ExportContext context(mPath.path()); // NoRecursion mode while (xml.readNextStartElement()) { if (xml.name() == QLatin1String("property")) diff --git a/src/libtiled/maptovariantconverter.cpp b/src/libtiled/maptovariantconverter.cpp index 565ddca67e..75cb83ab45 100644 --- a/src/libtiled/maptovariantconverter.cpp +++ b/src/libtiled/maptovariantconverter.cpp @@ -365,7 +365,10 @@ QVariant MapToVariantConverter::toVariant(const Tileset &tileset, QVariant MapToVariantConverter::toVariant(const Properties &properties) const { QVariantMap variantMap; - const ExportContext context(mDir.path()); + ExportContext context(mDir.path()); + // The JSON1 format can't represent typed lists, but classes still need + // their members converted to scalars. + context.setRecursiveBehavior(ExportContext::RecursiveBehavior::ValuesOnly); Properties::const_iterator it = properties.constBegin(); Properties::const_iterator it_end = properties.constEnd(); @@ -834,6 +837,8 @@ void MapToVariantConverter::addProperties(QVariantMap &variantMap, variantMap[QStringLiteral("properties")] = std::move(propertiesMap); variantMap[QStringLiteral("propertytypes")] = std::move(propertyTypesMap); } else { + context.setRecursiveBehavior(ExportContext::RecursiveBehavior::TypedListValues); + QVariantList propertiesVariantList; Properties::const_iterator it = properties.constBegin(); @@ -841,8 +846,12 @@ void MapToVariantConverter::addProperties(QVariantMap &variantMap, for (; it != it_end; ++it) { const auto exportValue = context.toExportValue(it.value()); - QVariantMap propertyVariantMap = toVariantMap(exportValue); + QVariantMap propertyVariantMap; propertyVariantMap[QStringLiteral("name")] = it.key(); + propertyVariantMap[QStringLiteral("type")] = exportValue.typeName; + propertyVariantMap[QStringLiteral("value")] = exportValue.value; + if (!exportValue.propertyTypeName.isEmpty()) + propertyVariantMap[QStringLiteral("propertytype")] = exportValue.propertyTypeName; propertiesVariantList << std::move(propertyVariantMap); } @@ -850,41 +859,3 @@ void MapToVariantConverter::addProperties(QVariantMap &variantMap, variantMap[QStringLiteral("properties")] = propertiesVariantList; } } - -QVariantMap MapToVariantConverter::toVariantMap(const ExportValue &exportValue) const -{ - QVariantMap propertyVariantMap; - - QVariant value = exportValue.value; - exportValuesToVariantMap(value); - - propertyVariantMap[QStringLiteral("type")] = exportValue.typeName; - propertyVariantMap[QStringLiteral("value")] = value; - if (!exportValue.propertyTypeName.isEmpty()) - propertyVariantMap[QStringLiteral("propertytype")] = exportValue.propertyTypeName; - - return propertyVariantMap; -} - -void MapToVariantConverter::exportValuesToVariantMap(QVariant &value) const -{ - switch (value.userType()) { - case QMetaType::QVariantMap: { - auto map = value.toMap(); - for (auto &value : map) - exportValuesToVariantMap(value); - value = std::move(map); - break; - } - case QMetaType::QVariantList: { - auto list = value.toList(); - for (QVariant &item : list) - item = toVariantMap(item.value()); - value = std::move(list); - break; - } - default: - // No conversion needed for other types - break; - } -} diff --git a/src/libtiled/maptovariantconverter.h b/src/libtiled/maptovariantconverter.h index 87af80b1e4..8f3bfd5f03 100644 --- a/src/libtiled/maptovariantconverter.h +++ b/src/libtiled/maptovariantconverter.h @@ -86,8 +86,6 @@ class TILEDSHARED_EXPORT MapToVariantConverter void addProperties(QVariantMap &variantMap, const Properties &properties) const; - QVariantMap toVariantMap(const ExportValue &exportValue) const; - void exportValuesToVariantMap(QVariant &value) const; int mVersion; QDir mDir; diff --git a/src/libtiled/mapwriter.cpp b/src/libtiled/mapwriter.cpp index 8b242f626f..aa1b667594 100644 --- a/src/libtiled/mapwriter.cpp +++ b/src/libtiled/mapwriter.cpp @@ -94,7 +94,9 @@ class MapWriterPrivate void writeGroupLayer(QXmlStreamWriter &w, const GroupLayer &groupLayer); void writeProperties(QXmlStreamWriter &w, const Properties &properties); - void writeExportValue(QXmlStreamWriter &w, const ExportValue &value); + void writePropertyValue(QXmlStreamWriter &w, + const QVariant &value, + const ExportContext &context); void writeImage(QXmlStreamWriter &w, const QUrl &source, const QPixmap &image, @@ -907,67 +909,57 @@ void MapWriterPrivate::writeProperties(QXmlStreamWriter &w, w.writeStartElement(QStringLiteral("properties")); - ExportContext context(mUseAbsolutePaths ? QString() : mDir.path()); - context.setRecursiveBehavior(ExportContext::RecursiveBehavior::ExportValuesOnly); + const ExportContext context(mUseAbsolutePaths ? QString() : mDir.path()); // NoRecursion mode Properties::const_iterator it = properties.constBegin(); Properties::const_iterator it_end = properties.constEnd(); for (; it != it_end; ++it) { w.writeStartElement(QStringLiteral("property")); w.writeAttribute(QStringLiteral("name"), it.key()); - - writeExportValue(w, context.toExportValue(it.value())); - + writePropertyValue(w, it.value(), context); w.writeEndElement(); // } w.writeEndElement(); // } -void MapWriterPrivate::writeExportValue(QXmlStreamWriter &w, const ExportValue &exportValue) +void MapWriterPrivate::writePropertyValue(QXmlStreamWriter &w, + const QVariant &value, + const ExportContext &context) { + const ExportValue exportValue = context.toExportValue(value); + if (exportValue.typeName != QLatin1String("string")) w.writeAttribute(QStringLiteral("type"), exportValue.typeName); if (!exportValue.propertyTypeName.isEmpty()) w.writeAttribute(QStringLiteral("propertytype"), exportValue.propertyTypeName); - switch (exportValue.value.userType()) { - case QMetaType::QVariantList: { - const auto values = exportValue.value.toList(); - for (const QVariant &value : values) { - w.writeStartElement(QStringLiteral("item")); - writeExportValue(w, value.value()); - w.writeEndElement(); // - } - break; - } - case QMetaType::QVariantMap: { - const auto map = exportValue.value.toMap(); - if (map.isEmpty()) - break; + if (exportValue.typeName == QLatin1String("class")) { + const QVariantMap members = exportValue.value.toMap(); + if (members.isEmpty()) + return; w.writeStartElement(QStringLiteral("properties")); - Properties::const_iterator it = map.constBegin(); - Properties::const_iterator it_end = map.constEnd(); - for (; it != it_end; ++it) { + for (auto it = members.constBegin(); it != members.constEnd(); ++it) { w.writeStartElement(QStringLiteral("property")); w.writeAttribute(QStringLiteral("name"), it.key()); - - writeExportValue(w, it.value().value()); - + writePropertyValue(w, it.value(), context); w.writeEndElement(); // } w.writeEndElement(); // - break; - } - default: - const QString value = exportValue.value.toString(); - - if (value.contains(QLatin1Char('\n'))) - w.writeCharacters(value); + } else if (exportValue.typeName == QLatin1String("list")) { + const auto list = exportValue.value.toList(); + for (const QVariant &item : list) { + w.writeStartElement(QStringLiteral("item")); + writePropertyValue(w, item, context); + w.writeEndElement(); // + } + } else { + const QString stringValue = exportValue.value.toString(); + if (stringValue.contains(QLatin1Char('\n'))) + w.writeCharacters(stringValue); else - w.writeAttribute(QStringLiteral("value"), value); - break; + w.writeAttribute(QStringLiteral("value"), stringValue); } } diff --git a/src/libtiled/properties.cpp b/src/libtiled/properties.cpp index 9a209b4f8f..01335e01b5 100644 --- a/src/libtiled/properties.cpp +++ b/src/libtiled/properties.cpp @@ -206,6 +206,8 @@ void mergeProperties(Properties &target, const Properties &source) QJsonArray propertiesToJson(const Properties &properties, const ExportContext &context) { + Q_ASSERT(context.recursiveBehavior() == ExportContext::RecursiveBehavior::TypedListValues); + QJsonArray json; Properties::const_iterator it = properties.begin(); @@ -218,7 +220,8 @@ QJsonArray propertiesToJson(const Properties &properties, const ExportContext &c propertyObject.insert(QLatin1String("name"), name); propertyObject.insert(QLatin1String("value"), QJsonValue::fromVariant(exportValue.value)); propertyObject.insert(QLatin1String("type"), exportValue.typeName); - propertyObject.insert(QLatin1String("propertytype"), exportValue.propertyTypeName); + if (!exportValue.propertyTypeName.isEmpty()) + propertyObject.insert(QLatin1String("propertytype"), exportValue.propertyTypeName); json.append(propertyObject); } @@ -226,8 +229,17 @@ QJsonArray propertiesToJson(const Properties &properties, const ExportContext &c return json; } +QJsonArray propertiesToJson(const Properties &properties, const QString &path) +{ + ExportContext context(path); + context.setRecursiveBehavior(ExportContext::RecursiveBehavior::TypedListValues); + return propertiesToJson(properties, context); +} + Properties propertiesFromJson(const QJsonArray &json, const ExportContext &context) { + Q_ASSERT(context.recursiveBehavior() == ExportContext::RecursiveBehavior::TypedListValues); + Properties properties; for (const QJsonValue &property : json) { @@ -245,8 +257,18 @@ Properties propertiesFromJson(const QJsonArray &json, const ExportContext &conte return properties; } -QJsonArray valuesToJson(const QVariantList &values, const ExportContext &context) +Properties propertiesFromJson(const QJsonArray &json, const QString &path) { + ExportContext context(path); + context.setRecursiveBehavior(ExportContext::RecursiveBehavior::TypedListValues); + return propertiesFromJson(json, context); +} + +QJsonArray valuesToJson(const QVariantList &values, const QString &path) +{ + ExportContext context(path); + context.setRecursiveBehavior(ExportContext::RecursiveBehavior::TypedListValues); + QJsonArray json; for (auto &value : values) { @@ -254,12 +276,9 @@ QJsonArray valuesToJson(const QVariantList &values, const ExportContext &context QJsonObject propertyObject; propertyObject.insert(QLatin1String("type"), exportValue.typeName); - propertyObject.insert(QLatin1String("propertytype"), exportValue.propertyTypeName); - - if (value.userType() == QMetaType::QVariantList) - propertyObject.insert(QLatin1String("value"), valuesToJson(value.toList(), context)); - else - propertyObject.insert(QLatin1String("value"), QJsonValue::fromVariant(exportValue.value)); + if (!exportValue.propertyTypeName.isEmpty()) + propertyObject.insert(QLatin1String("propertytype"), exportValue.propertyTypeName); + propertyObject.insert(QLatin1String("value"), QJsonValue::fromVariant(exportValue.value)); json.append(propertyObject); } @@ -267,8 +286,11 @@ QJsonArray valuesToJson(const QVariantList &values, const ExportContext &context return json; } -QVariantList valuesFromJson(const QJsonArray &json, const ExportContext &context) +QVariantList valuesFromJson(const QJsonArray &json, const QString &path) { + ExportContext context(path); + context.setRecursiveBehavior(ExportContext::RecursiveBehavior::TypedListValues); + QVariantList values; for (const QJsonValue &value : json) { @@ -402,16 +424,28 @@ ExportValue ExportContext::toExportValue(const QVariant &value) const ExportValue exportValue; - if (metaType == QMetaType::QVariantList) { + if (metaType == QMetaType::QVariantList && mRecursiveBehavior != RecursiveBehavior::NoRecursion) { QVariantList exportValues; const auto list = value.toList(); exportValues.reserve(list.size()); - if (mRecursiveBehavior == RecursiveBehavior::ValuesOnly) { + switch (mRecursiveBehavior) { + case RecursiveBehavior::NoRecursion: + break; // unreachable, handled above + case RecursiveBehavior::ValuesOnly: for (const QVariant &element : list) exportValues.append(toExportValue(element).value); - } else { - for (const QVariant &element : list) - exportValues.append(QVariant::fromValue(toExportValue(element))); + break; + case RecursiveBehavior::TypedListValues: + for (const QVariant &element : list) { + const ExportValue elementExport = toExportValue(element); + QVariantMap mapItem; + mapItem.insert(QLatin1String("type"), elementExport.typeName); + if (!elementExport.propertyTypeName.isEmpty()) + mapItem.insert(QLatin1String("propertytype"), elementExport.propertyTypeName); + mapItem.insert(QLatin1String("value"), elementExport.value); + exportValues.append(mapItem); + } + break; } exportValue.value = exportValues; } else if (metaType == QMetaType::QColor) { @@ -452,15 +486,30 @@ QVariant ExportContext::toPropertyValue(const ExportValue &exportValue) const QVariant ExportContext::toPropertyValue(const QVariant &value, int metaType) const { + if (metaType == QMetaType::QVariantList) { + if (mRecursiveBehavior != RecursiveBehavior::TypedListValues) + return value; // list elements are already in their final form + + // Each list element is a {type, propertytype, value} map. Reconstruct + // its ExportValue and recurse, which decodes any nested lists too. + QVariantList list = value.toList(); + for (QVariant &item : list) { + const QVariantMap itemMap = item.toMap(); + ExportValue elementExport; + elementExport.value = itemMap.value(QLatin1String("value")); + elementExport.typeName = itemMap.value(QLatin1String("type")).toString(); + elementExport.propertyTypeName = itemMap.value(QLatin1String("propertytype")).toString(); + item = toPropertyValue(elementExport); + } + return list; + } + if (metaType == QMetaType::UnknownType || value.userType() == metaType) return value; // value possibly already converted if (metaType == QMetaType::QVariantMap || metaType == propertyValueId()) return value; // should be covered by property type - if (metaType == QMetaType::QVariantList) - return value; // list elements should be converted individually - if (metaType == filePathTypeId()) { const QUrl url = toUrl(value.toString(), mPath); return QVariant::fromValue(FilePath { url }); diff --git a/src/libtiled/properties.h b/src/libtiled/properties.h index 1a8f54dc8a..ef8b218898 100644 --- a/src/libtiled/properties.h +++ b/src/libtiled/properties.h @@ -98,9 +98,12 @@ class TILEDSHARED_EXPORT ExportContext { public: enum class RecursiveBehavior { + NoRecursion, // Values are taken as-is. Used by readers and + // writers that walk compound values themselves. ValuesOnly, // Lua and JSON1 formats (loses types in lists) - ListsAsExportValues, // JSON2 format - ExportValuesOnly, // XML format (keep superfluous types in classes) + TypedListValues, // List elements expanded to {type, propertytype, + // value} maps so the result can be passed + // directly to QJsonValue::fromVariant. }; explicit ExportContext(const QString &path = QString()); @@ -126,7 +129,7 @@ class TILEDSHARED_EXPORT ExportContext private: const PropertyTypes &mTypes; const QString mPath; - RecursiveBehavior mRecursiveBehavior = RecursiveBehavior::ListsAsExportValues; + RecursiveBehavior mRecursiveBehavior = RecursiveBehavior::NoRecursion; }; class TILEDSHARED_EXPORT AggregatedPropertyData @@ -197,14 +200,19 @@ TILEDSHARED_EXPORT void aggregateProperties(AggregatedProperties &aggregated, co TILEDSHARED_EXPORT void mergeProperties(Properties &target, const Properties &source); TILEDSHARED_EXPORT QJsonArray propertiesToJson(const Properties &properties, - const ExportContext &context = ExportContext()); + const ExportContext &context); +TILEDSHARED_EXPORT QJsonArray propertiesToJson(const Properties &properties, + const QString &path = QString()); + +TILEDSHARED_EXPORT Properties propertiesFromJson(const QJsonArray &json, + const ExportContext &context); TILEDSHARED_EXPORT Properties propertiesFromJson(const QJsonArray &json, - const ExportContext &context = ExportContext()); + const QString &path = QString()); TILEDSHARED_EXPORT QJsonArray valuesToJson(const QVariantList &values, - const ExportContext &context = ExportContext()); + const QString &path = QString()); TILEDSHARED_EXPORT QVariantList valuesFromJson(const QJsonArray &json, - const ExportContext &context = ExportContext()); + const QString &path = QString()); constexpr int propertyValueId() { return qMetaTypeId(); } constexpr int filePathTypeId() { return qMetaTypeId(); } diff --git a/src/libtiled/propertytype.cpp b/src/libtiled/propertytype.cpp index 459bac0f9a..c743be0f85 100644 --- a/src/libtiled/propertytype.cpp +++ b/src/libtiled/propertytype.cpp @@ -242,12 +242,11 @@ ExportValue ClassPropertyType::toExportValue(const QVariant &value, const Export { Properties properties = value.toMap(); - for (auto &value : properties) { - ExportValue exportValue = context.toExportValue(value); - if (context.recursiveBehavior() == ExportContext::RecursiveBehavior::ExportValuesOnly) - value = QVariant::fromValue(std::move(exportValue)); - else + if (context.recursiveBehavior() != ExportContext::RecursiveBehavior::NoRecursion) { + for (auto &value : properties) { + ExportValue exportValue = context.toExportValue(value); value = exportValue.value; + } } return PropertyType::toExportValue(properties, context); @@ -267,8 +266,12 @@ QVariant ClassPropertyType::toPropertyValue(const QVariant &value, const ExportC if (!classMember.isValid()) continue; // ignore removed members - if (it.value().userType() == classMember.userType()) - continue; // leave members alone that already have the expected type + // Members that already match the expected type can be left alone, + // except for lists where the items themselves may still need decoding + // (in TypedListValues mode each item is a wrapper map). + if (it.value().userType() == classMember.userType() + && classMember.userType() != QMetaType::QVariantList) + continue; QVariant propertyValue = context.toPropertyValue(it.value(), classMember.userType()); @@ -305,20 +308,21 @@ static const struct { { ClassPropertyType::ProjectClass, QLatin1String("project") }, }; -QJsonValue exportValueToJson(const ExportValue &exportValue) +QJsonValue exportValueToJson(const QVariant &value, const ExportContext &context) { - switch (exportValue.value.userType()) { - case QMetaType::QVariantList: { + const ExportValue exportValue = context.toExportValue(value); + + if (exportValue.typeName == QLatin1String("list")) { // We have to include the type and possibly propertyType for each value // in the list so that we know the type of the values when loading the // list. QJsonArray jsonArray; const auto list = exportValue.value.toList(); - for (const auto &item : list) { - const auto itemExportValue = item.value(); + for (const QVariant &item : list) { + const ExportValue itemExportValue = context.toExportValue(item); QJsonObject member { - { QStringLiteral("type"), itemExportValue.typeName }, - { QStringLiteral("value"), exportValueToJson(itemExportValue) }, + { QStringLiteral("type"), itemExportValue.typeName }, + { QStringLiteral("value"), exportValueToJson(item, context) }, }; if (!itemExportValue.propertyTypeName.isEmpty()) @@ -328,18 +332,18 @@ QJsonValue exportValueToJson(const ExportValue &exportValue) } return jsonArray; } - case QMetaType::QVariantMap: { + + if (exportValue.typeName == QLatin1String("class")) { // For classes we only store the values, because the type of the // members is defined by the class. QJsonObject jsonObject; - const auto map = exportValue.value.toMap(); - for (auto it = map.constBegin(); it != map.constEnd(); ++it) - jsonObject.insert(it.key(), exportValueToJson(it.value().value())); + const QVariantMap members = exportValue.value.toMap(); + for (auto it = members.constBegin(); it != members.constEnd(); ++it) + jsonObject.insert(it.key(), exportValueToJson(it.value(), context)); return jsonObject; } - default: - return QJsonValue::fromVariant(exportValue.value); - } + + return QJsonValue::fromVariant(exportValue.value); } QJsonObject ClassPropertyType::toJson(const ExportContext &context) const @@ -355,7 +359,7 @@ QJsonObject ClassPropertyType::toJson(const ExportContext &context) const QJsonObject member { { QStringLiteral("name"), it.key() }, { QStringLiteral("type"), exportValue.typeName }, - { QStringLiteral("value"), exportValueToJson(exportValue) }, + { QStringLiteral("value"), exportValueToJson(it.value(), context) }, }; if (!exportValue.propertyTypeName.isEmpty()) @@ -783,8 +787,7 @@ void PropertyTypes::resolveMemberValues(ClassPropertyType *classType, QJsonArray PropertyTypes::toJson(const QString &path) const { - ExportContext context(*this, path); - context.setRecursiveBehavior(ExportContext::RecursiveBehavior::ExportValuesOnly); + const ExportContext context(*this, path); // NoRecursion mode QJsonArray propertyTypesJson; for (const auto &type : mTypes) diff --git a/src/libtiled/varianttomapconverter.cpp b/src/libtiled/varianttomapconverter.cpp index 96f4a833ea..fc26ce2efd 100644 --- a/src/libtiled/varianttomapconverter.cpp +++ b/src/libtiled/varianttomapconverter.cpp @@ -166,7 +166,7 @@ Properties VariantToMapConverter::toProperties(const QVariant &propertiesVariant { Properties properties; - const ExportContext context(mDir.path()); + ExportContext context(mDir.path()); // read object-based format (1.0) const QVariantMap propertiesMap = propertiesVariant.toMap(); @@ -184,49 +184,23 @@ Properties VariantToMapConverter::toProperties(const QVariant &propertiesVariant // read array-based format (1.2) const QVariantList propertiesList = propertiesVariant.toList(); + if (!propertiesList.isEmpty()) + context.setRecursiveBehavior(ExportContext::RecursiveBehavior::TypedListValues); for (const QVariant &propertyVariant : propertiesList) { const QVariantMap propertyVariantMap = propertyVariant.toMap(); const QString propertyName = propertyVariantMap[QStringLiteral("name")].toString(); - properties[propertyName] = toPropertyValue(propertyVariantMap, context); + ExportValue exportValue; + exportValue.value = propertyVariantMap[QStringLiteral("value")]; + exportValue.typeName = propertyVariantMap[QStringLiteral("type")].toString(); + exportValue.propertyTypeName = propertyVariantMap[QStringLiteral("propertytype")].toString(); + + properties[propertyName] = context.toPropertyValue(exportValue); } return properties; } -QVariant VariantToMapConverter::toPropertyValue(const QVariantMap &valueVariantMap, - const ExportContext &context) const -{ - ExportValue exportValue; - exportValue.value = valueVariantMap[QStringLiteral("value")]; - exportValue.typeName = valueVariantMap[QStringLiteral("type")].toString(); - exportValue.propertyTypeName = valueVariantMap[QStringLiteral("propertytype")].toString(); - - convertListValues(exportValue.value, context); - - return context.toPropertyValue(exportValue); -} - -void VariantToMapConverter::convertListValues(QVariant &value, const ExportContext &context) const -{ - switch (value.userType()) { - case QMetaType::QVariantList: { - QVariantList list = value.toList(); - for (QVariant &item : list) - item = toPropertyValue(item.toMap(), context); - value = std::move(list); - break; - } - case QMetaType::QVariantMap: { - QVariantMap map = value.toMap(); - for (QVariant &value : map) - convertListValues(value, context); - value = std::move(map); - break; - } - } -} - SharedTileset VariantToMapConverter::toTileset(const QVariant &variant) { const QVariantMap variantMap = variant.toMap(); diff --git a/src/libtiled/varianttomapconverter.h b/src/libtiled/varianttomapconverter.h index 751a8fc84f..0ea4432b8e 100644 --- a/src/libtiled/varianttomapconverter.h +++ b/src/libtiled/varianttomapconverter.h @@ -84,9 +84,6 @@ class TILEDSHARED_EXPORT VariantToMapConverter private: Properties toProperties(const QVariant &propertiesVariant, const QVariant &propertyTypesVariant) const; - QVariant toPropertyValue(const QVariantMap &valueVariantMap, - const ExportContext &context) const; - void convertListValues(QVariant &value, const ExportContext &context) const; SharedTileset toTileset(const QVariant &variant); std::unique_ptr toWangSet(const QVariantMap &variantMap, Tileset *tileset); diff --git a/src/libtiled/world.cpp b/src/libtiled/world.cpp index f817e0d4d3..756586f4f4 100644 --- a/src/libtiled/world.cpp +++ b/src/libtiled/world.cpp @@ -301,8 +301,7 @@ std::unique_ptr World::load(const QString &fileName, } const QJsonArray properties = object.value(QLatin1String("properties")).toArray(); - const ExportContext context(dir.path()); - world->setProperties(propertiesFromJson(properties, context)); + world->setProperties(propertiesFromJson(properties, dir.path())); world->onlyShowAdjacentMaps = object.value(QLatin1String("onlyShowAdjacentMaps")).toBool(); @@ -350,8 +349,7 @@ bool World::save(World &world, QString *errorString) patterns.append(jsonPattern); } - const ExportContext context(worldDir.path()); - const QJsonArray properties = propertiesToJson(world.properties(), context); + const QJsonArray properties = propertiesToJson(world.properties(), worldDir.path()); QJsonObject document; if (!maps.isEmpty()) diff --git a/src/tiled/project.cpp b/src/tiled/project.cpp index 82907ab83b..bbd7f63311 100644 --- a/src/tiled/project.cpp +++ b/src/tiled/project.cpp @@ -75,7 +75,8 @@ 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()); + ExportContext context(*mPropertyTypes, dir.path()); + context.setRecursiveBehavior(ExportContext::RecursiveBehavior::TypedListValues); const QJsonArray projectProperties = propertiesToJson(properties(), context); QJsonObject project { { QStringLiteral("propertyTypes"), propertyTypes }, @@ -131,7 +132,8 @@ std::unique_ptr Project::load(const QString &fileName) const QString projectPropertiesKey = QLatin1String("properties"); if (projectJson.contains(projectPropertiesKey)) { - const ExportContext context(*project->mPropertyTypes, dir.path()); + ExportContext context(*project->mPropertyTypes, dir.path()); + context.setRecursiveBehavior(ExportContext::RecursiveBehavior::TypedListValues); const Properties loadedProperties = propertiesFromJson(projectJson.value(projectPropertiesKey).toArray(), context); project->setProperties(loadedProperties); } diff --git a/tests/properties/test_properties.cpp b/tests/properties/test_properties.cpp index 75f3fc0150..325dba1138 100644 --- a/tests/properties/test_properties.cpp +++ b/tests/properties/test_properties.cpp @@ -1,5 +1,11 @@ +#include "map.h" +#include "mapreader.h" +#include "mapwriter.h" +#include "object.h" #include "properties.h" +#include "propertytype.h" +#include #include using namespace Tiled; @@ -252,6 +258,8 @@ private slots: void loadProperties(); void saveProperties(); + void roundTripListProperty(); + void typedListPassesThroughDefaultMode(); void mergeProperties(); void cleanupTestCase(); @@ -526,6 +534,75 @@ void test_Properties::saveProperties() // todo: test saving a class with nested class } +void test_Properties::roundTripListProperty() +{ + SharedPropertyTypes types(new PropertyTypes()); + auto classWithList = SharedPropertyType(new ClassPropertyType(QStringLiteral("ClassWithList"))); + classWithList->id = 1; + static_cast(*classWithList).members + .insert(QStringLiteral("items"), QVariantList()); + types->add(classWithList); + Object::setPropertyTypes(types); + + Properties properties; + properties.insert(QStringLiteral("ParallaxLayers"), + QVariantList { QStringLiteral("layer1"), QStringLiteral("layer2") }); + + QVariantList nested; + nested.append(QVariant::fromValue(QVariantList { QStringLiteral("a"), QStringLiteral("b") })); + nested.append(QVariant::fromValue(QVariantList { QStringLiteral("c") })); + properties.insert(QStringLiteral("Nested"), nested); + + QVariantMap classValue; + classValue.insert(QStringLiteral("items"), + QVariantList { QStringLiteral("x"), QStringLiteral("y") }); + properties.insert(QStringLiteral("Container"), classWithList->wrap(classValue)); + + // Round-trip through the JSON helpers (clipboard / project / world path). + { + const QJsonArray json = propertiesToJson(properties); + QCOMPARE(propertiesFromJson(json), properties); + } + + // Round-trip through the TMX XML reader and writer. + { + Map map(Map::Parameters{}); + map.setProperties(properties); + + QBuffer buffer; + buffer.open(QIODevice::ReadWrite); + MapWriter writer; + writer.writeMap(&map, &buffer); + + buffer.seek(0); + MapReader reader; + auto loaded = reader.readMap(&buffer, QString()); + QVERIFY(loaded.get()); + QCOMPARE(loaded->properties(), properties); + } + + Object::setPropertyTypes(SharedPropertyTypes(new PropertyTypes())); +} + +/** + * The XML reader builds an ExportValue whose value is already a QVariantList + * of typed values (not the TypedListValues wrapper form). Verify that under + * the default NoRecursion behavior toPropertyValue passes such a list through + * untouched. + */ +void test_Properties::typedListPassesThroughDefaultMode() +{ + ExportContext context(mTypes, QString()); + + ExportValue exportValue; + exportValue.typeName = QStringLiteral("list"); + exportValue.value = QVariantList { QStringLiteral("a"), QStringLiteral("b") }; + + const QVariant result = context.toPropertyValue(exportValue); + QCOMPARE(result.toList(), + (QVariantList { QStringLiteral("a"), QStringLiteral("b") })); +} + void test_Properties::mergeProperties() { PropertyTypes types; From 3c56e128fafc5071e9cc5bc086b2c0b0e43f31d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thorbj=C3=B8rn=20Lindeijer?= Date: Fri, 22 May 2026 09:27:34 +0200 Subject: [PATCH 16/21] Updated dependencies mentioned in the README.md (#4470) * Minimum version updated to Qt 5.15.2 * Ubuntu/Debian packages updated to Qt 6 * Arch Linux packages update to Qt 6 * Homebrew packages extended with qttools, necessary for lrelease (cherry picked from commit 7f243af4da9084fce704887bab3fa7537ac9a268) --- .github/workflows/ubuntu.yml | 4 +++- README.md | 8 ++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ubuntu.yml b/.github/workflows/ubuntu.yml index 916af2a9c8..f876cf3ec5 100644 --- a/.github/workflows/ubuntu.yml +++ b/.github/workflows/ubuntu.yml @@ -35,12 +35,14 @@ jobs: - name: Install dependencies run: | sudo apt update - sudo apt install qtbase5-dev libqt5svg5 qttools5-dev-tools zlib1g-dev qtdeclarative5-dev qbs python3-dev + sudo apt install qt6-base-dev libqt6svg6 libqt6test6 qt6-l10n-tools qt6-declarative-dev zlib1g-dev qbs python3-dev + sudo update-alternatives --install /usr/bin/qmake qmake /usr/bin/qmake6 100 - name: Setup qbs run: | qbs --version qbs setup-toolchains --detect + qbs config defaultProfile x86_64-linux-gnu-gcc-14 - name: Build run: | diff --git a/README.md b/README.md index 52a3122cfb..c667a2cde0 100644 --- a/README.md +++ b/README.md @@ -42,14 +42,14 @@ and a free code signing certificate by the Compiling Tiled ------------------------------------------------------------------------------- -Before you can compile Tiled, you must ensure the Qt (>= 5.12) development +Before you can compile Tiled, you must ensure the Qt (>= 5.15.2) development libraries have been installed as well as the Qbs build tool: -* On Ubuntu/Debian: `sudo apt install qtbase5-dev libqt5svg5 qttools5-dev-tools zlib1g-dev qtdeclarative5-dev qbs` +* On Ubuntu/Debian: `sudo apt install qt6-base-dev libqt6svg6 libqt6test6 qt6-l10n-tools qt6-declarative-dev zlib1g-dev qbs` * On Fedora: `sudo dnf builddep tiled` -* On Arch Linux: `sudo pacman -S qt qt5-tools qbs` +* On Arch Linux: `sudo pacman -S qt6-declarative qt6-svg qt6-tools qbs` * On macOS with [Homebrew](https://brew.sh/): - + `brew install qbs` (which automatically pulls in `qt`) + + `brew install qbs qttools` (which automatically pulls in `qt`) If you want to build the Python plugin, you additionally need to install the Python 3 development libraries: From 854a5c122e755de6b04643f3120e20a3d8be877e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thorbj=C3=B8rn=20Lindeijer?= Date: Sat, 23 May 2026 01:14:40 +0200 Subject: [PATCH 17/21] Fixed resolving of class members values in lists (#4525) The ExportHelper forgot to recurse into list values. Also documented exportValueToJson's NoRecursion precondition and tweaked some comments in MapToVariantConverter. (cherry picked from commit c6763012103dccf372df5271217c6bc7e24e225e) --- NEWS.md | 1 + src/libtiled/maptovariantconverter.cpp | 6 +++--- src/libtiled/propertytype.cpp | 8 ++++++++ src/tiled/exporthelper.cpp | 11 +++++++++++ 4 files changed, 23 insertions(+), 3 deletions(-) diff --git a/NEWS.md b/NEWS.md index 90bb49f004..af95f070b3 100644 --- a/NEWS.md +++ b/NEWS.md @@ -5,6 +5,7 @@ * Fixed ability to change properties after deselecting the current object (#4440) * Fixed Properties view getting stuck with updates disabled (#4506) * Fixed copy/paste of list properties losing item types (#4514) +* Fixed resolving of class members values in lists on export (#4525) * Fixed locale-aware parsing of numbers in expression-capable spin boxes * Fixed Tile Animation Editor update after tileset image reload (#3923) * Fixed runtime language switching in many editor widgets and models (by SIDDHAARTHAA, #4411) diff --git a/src/libtiled/maptovariantconverter.cpp b/src/libtiled/maptovariantconverter.cpp index 75cb83ab45..7d48213640 100644 --- a/src/libtiled/maptovariantconverter.cpp +++ b/src/libtiled/maptovariantconverter.cpp @@ -365,9 +365,9 @@ QVariant MapToVariantConverter::toVariant(const Tileset &tileset, QVariant MapToVariantConverter::toVariant(const Properties &properties) const { QVariantMap variantMap; + + // In the JSON1 format, none of the export values retain their type, we use the values only. ExportContext context(mDir.path()); - // The JSON1 format can't represent typed lists, but classes still need - // their members converted to scalars. context.setRecursiveBehavior(ExportContext::RecursiveBehavior::ValuesOnly); Properties::const_iterator it = properties.constBegin(); @@ -818,7 +818,7 @@ void MapToVariantConverter::addProperties(QVariantMap &variantMap, ExportContext context(mDir.path()); if (mVersion == 1) { - // The JSON1 format can't support typed lists + // The JSON1 format doesn't include type information for list values or class members context.setRecursiveBehavior(ExportContext::RecursiveBehavior::ValuesOnly); QVariantMap propertiesMap; diff --git a/src/libtiled/propertytype.cpp b/src/libtiled/propertytype.cpp index c743be0f85..5205689749 100644 --- a/src/libtiled/propertytype.cpp +++ b/src/libtiled/propertytype.cpp @@ -308,8 +308,16 @@ static const struct { { ClassPropertyType::ProjectClass, QLatin1String("project") }, }; +/* + * Walks the value tree ourselves rather than relying on + * RecursiveBehavior::TypedListValues, because the property types JSON format + * spells the key "propertyType" while TypedListValues emits "propertytype". + * Hence the context is expected to be in NoRecursion mode. + */ QJsonValue exportValueToJson(const QVariant &value, const ExportContext &context) { + Q_ASSERT(context.recursiveBehavior() == ExportContext::RecursiveBehavior::NoRecursion); + const ExportValue exportValue = context.toExportValue(value); if (exportValue.typeName == QLatin1String("list")) { diff --git a/src/tiled/exporthelper.cpp b/src/tiled/exporthelper.cpp index 3a73780302..3a199d99b4 100644 --- a/src/tiled/exporthelper.cpp +++ b/src/tiled/exporthelper.cpp @@ -143,6 +143,17 @@ const Map *ExportHelper::prepareExportMap(const Map *map, std::unique_ptr & static bool resolveClassPropertyMembers(QVariant &value) { + // Recurse into list values + if (value.userType() == QMetaType::QVariantList) { + bool changed = false; + auto list = value.value(); + for (auto &item : list) + changed |= resolveClassPropertyMembers(item); + if (changed) + value = list; + return changed; + } + if (value.userType() != propertyValueId()) return false; From cbe80a5b76928cfcd8e3aede7902d24d5253e004 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thorbj=C3=B8rn=20Lindeijer?= Date: Sat, 23 May 2026 02:00:57 +0200 Subject: [PATCH 18/21] CI: build snap without snapcore/action-build (#4527) Replaced with a direct snapcraft invocation using --destructive-mode, which builds on the runner host instead of in LXD. Pin the job to ubuntu-24.04 so the host matches the snap's core24 base. Also dropped the dead snap_channel step, since we're not publishing the snap anymore since d3530f437. (cherry picked from commit f4fcfcf7b77af8ea82a2d8f41e658703c72ca812) --- .github/workflows/packages.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/packages.yml b/.github/workflows/packages.yml index 4dbfb53930..0b3d13c743 100644 --- a/.github/workflows/packages.yml +++ b/.github/workflows/packages.yml @@ -158,7 +158,7 @@ jobs: snap: name: Linux (snap) - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 needs: version env: @@ -170,15 +170,15 @@ jobs: with: fetch-depth: 0 - - name: Set Tiled version - id: version - run: | - if [[ "$TILED_RELEASE" == 'true' ]]; then echo "snap_channel=candidate" >> $GITHUB_OUTPUT ; fi - if [[ "$TILED_RELEASE" != 'true' ]]; then echo "snap_channel=beta" >> $GITHUB_OUTPUT ; fi + - name: Install snapcraft + run: sudo snap install --classic snapcraft - name: Build snap id: build - uses: snapcore/action-build@v1 + env: + SNAPCRAFT_BUILD_INFO: "1" + SNAPCRAFT_IMAGE_INFO: '{"build_url":"https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"}' + run: sudo snapcraft --destructive-mode - name: Upload snap artifact uses: actions/upload-artifact@v6 From 1fd984638f9898af920103799bcd68b467287295 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thorbj=C3=B8rn=20Lindeijer?= Date: Wed, 27 May 2026 11:34:34 +0200 Subject: [PATCH 19/21] Prepare for Tiled 1.12.2 release --- NEWS.md | 2 +- docs/conf.py | 2 +- org.mapeditor.Tiled.appdata.xml | 18 ++++++++++++++++++ tiled.qbs | 2 +- 4 files changed, 21 insertions(+), 3 deletions(-) diff --git a/NEWS.md b/NEWS.md index af95f070b3..1b1c93095a 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,4 +1,4 @@ -### Unreleased +### Tiled 1.12.2 (27 May 2026) * Added whitespace trimming for custom property names (with Praagya26, #4486) * Reduced animated tile marker opacity during terrain editing (by Huy Vũ, #4449) diff --git a/docs/conf.py b/docs/conf.py index 00bb2c0ef2..aa6ccd0b57 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -69,7 +69,7 @@ # The short X.Y version. version = '1.12' # The full version, including alpha/beta/rc tags. -release = '1.12.1' +release = '1.12.2' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/org.mapeditor.Tiled.appdata.xml b/org.mapeditor.Tiled.appdata.xml index f991a3fe08..fe9d869317 100644 --- a/org.mapeditor.Tiled.appdata.xml +++ b/org.mapeditor.Tiled.appdata.xml @@ -19,6 +19,24 @@ + + https://www.mapeditor.org/2026/05/27/tiled-1-12-2-released.html + +
    +
  • Added whitespace trimming for custom property names (with Praagya26, #4486)
  • +
  • Reduced animated tile marker opacity during terrain editing (by Huy Vũ, #4449)
  • +
  • Fixed ability to change properties after deselecting the current object (#4440)
  • +
  • Fixed Properties view getting stuck with updates disabled (#4506)
  • +
  • Fixed copy/paste of list properties losing item types (#4514)
  • +
  • Fixed resolving of class members values in lists on export (#4525)
  • +
  • Fixed locale-aware parsing of numbers in expression-capable spin boxes
  • +
  • Fixed Tile Animation Editor update after tileset image reload (#3923)
  • +
  • Fixed runtime language switching in many editor widgets and models (by SIDDHAARTHAA, #4411)
  • +
  • Fixed multi-object selection bounding box for point objects (by Mollah Hamza, #4401)
  • +
  • Fixed non-tile locations in wrapping Tilesets view being selectable (#3498)
  • +
+
+
https://www.mapeditor.org/2026/03/25/tiled-1-12-1-released.html diff --git a/tiled.qbs b/tiled.qbs index b197e9840a..f301d4ad00 100644 --- a/tiled.qbs +++ b/tiled.qbs @@ -6,7 +6,7 @@ Project { qbsSearchPaths: "qbs" minimumQbsVersion: "1.18" - property string version: Environment.getEnv("TILED_VERSION") || "1.12.1"; + property string version: Environment.getEnv("TILED_VERSION") || "1.12.2"; property bool snapshot: Environment.getEnv("TILED_SNAPSHOT") == "true" property bool release: Environment.getEnv("TILED_RELEASE") == "true" property string libDir: "lib" From 7a94dc4c79be7483d51f2ceb5bbf0fb499515c40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thorbj=C3=B8rn=20Lindeijer?= Date: Wed, 27 May 2026 17:08:13 +0200 Subject: [PATCH 20/21] CI: Corrected macOS version suffixes in artifact names The Qt 6.10.2 build's deployment target is macOS 13, but the artifact was named "macOS-11+". The Qt 5.15.2 build covers macOS 10.13 through 12, but the artifact was named "macOS-10.13-10.15". Updated both version_suffix values to match the actual minimum supported version, matching the manual rename done for 1.12.1. --- .github/workflows/packages.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/packages.yml b/.github/workflows/packages.yml index 0b3d13c743..a197a6f229 100644 --- a/.github/workflows/packages.yml +++ b/.github/workflows/packages.yml @@ -196,12 +196,12 @@ jobs: include: - qt_version: 5.15.2 qt_modules: "" - version_suffix: "10.13-10.15" + version_suffix: "10.13-12" architectures: x86_64 cmake_architectures: x86_64 - qt_version: 6.10.2 qt_modules: "qtimageformats" - version_suffix: "11+" + version_suffix: "13+" architectures: x86_64,arm64 cmake_architectures: x86_64;arm64 @@ -450,8 +450,8 @@ jobs: Tiled-${{ needs.version.outputs.version }}_Windows-10+_x86_64.msi/*.msi Tiled-${{ needs.version.outputs.version }}_Windows-7-8_x86.msi/*.msi Tiled-${{ needs.version.outputs.version }}_Linux_x86_64.AppImage/*.AppImage - Tiled-${{ needs.version.outputs.version }}_macOS-10.13-10.15.app/*.zip - Tiled-${{ needs.version.outputs.version }}_macOS-11+.app/*.zip + Tiled-${{ needs.version.outputs.version }}_macOS-10.13-12.app/*.zip + Tiled-${{ needs.version.outputs.version }}_macOS-13+.app/*.zip sentry: name: Create Sentry release From 255c83c1f98ba168defc39555362acc725b6d74f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thorbj=C3=B8rn=20Lindeijer?= Date: Wed, 27 May 2026 18:27:30 +0200 Subject: [PATCH 21/21] CI: Build snap for both amd64 and arm64 (#4532) --- .github/workflows/packages.yml | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/.github/workflows/packages.yml b/.github/workflows/packages.yml index 0d4373f925..480c912c48 100644 --- a/.github/workflows/packages.yml +++ b/.github/workflows/packages.yml @@ -157,10 +157,18 @@ jobs: path: Tiled-${{ needs.version.outputs.version }}_Linux_x86_64.AppImage snap: - name: Linux (snap) - runs-on: ubuntu-24.04 + name: Linux (snap, ${{ matrix.arch }}) + runs-on: ${{ matrix.runs_on }} needs: version + strategy: + matrix: + include: + - runs_on: ubuntu-24.04 + arch: amd64 + - runs_on: ubuntu-24.04-arm + arch: arm64 + env: TILED_VERSION: ${{ needs.version.outputs.version }} @@ -183,8 +191,8 @@ jobs: - name: Upload snap artifact uses: actions/upload-artifact@v6 with: - name: tiled_amd64.snap - path: tiled_*_amd64.snap + name: tiled_${{ matrix.arch }}.snap + path: tiled_*_${{ matrix.arch }}.snap macos: name: macOS (${{ matrix.version_suffix }})