finale-lua / lua-scripts

A central repository for all Lua scripts for Finale.
https://finalelua.com
Creative Commons Zero v1.0 Universal
15 stars 14 forks source link

Mixins - Next Step? #173

Open ThistleSifter opened 2 years ago

ThistleSifter commented 2 years ago

Now that I've added most of the mixins that I had already planned, I just thought I'd post an issue to discuss what to do next with the mixin library. Here are some brief ideas that I had:

rpatters1 commented 2 years ago

My vote would be for a full wrapper of FCCustomLuaWindow with support for modeless and finenv.RetainLuaState. The requirements would be:

State includes:

If this is the first time running the script (i.e., finenv.RetainLuaState is false), the wrapper builds the window as it would now and builds the state alongside it.

If this is not the first time running the script (i.e., finenv.RetainLuaState is true), the wrapper builds the window using the state variables to initialize the controls and then moves the window to the saved x, y coordinates.

When the window closes and/or the script is ending, the wrapper captures the value of each control as well as the x, y coordinates.

The simplest script I know of that is doing this (without a wrapper of course) is transpose_by_step.lua.

ThistleSifter commented 2 years ago

Part of what you're after, as far as controls etc goes, is already happening. Run the following script in RGPLua and type things in the edit, then close and run it again.

local mixin = require("library.mixin")

if not finenv.RetainLuaState or not dialog then
    dialog = mixin.FCXCustomLuaWindow()
    dialog:CreateEdit(10, 10):SetText('hi')
    finenv.RegisterModelessDialog(dialog)
    finenv.RetainLuaState = true
end

dialog:ShowModeless()

That just leaves the window's position and size, which I'll look into.

rpatters1 commented 2 years ago

I haven't done anything with size, only with position. (Mine being dialog boxes with fixed control positions.) But I think the size functions are hooked up in the latest RGP Lua along with the position functions.

rpatters1 commented 2 years ago

Also, I love this example. I think it should probably be in the docs somewhere. (If it already is, my apologies. I've had my head buried in other project lately, but I mean to get back around to this.)

ThistleSifter commented 2 years ago

Thanks for the reminder about the docs, I've been meaning to update them but haven't gotten around to it yet.

Anyway, it turns out that the example above is somewhat of a fluke (the contents of an edit are only stored over if the text is manually set before the first time the window is shown) and the behaviour becomes even weirder for other controls like popups.

Is there anything you can do about it within RGP Lua? If not, I'll look into a solution in the mixins.

rpatters1 commented 2 years ago

@ThistleSifter As we discussed over on the PR thread, the PDK Framework is not designed with the ability to reuse FCCustomDialog or FCControl instances to open a 2nd or subsequent dialog after the first. It may work in some cases, but there are no guarantees. I'm having some success at the moment, but I realized something today that is possibly a bump in the road.

@Nick-Mazuk the mixin libraries for individual classes are not explicitly required in the code. Rather, the mixin library figures out which ones are needed and requires them by programmatically constructing their names and passing them to the require function. This works great, but it throws a wrench into our stand-alone deployment strategy. A simple scan of the source code does not reveal the library dependencies. If we are going to adopt mixins in a big way, we may need to come up with a different deployment strategy.

To give an example, the follow line of code kicks off a chain reaction of programmatically constructed require statements:

local window = mixin.FCXCustomLuaWindow()

The mixin library recursively attempts to require mixin class scripts all the way down the inheritance chain to __FCMBase. It's very cool, but it's also very hostile to stand-alone deployment. I'm not sure how to proceed.

Nick-Mazuk commented 2 years ago

Hmm… to get a single-file deployment, we need to "undo" all the require statements.

Background

For background with how it currently works, let's say you have the following library/foo.lua:

local foo = {}

foo.bar = function()
    return "hello world"
end

return foo

And now you have the following script my_script.lua:

local foo = require("library.foo")

print(foo.bar())

This currently will get bundled as the following bundled.lua, where essentially the contents of library/foo.lua will replace the line local foo = require("library.foo"):

let foo = {}

foo.bar = function()
    return "hello world"
end

print(foo.bar())

Proposed solution

So if we want to let people still download the script as a single file (preferred), we'll need to find a way to merge all the scripts into a single file.

Since the mixing imports seem to be dynamic, would it be possible to instead just bundle the entire mixin code into every file? That way we don't need to analyze any runtime require statements? We can just look to see if the mixins are imported. And if so, we bundle in all the mixins.

Do you foresee any issues with this approach?

rpatters1 commented 2 years ago

Dumping in all the mixin files is the best approach I can think of myself, though it will potentially create absurdly long deployed scripts. I have never seen any performance hit related to long scripts, but it is something to keep in mind as we go down the path.

I keep circling around to thinking about luac as a way to make the scripts more of a black box experience for end users. Unfortunately, there is no simple way that I can figure out to support the plugindef function if it is in a compiled script file. Which ends up, at least so far as I can tell, leaving luac off the table.

I would like to convert my scripts with dialog boxes to use FCXCustomLuaWindow, and that will be an opportunity to see how well it all works in real life.

Nick-Mazuk commented 2 years ago

Yeah, file sizes could get quite large. Luckily, I found a library to minify the source code:

https://github.com/mathiasbynens/luamin

It'll still keep an absurd amount of unused code, but the file size should still be smaller.

I think I'll also experiment if a dedicated Lua bundler will work (e.g., https://github.com/Benjamin-Dobell/luabundle). Perhaps it'll be smart enough to only bundle the necessary files? From looking at the mixin code, I don't think it'll do that, but perhaps it might.

ThistleSifter commented 2 years ago

I just remembered this comment https://github.com/finale-lua/lua-scripts/pull/144#issuecomment-1069101364 and I was wondering if it was suggesting the possibility of bundling mixins with RGPLua?

I've never played around with luac so I don't know how it works, but I just had this passing crazy idea. I'm assuming that even though it's compiled, the debug library still works. And from that, I was wondering if the concept of a line still exists at some level, since a number of the functions in the debug library deal with lines. So to extract plugindef, I was wondering if you could execute it in a sandbox of sorts, adding a hook for line change and just wait until either the plugindef function has been registered or the finale namespace is accessed, at which point the script is terminated.

Edit: Still with the sandbox idea, you could replace the finale namespace with a dummy object and when it's accessed for the first time, check if there's a plugindef function and if there is, call it and do whatever processing needs doing. If by then there is no plugindef defined, either it doesn't exist or the author didn't follow the guidelines. And then you just terminate.

Nick-Mazuk commented 2 years ago

Just chiming into say that the bundler now handles mixins properly and they can be used in future code.

rpatters1 commented 2 years ago

I was wondering if you could execute it in a sandbox of sorts, adding a hook for line change and just wait until either the plugindef function has been registered

That's actually quite an interesting idea. One reason it could be trouble (assuming it actually works) is if the script does something before registering the plugindef function. Right now, RGP Lua (and also I speculate, JW Lua) does not care where in the script you put plugindef. But for this approach to be safe, the script would have to put it first.