From 68046d58e34523df6da715231fc55a814d2ec711 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thorbj=C3=B8rn=20Lindeijer?= Date: Thu, 17 Sep 2026 12:29:20 +0200 Subject: [PATCH 1/2] Scripting: Raise an error when setting properties on read-only objects (#4614) setProperty and setProperties did not check whether the asset is read-only when there was no document, unlike removeProperty and setClassName. This allowed modifying the map or tileset passed to the write function of a scripted format, which bypasses the undo stack of the document being saved. Now they raise the "Asset is read-only" error as well. --- src/tiled/editableobject.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/tiled/editableobject.cpp b/src/tiled/editableobject.cpp index c868124ef4..59913e66f2 100644 --- a/src/tiled/editableobject.cpp +++ b/src/tiled/editableobject.cpp @@ -53,7 +53,7 @@ void EditableObject::setPropertyImpl(const QString &name, const QVariant &value) { if (Document *doc = document()) asset()->push(new SetProperty(doc, { mObject }, name, propertyValueFromScript(value))); - else + else if (!checkReadOnly()) mObject->setProperty(name, propertyValueFromScript(value)); } @@ -67,7 +67,7 @@ void EditableObject::setPropertyImpl(const QStringList &path, const QVariant &va if (Document *doc = document()) // todo: add support for array indices in path asset()->push(new SetProperty(doc, { mObject }, toPropertyPath(path), propertyValueFromScript(value))); - else + else if (!checkReadOnly()) mObject->setProperty(toPropertyPath(path), propertyValueFromScript(value)); } @@ -75,7 +75,7 @@ void EditableObject::setProperties(const QVariantMap &properties) { if (Document *doc = document()) asset()->push(new ChangeProperties(doc, QString(), mObject, propertyValueFromScript(properties))); - else + else if (!checkReadOnly()) mObject->setProperties(propertyValueFromScript(properties)); } From d853f582ef5138d30fe7c4f9c36a25702bb6ccfc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thorbj=C3=B8rn=20Lindeijer?= Date: Thu, 17 Sep 2026 12:32:59 +0200 Subject: [PATCH 2/2] Scripting: Added Tileset.selectedTilesChanged signal (#4615) Forwards the selection change signal of the tileset document, so that extensions no longer need to poll Tileset.selectedTiles. The tileset editor now also avoids setting the selection on the document again after updating its view to match the document, which would have caused the signal to be emitted twice for a change made from script. --- NEWS.md | 1 + docs/scripting-doc/index.d.ts | 7 +++++++ src/tiled/editabletileset.cpp | 1 + src/tiled/editabletileset.h | 5 ++++- src/tiled/tileseteditor.cpp | 12 ++++++++++-- src/tiled/tileseteditor.h | 2 +- 6 files changed, 24 insertions(+), 4 deletions(-) diff --git a/NEWS.md b/NEWS.md index 714bb2f432..1b12dafb56 100644 --- a/NEWS.md +++ b/NEWS.md @@ -9,6 +9,7 @@ * Persisted collapsed state of the properties groups in the session (#4561) * Scripting: Added 'tiled.cell' function, 'cell.flags' property and 'TileLayerEdit.setCell' function (#4538) * Scripting: Added MapObject.resolvedClassName() (by MatusGuy, #4529) +* Scripting: Added Tileset.selectedTilesChanged signal (#4615) * Fixed crash when the selection becomes empty while starting a move (#4536) * Fixed Properties view update on 'Reset Template Instance' and 'Replace With Template' actions * Fixed restoring of the layout for maximized windows on startup (#4580) diff --git a/docs/scripting-doc/index.d.ts b/docs/scripting-doc/index.d.ts index ba2e6ebb07..7e8989010a 100644 --- a/docs/scripting-doc/index.d.ts +++ b/docs/scripting-doc/index.d.ts @@ -4288,6 +4288,13 @@ declare class Tileset extends Asset { */ selectedTiles: Tile[]; + /** + * The signal emitted when {@link selectedTiles} changes. + * + * @since 1.13 + */ + readonly selectedTilesChanged: Signal; + /** * Constructs a new Tileset. */ diff --git a/src/tiled/editabletileset.cpp b/src/tiled/editabletileset.cpp index 008ab6412e..ef0e6a7dc0 100644 --- a/src/tiled/editabletileset.cpp +++ b/src/tiled/editabletileset.cpp @@ -422,6 +422,7 @@ void EditableTileset::setDocument(Document *document) connect(doc, &TilesetDocument::tilesAdded, this, &EditableTileset::attachTiles); connect(doc, &TilesetDocument::tilesRemoved, this, &EditableTileset::detachTiles); connect(doc, &TilesetDocument::tileObjectGroupChanged, this, &EditableTileset::tileObjectGroupChanged); + connect(doc, &TilesetDocument::selectedTilesChanged, this, &EditableTileset::selectedTilesChanged); connect(doc->wangSetModel(), &TilesetWangSetModel::wangSetAdded, this, &EditableTileset::wangSetAdded); connect(doc->wangSetModel(), &TilesetWangSetModel::wangSetRemoved, this, &EditableTileset::wangSetRemoved); } diff --git a/src/tiled/editabletileset.h b/src/tiled/editabletileset.h index 21f8422183..ca1cd18493 100644 --- a/src/tiled/editabletileset.h +++ b/src/tiled/editabletileset.h @@ -60,7 +60,7 @@ class EditableTileset final : public EditableAsset Q_PROPERTY(QColor backgroundColor READ backgroundColor WRITE setBackgroundColor) Q_PROPERTY(bool collection READ isCollection) // deprecated Q_PROPERTY(bool isCollection READ isCollection) - Q_PROPERTY(QList selectedTiles READ selectedTiles WRITE setSelectedTiles) + Q_PROPERTY(QList selectedTiles READ selectedTiles WRITE setSelectedTiles NOTIFY selectedTilesChanged) Q_PROPERTY(Tileset::TransformationFlags transformationFlags READ transformationFlags WRITE setTransformationFlags) public: @@ -188,6 +188,9 @@ public slots: void setBackgroundColor(const QColor &color); void setTransformationFlags(Tileset::TransformationFlags flags); +signals: + void selectedTilesChanged(); + protected: void setDocument(Document *document) override; diff --git a/src/tiled/tileseteditor.cpp b/src/tiled/tileseteditor.cpp index 280385d60c..2cf9bb206a 100644 --- a/src/tiled/tileseteditor.cpp +++ b/src/tiled/tileseteditor.cpp @@ -562,6 +562,10 @@ void TilesetEditor::selectionChanged() updateActions(); + // The view is being updated to match the document + if (mSynchronizingSelection) + return; + const QItemSelectionModel *s = view->selectionModel(); const QModelIndexList indexes = s->selection().indexes(); if (indexes.isEmpty()) @@ -574,7 +578,7 @@ void TilesetEditor::selectionChanged() if (Tile *tile = model->tileAt(index)) selectedTiles.append(tile); - QScopedValueRollback settingSelectedTiles(mSettingSelectedTiles, true); + QScopedValueRollback synchronizingSelection(mSynchronizingSelection, true); mCurrentTilesetDocument->setSelectedTiles(selectedTiles); } @@ -655,7 +659,8 @@ void TilesetEditor::tilesetChanged() void TilesetEditor::selectedTilesChanged() { - if (mSettingSelectedTiles) + // The document is being updated to match the view + if (mSynchronizingSelection) return; if (mCurrentTilesetDocument != sender()) @@ -671,6 +676,9 @@ void TilesetEditor::selectedTilesChanged() tileSelection.select(modelIndex, modelIndex); } + // Avoid setting the selection on the document again from selectionChanged + QScopedValueRollback synchronizingSelection(mSynchronizingSelection, true); + QItemSelectionModel *selectionModel = tilesetView->selectionModel(); selectionModel->select(tileSelection, QItemSelectionModel::SelectCurrent); if (!tileSelection.isEmpty()) { diff --git a/src/tiled/tileseteditor.h b/src/tiled/tileseteditor.h index f36805e506..d9df5b1647 100644 --- a/src/tiled/tileseteditor.h +++ b/src/tiled/tileseteditor.h @@ -182,7 +182,7 @@ class TilesetEditor final : public Editor TilesetDocument *mCurrentTilesetDocument = nullptr; Tile *mCurrentTile = nullptr; - bool mSettingSelectedTiles = false; + bool mSynchronizingSelection = false; }; inline QAction *TilesetEditor::addTilesAction() const