Building Custom UI Elements with Lua for GrandMA3 Part 2 - How to Make Your Own From Scratch

Hey lighting folks! The following is the transcript from my YouTube video, Lua for MA3 Custom UI Elements Pt 2 - How to Make Your Own From Scratch

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.


I'm about to show you everything you need to know to be able to build your own custom UI elements however you want them, to do whatever your individual needs are, rather than having to continue relying on the clunky built-in functions that don't always fit your individual use case.


Hello lighting people, welcome back to my mini-series on making custom UI elements. In the last video I explained what a custom UI element is and when you might want to use one instead of the built-in MA3 functions. Now I'm going to show you the basic building blocks used in making a UI element and where to find the details you need to make them if you don't know what you're doing. 


Let's take a look at a really simple window I built. I already loaded this into my A_art macro and I'm going to show you; this is, as I mentioned a super basic window, but this by itself actually required 77 lines of code just to create this very basic UI element. And this was a more streamlined version compared to how I originally was making them. Now I'm going to share with you a really wide overview of how I learned to make custom UI elements. There's a thread on the MA lighting forum where some people were discussing them and several files got shared. I simply downloaded one of the most advanced ones and started playing with it. It took a lot of trial and error to learn how it worked and in fact right up to the making of this video I was still learning new things and I don't know everything now. I doubt anyone ever could. When I was prepping for this video I actually asked someone in my Discord server who knows more about Lua than I do for some help and clarification on some things just so that I would be able to explain them better for you. And I'll state here now that I do not remember all of the details on how to build these from scratch on my own every time I'm programming. I do a lot of copying and pasting from my saved examples and then just tweak the settings for what I need. And there's no shame in doing that. If you understand what the code is doing, why not copy, paste and edit? At the very least it saves time. 

Okay, moving on. I'm going to show you the code I used to make this UI element and go over it a little bit. Now this might look a little bit overwhelming but I'm going to break it down for you and it shouldn't be. So the first thing I'm going to point out is I'm going to start with making an item. I set a local variable to the name of an item and that is baseInput in this case. And this name, it's a variable, could be whatever I wanted because it's a variable. Now it's actually a table, as my MA3 extension is showing you. It has all of this information kind of added into this table. And so the first thing we do is we set this table and we say we're going to get the display that's currently in focus. That's what this function does. And then we're going to find, within that, the UI object called screen overlay, which is something that exists in MA3. And then onto that screen overlay on the currently focused screen, we're going to append or add on an item called base input.

And this name comes from, it's the name of the type of object that we're appending and it comes from the MA3 system. I'll show you later where we find that. And then we have height, width, columns, rows, size, policy, size, auto close, different settings for this item, which in this case is base input.


And then on top of base input, we'll append a title bar and a title bar icon and a title bar close button. And then also on top of base input, we're going to append a dialog frame. And then on top of that, we'll append a UI object with a subtitle and so on. There's a whole bunch of appending that happens. So you're always going to find an object that exists and say, okay, now on top of that, I'm going to append this other thing. And basically in every instance, you're looking for an object. So you're going to create an actual object. This is a title bar. I'm going to add a title bar and then based on the settings that can be changed for a title bar, which is an existing object, I'm going to change those settings.


Now, the way these columns and rows work with the size policy and everything, is you have columns and you have rows and this, these indices right here basically let you select “column one, row one: size policy ‘fixed’ column one, row one: size ‘60’ column one, row two: size policy ‘stretch.’” 

And since it's a stretch, we're not going to give it a size because it's going to be based on what's inside of it.


Now I am going to talk about these things up here at the top. We have these four variables: pluginName, componentName, signalTable and myHandle, and this rather unfamiliar function called select.

Now the way this all works is still a little bit fuzzy to me, but I'm going to explain it as best I can. So essentially what we're doing is we're giving it this function, which allows it to have more arguments added later. So that is very important and as you can see in this particular plugin, we're not using pluginName or componentName. However, it's a good habit to have all of these in this type of plugin where you're building a UI element because they can be important, and this is just how all of MA3's built-in plugins are written. So I'm going to go ahead and plugins are written. So I'm going to stick with that format.


Keep in mind signalTable and myHandle and I'm going to scroll down and show you where we're using those. So right here we have an apply button that's an object. We have a button grid and we've appended a button on top of that and then there's this thing right here which says “applyButton.Clicked” and it says “ButtonClicked.” 

