-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add object reference property type #2712
Add object reference property type #2712
Conversation
This commit was based heavily on simlrh's PR of the same name, mapeditor#1408. Addresses some parts of issue mapeditor#707.
Tiles and tile objects can now have object references, and the UI handles the new cases.
I don't understand what's going on with the build For clang on linux, it never compiles the new files.
And the makefile appears to be pulled from the master branch's cache?
qmake is called, but it only runs for 0.01 seconds. The other builds always run qbs. |
I think I found the issue. That one build process is using .pro files instead of .qbs files. I pushed an updated .pro file. Why is it the only one using .pro files...? Could we use |
@Phlosioneer As long as we're still using the .pro files we need to make sure they are working, which that one build helps to ensure. The other build that is still using qmake is the snap built on build.snapcraft.io (which of course should also be moved to Qbs). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for picking this one up again! I've commented a few quick observations.
Mostly small code size reductions and style tweaks. The biggest change is to use a QTreeWidget instead of a QTableWidget in the ObjectRefDialog. The layout of the ObjectRefDialog was also simplified: * Replaced "Filter:" label with placeholder text * Replaced "Clear" button with clear icon button in QLineEdit * Replaced QLineEdit with FilterEdit for additional keyboard shortcuts
@Phlosioneer Please be aware that I've pushed some changes that already addressed a few of my comments. For now though I'm out of time this week and will continue Tuesday next week, so feel free to work on remaining tasks like trying to use It would also be nice to use the icon associated with objects in the Objects view, instead of the "missing image" icon which appears to be used right now: |
Alright, thanks for the comments. I'll continue working on this. |
It crashes when trying to edit a pre-existing value, probably because a QModelIndex is being constructed with the default no-arg constructor. There is a visibility checkmark for all layers and objects that should be removed, and a position column that should be removed. The resize rules also need to be updated so that name and type get priority. ID should be the first column, if possible, as well. Layers can be selected, which shouldn't happen. The filter bar is broken.
Also handle deselection better, and print out a warning for unknown object references.
Hide position column, move the ID column to the first index, and set name to the Stretch resize mode.
Okay, assuming that passes the tests, that should address every point except the iterating-all-objects one, and the objects view image thing. |
In preparation for adding a tileId to ObjectRef
The tileset version of the coose-object dialog can get confused if starting with a blank ObjectRef. It loses track of which tile's objectGroup it should show.
This is a lot more work than I expected for a seemingly simple feature, but it turns out to be quite tricky. I'm learning a ton about Qt in the process and I'm getting very familiar with Tiled's codebase, though. It looks like another few commits' worth of work still to go. Trying to keep the commits small and descriptive to make reviewing this PR easier. |
That's great. I'm itching to help but only have a bit of time in the evenings until Tuesday. Indeed this feature is more work than I had expected as well, but it's also what you get when you want to make the best of it. After all the initial patch already got the job done. The object selection dialog is especially more tricky than I had anticipated. Yes, we already have a Btw, we don't need to use the
That's working out great, thanks! At the end we can either squash it all or make a cleaned up set of commits for merging. |
It's now a tree that shows all tiles with sub-objects. It no longer relies on the current TilesetDocument.
The property manager would sometimes have trouble finding the name of the referenced object on tilesets.
With those pushes, I'm almost done. I've just gotta address your comment just now, then I'm happy with it and you can review it on teusday.
Wasn't the point of the refactoring to avoid copying all the values? Creating a new instance of MapObjectModel every time we edit an objectRef property seems quite counter-productive.
With the route I went, I wasn't able to leverage them. I opted to have all objects on the tileset selectable, and we only have a dummy map document for the currently selected tile if I read the code correctly. Ideally, we can refactor the code in this PR and the dummy map models into one tileset map model that understands how tiles can have objects? The most recent pushes were quite messy because I couldn't use an existing abstractdatamodel.
I didn't know there were selectable flags. I'll go change that. |
😄
The dialog should have the objects grouped by tile ID, in a tree...? It even displays the user-provided "type" field for the tiles.
If we have a Tile* reference on it, then that reference either needs to be a cache value (set it when we need it), or it needs to be always-correct. Making it always-correct complicates map loading a lot. Property loading code now needs to know about the current tileset's pointer. And if you say "let's just leave it as a nullptr until after loading", we need to iterate over every property in every tile and every property of every object on those tiles to set the pointers after loading. So I went with the "Tile* is a cached value" route. I think that if we want to build anything on top of this feature in the future (like scripting), we need a less ambiguous identifier. Currently, code looking at just properties without respect for the object that owns the property (like propertybrowser) need to use It was also causing lots of corner cases when a property type (object ref) couldn't be placed on some things (tileset, wang set), and also couldn't be edited on multiple tiles simultaneously (if you select multiple tiles). |
I think In scripts, getting an I guess when you copy/paste an object reference, eventually we may want to include a file name and tile ID, so that references to objects in other files could be made, but for now I think it's fine to just copy the object ID only. |
I still think treating the disambiguating values as "cached" is going to hamper other features in the future. For example, scripts calling I think we have disagreeing opinions on this that aren't going to be settled, though. My priority right now is to have this feature in tiled. So if the above doesn't change your mind, I'll do it your way. |
Are you open for a video chat about this tomorrow? I think we may just have a misunderstanding rather than a disagreement. I try to clarify a little here:
That's right, but not if we introduce a separate struct for UI purposes.
The value of those properties would be immediately mapped |
As is, we currently just return the underlying QVariantMap.
Sure. I'm available 12pm-9pm EST. |
Actually, even the current implementation fails when object refs are accessed through a property - Qt has no way to turn Edit: It also fails with File properties, spitting out a |
Hmm, could you be available a little earlier perhaps? I am working on Tiled until about 11am EST (17:00 CET). Though we could also try chatting in my evening, starting between 2-3pm your time.
That's true, custom types like |
* Move ID column to the right, because it looks strange to have it in to the left of the expanders. * Stretch both Name and Type columns but adjust ID column to the contents. * Expand all object groups to avoid having to expand them manually each time. * Removed infinite loop that appeared to be trying to select parent layers. * Scroll to the selected object.
* ObjectRef no longer includes a tile ID. I think there is not enough value in supporting references to collision objects on other tiles in a tileset, support for which adds quite a bit of complexity. * DisplayObjectRef is now used by the VariantPropertyManager, ObjectRefEdit and ObjectRefDialog. It's an ObjectRef + MapDocument* to give the UI enough context when displaying and editing the value. Conversion between ObjectRef and DisplayObjectRef happens in the PropertyBrowser, in to/fromDisplayValue.
An custom property of type 'object' is now converted to an EditableMapObject when passed to the script, and the other way around.
91f13bd
to
c2baa1b
Compare
* ObjectsFilterModel > ReversingRecursiveFilterModel * ImmutableRoleModel > ImmutableMapObjectProxyModel * Immutable map objects model is now even more immutable
@Phlosioneer I'll try to be around this evening. For this change, I think the main remaining change to do is the adjust the documentation. I hope you're also fine with these changes, but let me know if you have any concerns. |
XD
If we're doing it this way, there are some design challenges we still need to solve. ObjectRefs are not allowed on tilesets, terrains, wang sets, wang colors, or object templates. Here's a list of the corner cases:
On an unrelated note, I thought of a new corner case that both approaches will need to handle:
|
In c++11 (and *only* in that version), structs cannot be aggregate initialized if they have default member initializers.
src/tiled/editableobject.cpp
Outdated
case Object::LayerType: | ||
return static_cast<Layer*>(object)->map(); | ||
case Object::MapObjectType: | ||
return mapForObject(static_cast<MapObject*>(object)->objectGroup()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This feels like a property MapObject should have.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I guess there are lots of places where we want to get the map of an object already.
if (auto map = mapForObject(object())) { | ||
referencedObject = map->findObjectById(ref.id); | ||
} else if (object()->typeId() == Object::MapObjectType) { | ||
if (auto objectGroup = static_cast<MapObject*>(object())->objectGroup()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we want to support the case where there's a more complex detached layer tree? Currently, this just looks at the immediate parent of the map object. If a user does this:
root = new GroupLayer();
child1 = new ObjectLayer();
child2 = new ObjectLayer();
root.addLayer(child1);
root.addLayer(child2);
obj1 = new MapObject();
obj2 = new MapObject();
child1.add(obj1);
child2.add(obj2);
// What happens here?
obj1.setProperty("test", obj2);
// Or here?
root.setProperty("test2", obj2);
I think we should fallback on just returning the id as an integer. The user can do any combination of detaching layers, attaching objects, and setting properties.
We should make a best-effort to locate the correct object by id; then return just the id if we can't find the object. This would only ever happen on detached layers / detached objects. JS is dynamically typed, so the user can check Number.isInteger() when working with it.
When we add a detached layer or object to a map (or tile), we should do something like (python-pseudocode):
for object in added layers:
for property in object.properties:
if property.type == ObjectRef:
# NOTE: Do this after adding the new layers, so that it can find id's inside the newly added layers.
if object.asset() != asset() or not findObjectById(property.value.toInt()):
property.value = 0 # Unset
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess we'd have to add something like object.originalAsset()
though.
And if object.originalAsset()
is null, we should assume the object refs are intentional and try to resolve them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I only had tile object group in mind for this code, but indeed it also covers detached object groups. Sure we can make a best effort to handle detached hierarchies, and I like the idea of falling back to returning the ObjectRef
itself (not the ID, because we need to keep the type of the value intact).
I don't think we should be actively invalidating object references though. As it is, you can also copy/paste them and as long as there is an object with a matching ID, it will refer to it. That behavior is somewhat predictable and I think preferable over the IDs getting set to 0. We just need to make sure it doesn't crash.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's reasonable.
What do you mean by returning the ObjectRef, not the ID? The object ref can't be manipulated by JS at all. Perhaps we need an EditableObjectRef
class.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We just need to do something similar to what I did with FilePath
in change d13afac.
Another corner case I forgot about:
|
@@ -43,7 +43,7 @@ struct FilePath { | |||
}; | |||
|
|||
struct ObjectRef { | |||
int id = 0; | |||
int id; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure about this being only a C++11 problem. The code is compiled as C++14 everywhere and compiled fine with both GCC and clang (are you on Visual Studio?). That said, we can't just remove this, since we need to initialize somehow. Let's just have a constructor then.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
https://en.cppreference.com/w/cpp/language/aggregate_initialization
no default member initializers (since C++11, until C++14)
So I guess that includes C++14?
Also, it's supposed to be zero-initialized by the aggregate constructor:
If the number of initializer clauses is less than the number of members or initializer list is completely empty, the remaining members are value-initialized. If a member of a reference type is one of these remaining members, the program is ill-formed.
And on https://en.cppreference.com/w/cpp/language/value_initialization
1) if T is a class type with no default constructor...
2) if T is a class type with a default constructor that is neither user-provided nor deleted (that is, it may be a class with an implicitly-defined or defaulted default constructor), the object is zero-initialized and then it is default-initialized if it has a non-trivial default constructor;
3) if T is an array type...
4) otherwise, the object is zero-initialized.
So we don't need to provide a constructor to ensure id is set to 0.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, no, it didn't compile fine on MINGW. One of the automated tests failed with that error.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, no, it didn't compile fine on MINGW. One of the automated tests failed with that error.
Whoops, I guess I checked the CI builds after they had already run with your fix...
Thanks for pointing out the value-initialization for initializer lists! That does mean that without this explicit initialization, we need to avoid writing ObjectRef()
(currently see three occurrences).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That does mean that without this explicit initialization, we need to avoid writing
ObjectRef()
(currently see three occurrences).
So, actually according to https://en.cppreference.com/w/cpp/language/value_initialization, the form T()
also does value-initialization, so there is no need to change those occurrences.
Let's try to keep things simple. We can add a property of type 'object' anywhere. It just won't be possible to set it to a valid reference in all contexts. It's not a big deal, but trying to disallowing it is a pain and can even be an inconvenience to the user. Take the example of the tile properties or the object types editor. You'd say on tiles you can't refer to objects, and in the object types editor you can't either. But in both cases it can make sense to add a property of that type, so that once you instantiate a tile object, it will have already that property and there you can set a valid value on it. In the end, the So, there's probably some places where we need to avoid crashing and we could probably help the user realize he can't actually set a valid value in some contexts by disabling the edit widget, but I don't see big design challenges there. |
We still need to jump through some hoops though:
|
* Introduced MapObject::map() for convenience. * Added meta object to ObjectRef so it can be used in scripts. * Fall back to returning the ObjectRef for property values in scripts, when the object reference can't be resolved. In effect, the snippet "object.property('foo').id" will work as long as 'foo' is an object property, regardless of whether an object with that ID could be found. * Removed the check on same-asset when setting object properties. While it may be useful, it may also prevent the setting of a desired value just because a detached object was used to set a reference. * Fixed potential crash in DisplayObjectRef::object.
* Changed the validator back to QIntValidator * Disable "..." button when no mapDocument is available * Fixed setting of initial focus on text edit (was not proxied from the ResetWidget)
A way to create an ObjectRef value without having a MapObject.
85c9960
to
6dbbb61
Compare
This PR is based heavily on @simlrh's PR of the same name, #1408. It addresses all of @bjorn's comments on that PR.
This commit is a partial solution to #707. It allows a new property type for object ids. It also adds a dialog box to choose an object from a filterable, sortable list.