Building Custom UI Elements with Lua for GrandMA3 Part 3 - A Streamlined Method
Hey lighting folks! The following is the transcript from my YouTube video, Lua for MA3 Custom UI Elements Pt 3 - A Streamlined Method
This video is part of my Building Custom UI Elements with Lua for GrandMA3 series. Please check out my channel, From Dark To Light, on YouTube, and you can find the code to go along with my tutorials here on GitHub.
Okay, how on earth are the built-in MA3 functions able to take so little information and make a functioning UI element that looks good every time, no matter how you've customized it? It's actually way more efficient than you would think and today I'm going to explain in detail how I've managed to make my own work, in my opinion, even better than MessageBox and so can you.
Hello lighting people, welcome back to my short series on building custom UI elements. In the last video I showed you how to make them and where to find examples that you can use in the MA lighting folder on your PC. I hope you're fairly confident with the way these work at this point. If you're not, you may feel more comfortable reviewing that video before continuing with this one just because I'm not going to review the basics this time around. Today I'm going to be showing you how to build a streamlined function that allows you to quickly call and customize it to meet your needs in any other plugin. You'll be able to reuse your one function over and over again in many different ways while also using less code just in that one function than anything you would think is possible so far.
Now I did take the
plugin from the other day and just copy it into a new file. I removed
the comments and we're going to play with this file today. So the
first thing we're going to have to do is add an argument to this
function. I'm going to also rename the function. I'm going to call it
CustomChecklist() because that's what it is and then I'm going to add
an argument and I'm going to call it Table with a capital T. You do
not want to use a lowercase t for this name because table with a
lowercase t is the name of a Lua function so you can mess things up
if you try to use that. Now since I want to call this function from a
different one I'm going to scroll down to the bottom and remove the
return command which is wrong anyway now that I change the name and
I'm going to instead create a separate main function and use it to
call custom checklist.
I'm going to leave the parentheses empty
at the moment but I'll be changing that soon. For now, I need to
start customizing some information I'll be receiving from this
argument. The first thing that comes to mind that I want to customize
is the title and button text based on what I put in the argument.
This is super simple. I’m going to find my title bar element right
here and change the text which is actually in the title bar icon from
this string to table.title. I’m choosing that I'm going to use the
word title to define this and just so I don't forget I'm actually
going to go down to where I'm calling this and insert a table with
this value in my calling function. That's going to look like table
braces and then I'm just going to put “title = ‘My new title’”
because I can right?
I would normally want to do the same thing for the apply button text but since it's just repeating the exact same process I'm going to leave that out of this function and move on to the next thing which is making the dialogue width customizable. Similar process but in this case I want it to have a default value so that if I want to let it use the default and not actually have to put that value in my table I can leave it out and not get an error. To do that I'm just going to add an if statement at the top of my function it's going to look like… Actually I'm going to delete this because we're not going to want that anymore.
“If (table.dialogWidth == nil) then
DialogWidth =” - I'm going to put 700 for my default
“else
DialogWidth = table.dialogWidth”
and I'm going to change this to capital D. So essentially what's happening here is we're removing this from being just a table value and we're going to put it into a global variable and we're going to make sure that that has a value, so we're going to go to let's see where does our dialogWidth go the dialogWidth is this number right here baseInput.W so I'm going to change this to dialogWidth and now by the time we get here this has already been set if it was not set by our table it will be set and then we will put that here so that's great.
Now, I am very ambitious and the dream I had for this function is that it would be possible to give it checkboxes and subtitles with text information and optional position information in case I wanted to find where it will be located otherwise the default position will be in a single column in the order in which they're listed. The check boxes and subtitles would be created only if I define at least a name for them. I think this will make more sense if I first show you how I intend to provide the information to this function when I call it. My table is going to have the title which I already put in it and then I obviously I can add dialogWidth if I want to, in this case I'm not going to at the moment then I'm going to add an elements table with the information about the elements inside of it, so it's going to look like this.
So this is what it's going to look like. Oh, I don't like the fact that it did that that way.
I'm going to have my custom checklist function look into the elements table, which is inside of here. So we have this table and then within that, we're going to look for the elements table and we're going to create an element based on whether the type is subtitle or checkbox and then apply the defined name and position information.
It's also optional, but you can add a state to here, which would be, I mean, it could be anywhere in here, but for the checkbox, you can add a state, which would be either zero or one. And actually, I'll go ahead and add that.
I'll set some code to set the defaults for the position and the states later on, but the first step to make all this happen would be to create a function that will add a UI element, only if it's called, and a for loop that will call the function only if the information is given for the element. Let's try that. I'm going to start with the checkbox. So I'm going to go up here and as you can see, I have my checkbox grid, which I do want to keep that. However, the number of rows and columns is going to have to be created differently. But for the checkbox, I'm going to be changing a lot of this information. I think I'll keep this kind of as a template, but this is not what it's going to look like.
So I have all of this data written out. This is what's necessary to make a checkbox. I'm going to place this first inside of a function, which will be called, ActivateCheckbox(). So let's go with
“local function ActivateCheckbox()”
Oh, actually I do want an argument. Argument will be “index” and then just because the way I wrote this, it didn't add an end for me. So I'm going to cut this, paste it and, this is annoying. You can select these rows and then press tab and that moves them over one. And then I'm going to add my end, and then I have this function right here, which currently it's slightly grayed out because of the fact that it's not being called from anywhere, but that's not going to be a big deal at the moment.
This argument I added, index, is very important because it is going to be the thing that determines that each created element is separate and they're not overriding each other. Now I’m going to name the created object “Element[index]” so I'm going to start here and actually now that this is inside a function, this should not be local. Let's go with “Element[index]”
So our Element[index] is going to be equal to this checkbox grid thing. It's complaining at me because these functions are unused. That's fine. It'll be used. And then checkBox1, actually I can just change all occurrences to “Element[index]” and that's cool. This one down here I don't want to change, but I'll fix that later.
I'm going to replace these anchors with variables. Now to do that, you actually can't write it this way. So it's going to be a little more complicated. I'm going to make a table and it's going to say “top.” And we're going to say, this is going to be the vertical position. So I'm going to set it to ElementPositionV[index].
And then I'm going to set, I'm not entirely sure why this has to be this way, but you have to do top and bottom for the vertical position. And then same thing. And then we're going to do left, and this will be ElementPositionH and the same thing for right.
Okay and then for the text, I'm going to replace this with “table.elements[index].name” and the state is going to be CheckboxState[index].
Now I'm sure you're wondering why the checkbox state and the element positions aren't defined directly by the table like the name, but don't worry, it will make sense soon. I'm going to repeat this process to make an activate subtitle function again with this index and all that.
I'm going to actually, yeah… I'll do it right after just cause that's the way it works in my brain.
Copy this. Or actually cut it.
And now I'm going to have to change this name also to Element[index].
That's annoying that it has like this and that, but… Tell you what I'll do. I'll just change this one at the moment. And now I'll use a sneaky little trick and I'll include the period. It'll change this one, but I don't care cause I'm going to change that in a minute anyway. And, change all occurrences. There we go.
And, I'm going to remove this and replace it with “table.elements[index].name”
Looking through the rest of these, anchors will also need to be adjusted. I can just copy this information cause it's going to be exactly the same. And moving on. Okay, none of that needs to be changed.
Okay, now that I've done all of this with both of these functions, which are screaming at me, I'm going to create a for loop. Let's go down here and I'm going to do for,
“for i = 1, 100” should be sufficient. I don't think I can fit more than 100 elements even on the screen if I wanted to “do: and, I'm just going to write all this out really quick.
So, now I think you can see what this is doing for the most part. Obviously, here's where I'm dealing with the checkbox states and taking the information from the table to make sure they're set properly. I was also having issues trying to use direct argument table values when trying to change the checkbox states later, so this ended up working nicely. And, I'll show you a neat trick for saving states and recalling them.
So whenever it's going through this, it starts with a “for i = 1, 100” it goes, “Alright, if this table element is not equal to nil, we'll go through this, otherwise we'll break.” And, what it's looking for is, first, if the type is a checkbox, then we set a variable for the state and we activate checkbox. If it's a subtitle, then we activate the subtitle. Pretty simple. In either case, we are giving it as the index, the number of the element in the table, so we can keep track of them. So then, what happens is, when a checkbox element is defined, and the element is not nil, then if it is a checkbox, it calls the activate checkbox function and it has all this information. If it's a subtitle, then it calls the activate subtitle function and it has all this information.
Cool. Now, what about the positions? I found the best way to handle position defaults the way I wanted to was by putting this for loop near the beginning of my function. I'm going to go up here and I'm going to create a for loop. I'm going to put it down right here and I will just type it out real fast.
So essentially we're just going through all existing table elements using the I 1 to 100 and if they've been defined, so they're not equal to nil, then setting the position values to one less than the defined one and if they're not defined setting the horizontal position to zero and the vertical position to the element's index minus one. Why the subtraction? Well, because the elements positions are numbered starting from zero rather than one. So the first row is row zero, the second row is row one and so on and the same with the columns. I didn't want to have to think about that when I'm calling the function so I have it built in to change it so I can make the first one number one and the second one number two and so on. The next thing this for loop does is set the DialogRows and DialogColumns variables. They need to have one added since the number of rows or columns is one more than the number of the last row or column. I'm using these variables down in the checkbox grid to make sure we'll have the right number of rows and columns to display all the elements.
So in checkboxGrid I'm going to set this to DialogColumns and I'm going to set this one to DialogRows.
We're almost done! One important thing to do is make sure the tables for things like CheckboxState, element positions, elements and dialog rows and columns are being set back at the beginning. I'm going to just set those which are tables to empty tables so they can have values added to them and then because of the way dialog rows and dialog columns variables are being set they're only going to work if I set them to zero at the beginning so I'll do that. So I’m going to actually do that just before this dialogue width setting, just setting the defaults.
Let's see, what are the ones I need to be setting? I need to set DialogColumns to zero. I need to set DialogRows to zero. I need to set ElementPositionH to an empty table. I need to set ElementPositionV to an empty table. And then, let's see, I know I'm missing something else. Probably all these ones that are yellow. Element. I need to set element to an empty table so let's do that.
Now all those error messages went away. I still have a few. What are they about? CheckboxState. Ah! I didn't capitalize this the same way as I did elsewhere so now that's fixed and then down here we have another one. This is saying that index is… Oh yeah I see what happened here. So I'm not going to want this any longer. I'm just going to do away with that entirely. That was from our old thing and then this one here is printing CheckboxState, which doesn't exist. I don't want to print CheckboxState. Let's get rid of that and I have more yellow things. What are these? Oh we're setting CheckboxState and CheckboxState doesn't exist. So again CheckboxState actually needs to be set at the beginning, to an empty table.
And now all of our stuff is gone. The only problem is it's complaining that this is a global with a lowercase. So as you can see that was super helpful using this MA3 API extension. That is what is giving me all this information. That just made finding all these things way faster and easier. I really wish I had this when I was building it the first time.
Okay so now the final thing is to make sure the calling function that would be this one right here; it's calling this. Make sure the calling function can retrieve the checkbox states after we close the dialog. After all, what good would the checkboxes be if we can't get any data from them?
Now, it’s not possible to get the checkbox states after the dialog closes. So we're going to have to update a variable for each checkbox state every time a checkbox gets clicked. Let's add a for loop to the checkbox clicked function that, as you can see, it's already changing the state as we see it on the screen. But this doesn't change anything that helps us. So let's try adding a for loop that's going to look like this.
That will do the trick! So all it's doing is taking the element and it's looking at the current state according to what the caller has changed and then it is changing the state for the checkbox with the matching index. That's why I'm doing a for loop is because I have to make sure it's the one with the matching index and then I'm changing that to match it. And yes every single time you check a single checkbox it is going to update the states on all of them which is a little overkill but this is a good way to do it so it works.
Okay, well this is working to give us the checkbox states within this plugin. Actually I think I might have done this wrong up here. This, all of these, should be local variables. So if I have this one set to local and this one set to local and this one set to local and so on.
Now what we have is... and this is how it should be. They should be local. You do not want these to be global variables. But if checkboxState is local and these states are in checkboxState and that's all local, then we're not going to be able to access this outside. So if we want to access these states from this function that called it, we're not going to be able to.
How do we get around that? Well, the proper way to do that is to have the code return checkboxState. It will just return this table. So you can do, like this, you can do “local myStates =” and then what's returned by CustomChecklist(), which would be this, goes into this variable. And you can deal with that as you would.
But this code should not be at the end of this function because it is not going to work properly. It's going to return these before you click the apply button and before you've made any changes. It's going to go through the whole thing, put this on the screen and then return checkboxState. That is not useful if you want to actually have the changes that you've entered in get back to you. So where do you do it?
You might want to try putting it inside of the apply button clicked function. But that won't work either because then it's returning the data from the button clicked function to the custom checklist function. And that doesn't help anything because it's not getting into this one.
No, it has to be coming from the custom checklist function and it has to wait to execute until after the apply button has been clicked. An if statement won't work because then it'll just find it false and not run again. It has to essentially be some type of loop. The way I solved it was like this. I set the local variable “continue” to false at the beginning and then I came down here and I made a little loop.
“repeat until continue” and inside of the button clicked function, I do “continue = true”
So now when you click the button, continue becomes true. This is just stalling endlessly until continue is true. And then it continues and we return the checkboxState.
A good way to check this whole function and a way that I often use it is to use the states retrieved from calling it once to call it again. So like this. I already have this “local myStates” right here, now what I’m going to do is, I’m going to come in here, I have my checkbox, I'm going to change the state to myStates[2]. Now that is important. You do not want this to be the first one because our first element is the subtitle and the way these are ordered is that the element number is given, not the number of the checkbox. Subtitles don't have states, but those numbers are just skipped. So I'm going to have this one set to myStates[2] and then let's create another one of these. I'm just going to close this and do another checkbox. Let’s do “type = ‘Checkbox’”…
That should do it. All right. Now we have two different checkboxes, so I mean, honestly I feel like that's enough to check it.
I'm going to go ahead and check it with just these ones that are currently in here. Let's go ahead and go over to MA3. And I'm going to use this macro and it's called part three example… Oh, it's saying there's a… Oh. My bad, I missed one little thing that I needed to do in here. So what I actually need to do is at the top of this function, “if (myStates) == nil then myStates = {}”
I don't think I can actually have this be local at that point. It's going to have to be global. But it is. It's giving me errors because of the way I named it. And I don't really care. I really don't. So now we have this and it doesn't matter that these indices don't have any information. They will default to zero if they're nil. But the table has to exist in order for us to access these indices. So let's go back and run this. We got another error. Let's go take a look at that. In line 41. ElementPositionV… i+1…
Man, well, if you stare at something long enough, you'll figure it out. I missed putting in the "i" after this, so it was setting it incorrectly, and this was not able to find it. Okay, let's go run that again.
And… That wasn't quite what I was expecting when I did this, but I understand, I think, why that happened. Let's go look at this code. I put the… I put them all vertically in the same row, and then I put them in different horizontal positions. So they should be displaying… This one should be right here. And I'm not quite sure why it's not… Let's look for… Where that text is going. So… It should be… Table.elements[index].name should be right there. But… Where it's going is just in the wrong place. I should check the anchors. The anchors appear to be correct.
Ah, okay, I did a little bit of looking around and troubleshooting and I figured out that I'm appending the subtitle to the wrong thing. It should be appending it to the checkboxGrid, not the dlgFrame. So, I'm going to fix that. And at this point, it should work beautifully. There we go. Very lovely. And so, it does have this extra space up here. And what's causing that is the fact that the dialog frame has three rows and we're only using two. This one's being appended onto the second row and nothing is being appended onto the first row. So, I'm going to go ahead and change this to two, delete this information, and this needs to be changed to “0,0” and then this one down here for the button grid needs to be changed to “0,1” so it'll be on the second row and then let's try that.
Okay, so I got the with the actual code fixed, but for some reason my macro is not working. It's giving me problems whenever I try to run the plugin. So I'm going to try running it from the plugins pool with the whole familiar copy-paste thing instead and see if that works.
And it did work wonderfully. So we've done that. So now I'm going to show you the point I was trying to make about checking boxes off. They're saving the states and that is beautiful.
So in other words, we can tell that the myStates thing is working and these states are getting saved not in the CustomChecklist plugin but in this plugin because we want them saved in here so that we can use the CustomChecklist function in other places and then we can have individual data stored in the calling functions that are using it.
Man, it took me so many hours to figure all this out and now it just works for me whenever I need a checkbox dialog. It's such a relief. I actually have a couple of other customizations included in mine but this is about all you really need to get started and understand what's happening.
I hope it will help you a ton with being able to build your own without too much trouble, and of course, you can also access the code I worked on in this video on GitHub if you want to work with that as a starting point in making your own.
Thank you for hanging out with me through this whole long explanation of everything! Let me know if you have any questions or suggestions for better ways to streamline it even more. I'd definitely be interested in doing that. I hope you have a fantastic week and enjoy playing with some custom UI elements of your own at this point!
Comments
Post a Comment