And so what that actually does, interestingly enough, is whenever you click on that button it sends this value to the signal table right here and then that can trigger a function. So we say here, whenever the signal table receives the value “ButtonClicked” then do this function, and in this case what it actually does is it gets the display and it clears, so it is going to clear this dialog box when you click the apply button.

The way that this knows that the signal table to be used for this signal is the one up here is by the plugin component information. So that is important. So as soon as you call the plugin, it sets this to that plugin component, and then we have that information right here. We’re referencing the signal table for this plugin component based on the fact that this has that data right here.


Now whenever you have this function called it receives an argument “caller” right here, saying what object, in this case “applyButton,” called it. This is because, in this case we only have one object that can call this function, but you could have it to where multiple objects could call the same function and then you could have it do a different action based on what object called it, or you could have it do an action to that object, and that is something I'll definitely be showing you soon.


There are multiple ways to clear the screen. This is not the only command to use, but this is the one that I like best and so this is the one I'm going to be working with. If you see other people do it other ways, that's fine, this one just happens to be my favorite.


Now I am going to go back and talk about just some of these different things. So, anchors for instance, is basically saying where on the object this is appended on, this goes onto. So I'm appending a title bar onto my base input. I have an object; it's called base input. Where on the base input do I append the title bar? “0,0” refers to the top left space, and so it's appending it in the top left. If you change this to a one then it would be going down one. And so, it is a little difficult to keep in mind that the first location referenced is always going to be zero. The second location is going to be ONE and so on, but whenever you are appending an item you're going to want to know that.


Whenever you have something like the title bar icon that has an icon object, you set the icon, but it already has information telling it where that icon is going to go. So yes I said you were building these from scratch, but there are some things kind of already built for you.



Now, I would like to take this plug in right here and start copying it to a second file to show you each piece one at a time and how some of it works. So I have this empty file over here. I'm going to start by copying this and… I'm going to start a new function. And now I'm going to go back and I'm going to start by just adding the base input right here. And now I'm going to go over to MA3 and add this item.


I'm getting an error, let me deal with that real quick.


Okay, here we go, let's run it and we just ended up with this blank black box. It's not very large. It's just sitting in the middle of our screen we can't click it or drag it or anything. I'm going to hit escape and that will close it for us and then let's go back over here.


So this is our base input right here we just have a little black box and I can tell you it's probably getting the width from right here 600 and then it's getting the size from here because the height is zero but then it has rows inside of it and one of them is set to 60. So it's getting a width of 600 and a height of 60 and that is what we're seeing and thankfully it's set to close on escape. I've never changed this setting; I don't know what would happen exactly if I change this setting. It would be definitely more difficult to close it so that's not cool. All right let's go back over here and add the title bar and see what happens.


Now as you can see it says the anchors are “0,0” and then it says we have two columns and one row and it has a size set for the second column. It doesn't have a size set for the first column, so the first column is going to be the rest of the space, whatever that is; the second column is just defined to 50.


We're seeing exactly the same thing before. I believe this is because the title bar by itself doesn't actually have any information to tell it to look different; it's basically more so a size and division kind of an element. So I'm going to go over here and I'm going to copy the title bar icon and put that in. And now let's try it. 

Aha so now we have the title bar icon is actually including this whole section which is the entire first column of our title bar. So our title bar is this whole section and this is 50 wide and so it's keeping this section separate because there's a second column and this is going in the first column and you can see that because it has the text in the title bar that's the text we were seeing, it has the texture for that corner, it has the anchors, and it has the icon which is the star.


Now I'm going to try changing this to 10 just to see what happens. This is going to try to put it into the second column of the first row and it doesn't fit very well. I can see the icon, it's showing me this rounded corner on the left hand side which is kind of weird since this is the right corner of my dialog.

Let's go back and change this back to zero and now I'm going to go ahead and copy my title bar close button. I'm going to paste that right here and go run that and now we have a complete title bar. This is my second column, this is my first column and this one is clickable. Now I can close it that way, super nice, and that is an attribute of the title bar close button object is that it is a close button and so clicking it does close the pop-up, and again we can see it's anchored at “1,0” so it's in the second column of the first row.


