-
Notifications
You must be signed in to change notification settings - Fork 27
Add dynamic options lists for blocks #206
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
Conversation
f6ed144
to
739c5d4
Compare
# FIXME: We should use some kind of prefix for these things. Is this okay? | ||
class_name EBC_BlockExtension |
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.
Yes I love the idea of a EBC_ prefix for every class that needs a name. We started to follow this pattern in f3cbacb for internal scripts:
const BlockExtension = preload("res://addons/block_code/code_generation/block_extension.gd")
But this doesn't allow extending the class like in extends EBC_BlockExtension
.
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 got rid of this here since there are dozens of other new things in this branch, but let's file away a wishlist item somewhere about adding a prefix :)
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.
Is that... a bike shed?! I feel like we either want underscores or camel case but not both. So, EbcSomeClass
seems like the natural continuation of the camel case way.
59f7c04
to
dd230f9
Compare
b553433
to
4a51075
Compare
Okay, finally back on track here. The remaining work is pretty much go through and delete some now-obsolete code, fix some GUT tests, reorganize commits, and fix one stubborn new bug (probably from clumsy rebasing) where a block script isn't loading if it has loose non-Entry blocks placed in it. While I was here, I tore out some unnecessary optimization around A brave person in the future might come back and improve our plumbing a bit so we're passing a single There's a bunch here, but one new thing to highlight: I didn't like how |
a48e1cb
to
c8bb9e3
Compare
c8bb9e3
to
bcf406e
Compare
One last opinionated change here: my template editor / options list stuff came with a side-effect in our generated code: the AST to code block stuff helpfully wraps strings in quote marks, except when those strings come from an options list. We can't do that anymore since the value it sees is always just a string now :) To address that, I added a way to specify a parameter substitution that won't be quoted: just Also, note there is a sneaky error which the CI doesn't like, printing an incomprehensible error message as a result. If you happen to spot what that is while I'm out on Friday I'll be very happy on Monday. |
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 haven't gotten too far, but I wanted to share some review so far since I have to run around a bit today.
var block_code_node: BlockCode | ||
var context_node: Node: | ||
get: | ||
return block_code_node.get_parent() if block_code_node is Node else null |
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.
Why is block_code_node
here? As far as I can tell, the only reason for it is one use of context_node
in Block._get_parameter_defaults
. It seems easy enough to add the context node to Block
.
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.
In an earlier version of this I had BlockFactory setting defaults from an extension script itself, but I ended up moving that to Block itself so this is indeed very superfluous here. It might need to come back later if we want our set of available blocks to involve other nodes in the scene, which I had in mind here but it isn't a thing we've reached any decisions on.
@@ -71,46 +44,46 @@ func get_categories() -> Array[BlockCategory]: | |||
return [] | |||
|
|||
|
|||
func generate_ast_list() -> ASTList: | |||
func generate_ast_list(block_factory: BlockFactory) -> ASTList: |
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 makes the serialization to code generation dependent on the UI again since BlockFactory depends on BlockCodeNode.
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.
Oops! The circular dependency will be resolved with the other change removing BlockFactory.block_node
, and I'll see if that lands us on block_factory
being a property of BlockScript (which would certainly make some sense).
_available_blocks.clear() | ||
_available_blocks.append_array(_get_default_block_definitions()) | ||
_available_blocks.append_array(_get_inherited_block_definitions()) | ||
_available_blocks.append_array(_get_variable_block_definitions()) |
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 looks like it's doing exactly the same thing that was in BlockScriptSerialization. Why was it moved here? It seems like you could continue to have all the block definition handling continue to live there. If the point is that we want to have a per-script set of block definitions, then the script object would seem to be the place to have that. What would be in BlockFactory would be BlockDefinition to Block conversion and category collection.
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'll shuffle this around a bit and see if I can flatten these things out a bit and it should reduce the diff a bunch. Mainly this happened because I was frustrated with the number of different places where we gather up block definitions, although that frustration was more with the code before #147. Looking at this at the moment, I'm extremely annoyed that _get_variable_block_definitions
is accessing block_script.variables
, so maaaybe BlockScript should be more involved there. I'll leave a note once when I've addressed that!
_init_block_definitions() | ||
|
||
|
||
func instantiate_block(block_definition: BlockDefinition) -> Block: |
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 was a static function before, but I don't see any reason why it isn't anymore. Now that it's a method, it requires a BlockFactory instance. That appears to be the only reason for Block.block_factory
.
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.
It ended up like that so we can get at the list of available categories and set Block.color
accordingly. This way we can lose Util.get_category_color
, and for instance, DragManager doesn't need to copy the color property in its copy_block
function. (Although actually I forgot to remove that).
But really the only reason for Block.block_factory
is so the ParameterOutput component has something on which to call _block_factory.instantiate_block_by_name()
. That whole scheme should be improved, but I think we'd want to dig in to DragManager to do it. For now, perhaps I can make ParameterOutput and DragManager get this information in a more special way (the current BlockCanvas could just be a static somewhere) so we don't need to have a general-purpose Block.block_factory
.
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.
Maybe the thing to do here is to keep a reference to either the current BlockScriptSerialization
or BlockCodeNode
instance. One or the other encompasses the block coding scene with which you'd have custom or property specific block definitions. With access to the current BlockScriptSerialization
, you can query or add BlockDefinition
s as needed.
instantiate_block_by_name
is really nothing more than finding the BlockDefinition
and creating a Block
scene from it. Similarly, the available categories are already stored in the BlockScriptSerialization
instance and getting the color is just a straight dictionary lookup.
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 is working like a charm for animations! I will leave to @dbnicholson the code review. I appreciate how the last commit improves the UI for the "is action pressed/released" block, which was extra large. On the other hand, I see a bug in that one putting the action name verbatim when it should be quoted for it to work:
This then fails with error:
Parser Error: Identifier "ui_up" not declared in the current scope.
7d48c1a
to
74aed6d
Compare
This is all rebased, passing tests, and ready to review :) |
6f7ae2d
to
1a6ceec
Compare
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 is great! I left some nitpicks and comments, but nothing that would stop this from being merged now. I'm going to leave this for a couple hours to see if @manuq has any further input, and then I'll merge.
_var_block_definitions.clear() | ||
for block_def in BlocksCatalog.get_variable_block_definitions(variables): | ||
_var_block_definitions[block_def.name] = block_def | ||
func instantiate_block(block_definition: BlockDefinition) -> Block: |
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'd still prefer the UI parts to be entirely out of here, but I think you've shed enough tears on this refactoring. I certainly like this much better than the previous scenario.
return block_code_node.get_parent() | ||
|
||
|
||
func set_block_code_node(value: BlockCode): |
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 could probably be the setter for block_code_node
, but no big deal. If it was the setter, then I don't think you even need the internal _block_code_node
.
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'll do that :) I had it as a setter at one point and then it was doing other stuff so I felt better with it being a function, but it isn't doing other stuff at the moment.
@@ -0,0 +1,38 @@ | |||
class_name BlockEditorContext |
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 whole class is lovely. Nice work!
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 second that!
@@ -95,4 +95,5 @@ shift_top = 20.0 | |||
[node name="SnapPoint" parent="VBoxContainer" instance=ExtResource("3_nhryi")] | |||
layout_mode = 2 | |||
|
|||
[connection signal="modified" from="." to="." method="_modified"] |
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 seems wrong. The modified
signal triggers _modified
, which emits modified
... Or am I reading this all wrong?
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.
Oh, yes, that was connected to the wrong thing! Ugh, that's a dreadful category of errors. I just remembered block.gd actually knows about its TemplateEditor now, so it can do all of the signal propagation in gdscript instead of the current scheme.
arg_name_to_param_input_dict = format_string(self, %HBoxContainer, definition.display_template, definition.defaults) | ||
|
||
|
||
static func format_string(parent_block: Block, attach_to: Node, string: String, _defaults: Dictionary) -> Dictionary: |
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.
So long, StatementBlock.format_string
. It was nice knowing you.
return | ||
|
||
if current_value is OptionData: | ||
# Temporary hack: previously, the value was stored as an OptionValue |
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.
s/OptionValue/OptionData/
@@ -114,13 +114,11 @@ func _append_input_parameter(parameter: Dictionary, id: int): | |||
parameter_input.placeholder = parameter["name"] | |||
parameter_input.variant_type = parameter["type"] | |||
|
|||
if parameter["is_option"] and default_value is OptionData: |
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.
FWIW, is_option
is never set in the current PR. I guess it must have existed in an earlier version.
@dylanmccall @dbnicholson I gave it a quick test, and found the following error. Adding a "change rotation by" to the "on process" leaves the EDIT In the output I see:
|
This changes BlockScriptSerialization to manage its own list of available block definitions and instantiate them, replacing the existing functionality that was in CategoryFactory and ui/util.gd. The change makes it easier to understand the state of the block catalog, because it is a component of the block script itself instead of spread across different static variables. In addition, this adds a singleton block editor context, which keeps track of the current block code node and its associated block script. The context object provides enough information for different UI components to update automatically when the block script changes. https://phabricator.endlessm.com/T35564
Instead, the list of blocks by class name is generated as needed inside get_blocks_by_class.
An extension script can implement functions which refine a block's presentation depending on the current context, such as the node it is attached to. https://phabricator.endlessm.com/T35564
This extension customizes the block's "animation" options list using the list of animations in the block script's context node. https://phabricator.endlessm.com/T35564
This new component replaces StatementBlock.format_string in favour of a more declarative API. https://phabricator.endlessm.com/T35564
Instead of inferring that there is a list of options based on the current value, ParameterInput expects to be given a list of options, and get_raw_input and set_raw_input understand that the raw input is a value from the list. In addition, change TemplateEditor itself to determine the list of options for a given parameter. This option data is always from the parameter_defaults property, regardless of the block's current value. https://phabricator.endlessm.com/T35564
If the current value is not already in the options list, add it to the list. https://phabricator.endlessm.com/T35564
Instead, we infer that a parameter has a set of options by the existence of an OptionData as its default value. In addition, add a way for a code template to include a plain, unquoted string by using `{{parameter}}`. Previously this happened automatically for any parameter whose value was an OptionData. https://phabricator.endlessm.com/T35564
It is possible for some options to be very long, causing a block to appear wider than necessary. https://phabricator.endlessm.com/T35564
Instead of getting specific blocks we know to have target_node_class "", change BlocksCatalog to include those blocks in the return value from get_inherited_blocks. This is possible due to the newly simplified implementation of _get_blocks_by_class. With this change, we can completely remove the list of default block definition names from BlockScriptSerialization.
With a block code script created using a previous version of the plugin, it is possible for a node's value to still be an OptionData object at the time BlockAST.format_code_template is called. https://phabricator.endlessm.com/T35564
Oops, that was a dumb mistake in 0bd3c26189adc755faa7bcc0d5973ef628879115. Fixed :) |
d4f9cb2
to
a14eed5
Compare
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.
All looks good from my side and the last bug I found is already fixed.
@@ -0,0 +1,38 @@ | |||
class_name BlockEditorContext |
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 second that!
@@ -0,0 +1,132 @@ | |||
@tool | |||
class_name TemplateEditor |
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 really like the TemplateEditor, much better than the formatting used before.
@@ -176,64 +175,6 @@ func load_object_script() -> Object: | |||
return null | |||
|
|||
|
|||
func _get_default_block_definitions() -> Array[BlockDefinition]: |
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.
Great code removal! Having this dynamic is much better.
if argument_value is OptionData: | ||
# Temporary hack: previously, the value was stored as an OptionData | ||
# object with a list of items and a "selected" property. If we are | ||
# using an older block script where that is the case, convert the | ||
# value to the value of its selected item. | ||
# See also, ParameterInput._update_option_input. | ||
argument_value = argument_value.items[argument_value.selected] |
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 don't think that we need to maintain backwards compatibility, there has been wild refactors since the last release. It doesn't harm to have this anyways.
We need a way to customize blocks depending on the node being edited. To address this, I found myself doing a few things:
CategoryFactory
with aBlockBuilder
that is instantiated for the currently selected block script. I don't super like how I did this - in particular that both picker.gd and block_canvas.gd instantiate their own (redundant)BlockBuilder
instances. Getting it right means further adjusting how data flows between these things.BlockBuilder
since it implies we're using the Builder pattern, which was my original intent but it is not what it is doing. Probably justBlockFactory
would make sense).BlockBuilder
has a single function which generates a single big list of blocks, including variable blocks. It knows what node our block script belongs to. I think that is a reasonable thing for it to know, but we could specify some rules here like "don't do anything that makes it impossible to move a block script to another node."setup_custom_blocks()
in node scripts.animationplayer_play.tres
. We should take the time to actually specify an API here, and perhaps aBlockExtensionScript
base class just to be sure. For now, I just haveanimationplayer_play.gd
withfunc get_defaults_for_node(context_node: Node)
. It's a little broken still.https://phabricator.endlessm.com/T35564