Let's go back here and add the dialog frame. Okay so now we haven't really added anything but we have a black box with a gray border around it, so it is adding this dialog frame to our thing. It basically has set it to have one column in two rows it's going to be anchored at “0,1” so going back, this is appended onto the base input, so on the base input this is in the second row, which is to stretch based on the size of the object put in it. So we have said that this one is divided into two rows, one of which is a fixed size of 60 and the other is a fixed size of 60, so the total size then is 120, but currently there's nothing sitting in those places, they're just an empty place, so let's go back here and add on a subtitle and let's see what happens. Yay! We now have a subtitle sitting in the top half of our dialog, and this still looks honestly kind of ugly; we just have a black box and it says “This example contains a subtitle” and it's not even centered, but we can go back here and talk about basically what it's doing. So all of these settings are referring to how much space it takes up and how it is sized in that box, and you can definitely play around with these and see what happens, but these settings typically work for me. 

And then we have the anchors, so it's in the first row and column of the dialog frame. Padding refers to how much space is placed on each side of the text, and so what I believe this is is like, top and bottom and then left and right; I haven’t actually fully confirmed that but you can adjust it to your liking. And then it has the font set, and there aren't a lot of options for fonts, but you will be able to find them in the MA3 folders on your computer. “HasHover no;” if we set this to yes, I just want to show you what happens. Now I can hover my mouse over it. Now it doesn't click, but it'll like, light up if I hover over it, which is interesting. So I'm going to go ahead and change this back to “no” because I don't really care to have it say yes, and then the back color in this case is transparent, which is why it shows up black, but if you wanted to change it to something else you could do that and it would be, it would be interesting. You could do some cool stuff, but you can make it any color in the MA3 color themes. So let's go back to over here and add a button grid, and pretty sure you can probably tell me already what's going to happen when I add this; it's not going to cause a noticeable difference, right? Right, absolutely nothing really happened that we can see, however it did add a row and a column and it anchored this in the second row of the dialog frame, which has this fixed size of 60. Now on top of this button grid we can add the apply button and yay, we have an apply button, but nothing's happening when I click on it, it's just there. That's kind of annoying. I can still close when I hit x. 

I just realized it's a little dumb that this button says “apply” because there's nothing to apply; I don't have check boxes or anything in it, but anyway, there's a button and nothing happens when I click it. It has settings about the anchor, so it's set in the first row. Well there's only one row, so I hope so. And then the text shadow. It has hover, which is why it lights up when I move my mouse over it, but I can't click it. It says the text is “apply” which is dumb, the font is “medium20,” the text is aligned in the center…  this is the British spelling of center, which is slightly annoying to me as an American, but here we go, and then it has the plugin component and button clicked information, so if this is saying if you click it to say button clicked, then why isn't anything happening? Wait a minute, we're missing something! “signalTable.ButtonClicked” oh we have to actually do something with the information that the button was clicked. Let's go back here and paste this code. Now let's try that and see what actually happens. Yeah, it closed it. That is super cool!


Obviously, you could have this function do other stuff too if you wanted to; that's not the only thing that you can do with the information that the button has been clicked, but in this case that's what I'm doing with it, so…


Now that we have a functioning UI element again I want to explain where I'm getting the objects I'm appending and defining and how I know what information each one needs to work. Basically there are files in your MA3 file structure on your PC that will give you some of this information. It's not simple or easy to read, but it works. Let's have a look now. On Windows the file path to check out is going to be posted on the screen for you to see and I'm also going to follow it to show you, but basically I'm going to go to my Windows drive and then I'm going to look for a folder called ProgramData. Now this folder is a little bit lighter in color than the others; that means it's a hidden folder. If you cannot see the hidden folders, then on Windows 11 you click on these three dots and then you choose “options” and then navigate to “view” and choose whether to show or not show these. I believe they're hidden by default so I had to change the setting originally, but it just stays at that point. On other types of devices I can't tell you exactly how to do it, but I'm sure you can figure it out. So you go to ProgramData and then within this file we're going to be looking for “MA Lighting Technology” singular; the one with the plural is related to GrandMA2. Open this one and find the version of MA3 that you're on, I'm on 2.2.5, and then you'll open “shared,” “resource,” this does not make sense but it's “lib_menus” and then “UI.”


Now we have a whole bunch of different options for different things that we could go ahead and look; at right now I'm going to navigate to “overlays” and open the dmx tester files, which is right here, so there is for each file, or most of these, there are three. There’s a Lua source file, there's a UIXML file, and there is an XML file, and you can open all of these in Visual Studio Code and have a look at them. 

I'm going to start with the Lua source file just to kind of show you what that looks like. This is what I discussed earlier where we have plugin name, component name, signal table, my handle, at the top of the plugin, and then they have all these different signal table stuff in here. Obviously there is some code that is happening in MA3 that is not in the Lua file; in most cases this would be either written somewhere else, possibly even not in Lua but in, I believe C++ is what it's written in, there's some type of interaction between C++ and Lua that I do not fully understand how that works, but there are important things that happen in both languages. I'm going to go back to here and open the UIXML file and it is asking me to select an app, I'm going to go with Visual Studio Code, I can do that always, I don't know what I would want to open it anything else for, and then it's showing me all of this information and this is not Lua and honestly, it's not very complicated, but I don't really understand it very well. So basically you're going to be looking for certain information here. Essentially what you want is to find each item and see what types of things are in it, what types of settings. So you'll find a “PatchToOverlay.” This is like their window that they're using as a base input. And yes, this is another option.

And it has this data and you can scroll and see, these are the different options that you can use. And then there's size policy and the size for each row. And that's how we know how to set those things. We have anchors and the name for the title bar right here. Then there's rows, columns, there's the title button and different things like that. So you can find a whole bunch of clues to how you should write things in the UIXML files.

I don’t really use the XML files, but I will show you just what that looks like. Not a lot of information here, but now you’ve seen it.


So I'm going to close these and… At this point, I would like to try adding some more information to this custom UI element. I'm going to actually create a new file and paste something I already copied in here. I made a copy of this one, really. I just added a whole bunch of information to it. And I made comments about these things so we can see that, and I can show you exactly what I have made changes to, and we can talk about it. I'm going to go up to the top, and we'll just scroll through. 

So the first change I made was I changed the number of rows in the dialog frame to 3. Then I obviously had to add code to say that the size policy and size for that row. Then I appended a checkbox grid, which is not the same thing as my button grid down here. I added a checkbox grid. This is on the second row of the dialog frame that it's appended onto and it has a margin of 5, so it has extra space between the checkbox grid and the dialog frame that it's placed on. Then we have on top of the checkbox grid, we've appended a checkbox called “checkBox1.” It's on row 1. There's only one row.

It has text, which says “Checkbox 1.” Text alignment is left. These semicolons actually don't have to be here. I don't know why they're here. The state is 0. That means unchecked.

And just like with the Apply button, we have added the information about the plugin component being myHandle. The clicked response, which is in this case, “CheckboxClicked.” Then down here, I had to change this to be in the third row of the dialog since checkboxGrid is in the second place. Then down here, I added a function to change the state of the checkbox if it's clicked. Here's where we use argument “caller” in the function and we say “Okay, so this, this item that called this function, that one, change its state.” 

And so, let's go ahead and run this and just see how that works. I'm going to have to create a new macro with this one… And let’s try it.

We have our beautiful dialog box with a checkbox. I can click it and it becomes clicked. I can unclick it and it toggles. I can hit Apply. Very fun!


There are, of course, many different types of buttons and things you can put in your UI elements. Personally, I've so far only used checkboxes and subtitles, just because those are the things I've had a use for so far. For that reason, I'm only going to use those in my examples just to keep things simple and on a topic I'm currently familiar with. But the general rules do always apply. Refer to the UIXML files to find examples of your elements you can use for your purposes. Don't be afraid to experiment with them until you get your needs met. Or ask people if you're really stumped.


An important thing I do want to bring up is we've created a UI element and now I can run it and it clearly lets me click on it and it responds as I expect it to.

But what if I actually want to store data based on whether or not this checkbox was clicked? 

For that matter, since it's possible to have multiple buttons where the apply button is, what if I want to have something happen based on which button I clicked? Right now, this plugin gives us a message, a checkbox, and a command button. And we can interact with it, but nothing happens based on our interactions.


We're used to the built-in functions like MessageBox(), which return values based on our choices and let us use those values for things. But what about in our custom UI elements?

Well, I'm going to go back to the code here and show you. You see how we have this “state” in our checkbox? This indicates whether the checkbox is checked or not, as I've already mentioned. But let's start here. A really important note to make is that if I place code to check the state, just at the end of the plugin down here, maybe go “if checkbox1.state == 0,” you know that kind of thing, then it's going to go through all of this code, put the UI element on the screen, and then run that code and check this state before the user has had a chance to interact with the object. So it’s going to show me the state that was there when I first ran the plugin, not after I changed it.


So how do I make sure it doesn't check the state until I close the pop-up? Well, the first thing that comes into my mind is to check it after the apply button is clicked. The problem with that is that after the apply button is clicked, the checkbox doesn't exist, so it doesn't find that information. Instead, here's my approach. Inside of the checkboxClicked function, I’m going to put an if statement that says, "If the state is zero, set checkboxState to clicked, otherwise set it to unclicked." Let's do that.


Now, this variable could be anything, obviously, and if I had multiple checkboxes, of course, I would give them different names. So now, I can click that checkbox as many times as I want, and when I finish, this variable will represent the final state of the checkbox. Now, I'm going to go down to my apply function and tell it to print based on the status of that variable, because the variable isn't going away.

Let's do a “Printf()” and of course, you could do whatever with this, but I'm going to just “Printf(checkboxState)” because that's pretty simple.


Now, there is just one problem remaining with the way I currently have this written, and I'm going to see if you can figure out what it is. I'm going to run this plugin and just see what happens. I click the checkbox, I'm going to click apply, and it says "clicked." Oh, I need to scroll this down in order to see it here. 

It says "clicked," and then I click it, unclick it, apply. It says "unclicked," click it, apply, it's clicked. I'm just actually not going to click it this time. I hit apply. Wait, it says "clicked." I didn't click that. What?

Well, what happened is, because this is set whenever I click the checkbox, since I didn't click it, it stayed in the state it was previously.

What I really need to do is reset this at the beginning of my plugin so that it will not be confused if I haven't clicked it. To do that, since obviously if I haven't clicked it, it's unclicked, I'm going to set it to unclicked right up here at the top.

There we go. Now it will work.


Now, suppose we have multiple command buttons. You'd want to have a different value returned based on which button was clicked, and this would be pretty straightforward. You just set a variable based on which button clicked called the function. I'll let you play with that on your own, but suffice it to say there are options.


Now you know how to build a custom UI element. There are a few issues with this, though. First, it's a lot of code. This is 117 lines of code for one checkbox. If I want to add another one, it's going to be like six more lines for each individual one to add more.

Also, whenever you add something, you also have to change the information about which row it's in, and in some cases adjust the size of the overlay and things like that. There's a lot of room for error. When I first built a custom UI element, I wanted to eliminate the part of scrolling through all that stuff, so I put in variables for each checkbox's name and things like that at the top of the plugin, like right here, like where I set this. I put some more information with the names of the checkboxes and the size and things like that.


I put each checkbox inside of an if statement that would make it only show if a certain variable was set. Then at the top of the plugin, I would modify those variables and it would make my checkboxes appear or not and have the correct information. But this was like 800 lines of code, even if I only needed to display a few checkboxes, just so that I would have the option of being able to display more if I wanted to. It was really crazy and clunky, and there was still a limit built in. And while it technically worked, it was not smart. But guess what? I found a way better way to handle custom UI elements. I built a function that was much more streamlined anyway and then put it into its own plugin so I could just reference it from others and give it arguments to define how it appears. A lot like the built-in MessageBox function, in fact. This is great because it helped me understand how MessageBox itself is designed, but also I don't have to put all that code in every plugin I want a checkbox dialog in, and it returns values like the MessageBox that I can use in my main plugin. 

It's super customizable, I built it with the options I need, and I can easily change it if I decide I need something else from it or build another version for another purpose. It's so freeing. I was even able to utilize tables and some other more advanced concepts to make it only take about 300 lines of code. And I can literally put as many checkboxes and subtitles as I want in an unlimited variety of configurations, which is really cool because with MessageBox you can only put the message in one place, you know?

Considering the fact that the method I've shown you today would require an extra, what, six lines of code per added checkbox, it would be a similar thing for a subtitle, and a whole bunch of other changes necessary that are error-prone and just difficult to keep track of, the ability to use an unlimited number of subtitles and checkboxes, arranged different ways with only a couple lines in a table in the calling argument is pretty impressive. And of course, mine is built to just display those things, but you could make it with whatever options you like.


So in the next video I'm going to show you how I built that amazing function I just finished describing, and from there, you should be set to do whatever you want to with custom UI elements. I hope you're as excited as I am to talk about that. But until next week, happy programming.


Comments

Popular posts from this blog

Lua for GrandMA3 YouTube Crash Course

Intro to Lua for GrandMA3

How to Make a Reminder Plugin for GrandMA3 Using Lua