Old 05-09-2016, 05:11 PM   #1
Lokasenna
Human being with feelings
 
Lokasenna's Avatar
 
Join Date: Sep 2008
Location: The vast, frozen wasteland of western Canadia.
Posts: 4,081
Default Tutorial: Building a GUI in Lua.

It seems like the majority of scripting questions these days revolve around how to build a GUI; understandable, since ReaScript doesn't have anything built-in to do it with. No matter though, it's not too hard.

We'll have to start with some fairly basic things, but by the time we're done you'll be able to do fancy stuff like this:



Disclaimer:
For the purposes of this tutorial, I'm assuming that the reader is familiar with the basics of Lua. If you aren't, I highly recommend X-Raym's videos for an introduction. Anyone who's watched his videos will also be familiar with the easier-to-read version of the ReaScript API documentation he put together, found here. It may not be up to date depending on when you read this, but personally I keep it open in a window all the time while I'm scripting.

Note: Writing these tutorials and the GUI library took a hell of a lot of time. If you find this useful, please consider clicking the donation link in my signature and tossing a few cents my way.

Table of Contents

1. Creating and maintaining a window
2. Colors
3. Drawing (part 1)
4. Text
5. Getting user input (part 1)
6. Introducing the LS GUI library
7. Example class - Button
8. Tables, classes, and methods
9. Working with strings
10. LS GUI in detail
11. Example classes - TxtBox, Sldr, and Knb
12. Using images
13. Having your script interact with the GUI elements

Other tips and tricks:

Automatically resizing text to fit a given space
Removing an element from the table

General Lua resources:

The Lua 5.3 Reference Manual
Lua-Users.org's tutorials
This cheat sheet and video that Garrick found
...or just our old friend Google. Searching something like "lua return multiple values from a function" will often get you a ton of results. If you're lucky, people will even put comments in their code. If you aren't lucky, you'll be stuck trying to make sense of paragraph-length jumbles of letters and symbols that look like this: (%a+):%s*(%d+)%s+(%a+):%s*(%d+)

Seriously. That's a thing in Lua. Dealing with strings of text gets scary sometimes.

Last edited by Lokasenna; 12-23-2016 at 01:30 PM.
Lokasenna is offline   Reply With Quote
Old 05-09-2016, 05:16 PM   #2
Lokasenna
Human being with feelings
 
Lokasenna's Avatar
 
Join Date: Sep 2008
Location: The vast, frozen wasteland of western Canadia.
Posts: 4,081
Default

1. Creating and maintaining a window

gfx.init("name"[,width,height,dockstate, x, y])
The parameters should be pretty obvious - "name" is what will appear in the window's title bar, then your dimensions, docking state, and the coordinates of the top-left corner. Note that this convention applies to almost all of ReaScript's graphics functions: X and Y are always the top-left corner of whatever you're drawing, unless you're told otherwise.

- A docking state of 0 will give you a window. Other values are related to which docker you want the window in, but I don't have a clue how that stuff works. Someone cooler than me can answer that question.

- The [square brackets] let us know that those parameters are all optional. You can provide just the name, or the name and width, or all of them, etc.

Theoretically, this is a perfectly acceptable script:
Code:
local function Main()
	gfx.init("My Window", 640, 480, 0, 200, 200)
end

Main()
However, if you run it, you'll immediately see a new problem. The window is created with the name, size, and position we specified, but then it immediately disappears. Why? Because we didn't give it anything to do, so it showed up for the party and went "dude, there isn't even any punch" before turning around and going home.

In ReaScript, getting our window to stick around requires three things:

1. A way to keep the script running.
reaper.defer(function name or anonymous function definition)
This is Reaper's equivalent of "hey, remind me to get groceries later" - the next time Reaper is processing scripts, defer() tells it to run your function again. If a function uses defer() to call itself again then, obviously, it will just keep running endlessly.

Code:
reaper.defer(Main)
2. A way to keep the window open.
gfx.update()
Tells the window we opened to quit falling asleep, making sure it stays visible as long as our defer() loop is running. If you simply made the above function into a defer() loop, it would keep opening new windows rather than maintaining one.

Code:
gfx.update()
3. A way to determine when the script should close.
gfx.getchar([char])
This function simply gets the current keyboard state, which doesn't help us. However, the Reaper devs were nice enough to include a small treat for us - if the window has been closed, i.e. by clicking the big [X] button in Windows, gfx.getchar() will return -1. This gives us an easy way to break the defer() loop we started.

(Just for fun, this example will also let you close the window by pressing Escape.)

Code:
 local char = gfx.getchar()

--	(Escape key)	(Window closed)
if char ~= 27 and char ~= -1 then
	reaper.defer(Main)
end
So, with a bit of rearranging, we can now open a window and keep it there long enough to do something useful. Note that the gfx.init() call has to be outside of whatever function our defer() loop is running; we want to initialize the window once, and then include gfx.update() in our loop to keep it open.

Code:
local function Main()

	local char = gfx.getchar()
	if char ~= 27 and char ~= -1 then
		reaper.defer(Main)
	end
	
	gfx.update()

end

gfx.init("My Window", 640, 480, 0, 200, 200)
Main()

Last edited by Lokasenna; 05-10-2016 at 08:31 PM.
Lokasenna is offline   Reply With Quote
Old 05-09-2016, 05:20 PM   #3
Lokasenna
Human being with feelings
 
Lokasenna's Avatar
 
Join Date: Sep 2008
Location: The vast, frozen wasteland of western Canadia.
Posts: 4,081
Default

2. Colors

On to the, er, slightly less boring stuff. Drawing things! Yay! And of course, when you're drawing, the first question to ask yourself is "what color feels most like the particularly annoying shade of ennui I feel reading this forum post?"

gfx.set(r[,g,b,a,mode,dest])
Again, some of these parameters are optional. R, G, and B are the components of our color, expressed here as a decimal from 0 to 1. 1,1,1 is white, 1,0,0 is red, and so on. Tagging along with them is A, which stands for alpha. Alpha refers to the transparency of an image or, in this case, the drawing we'll be doing. 1 is opaque, 0 is transparent, 0.5 is see-through.

Mode refers to the blending mode, which any Photoshop user will be familiar. This determines how the pixels we draw combine with those already in our window. For the most part it's irrelevant to us, but some fancier effects would definitely make use of it. The final parameter, Dest, lets you draw to the things other than the main window. Again, for basic stuff it's not at all necessary, but it has a few really handy uses that we might get to if I can stay interested in writing these tutorials long enough.

Code:
gfx.set(1, 0.5, 0.5, 1)
That's a lovely shade of... uh... I guess "salmon"?. Or "sunburn", maybe? It's definitely A color, at any rate. Readers who don't know computer colors from a hole in the ground - and why you can't tell the difference there is a good question for you therapist - are kindly directed to this site I just found: http://www.colorpicker.com/

(Pause for a brief interlude...)

Color conversion:
- For anyone like me who prefers to work with their colors as good ol' 8-bit values like 255, 128, 128 just divide your value by 255 to get the 0-1 equivalent. Personally, I write all of my functions and classes to take 8-bit as their input and then immediately convert it just so I never have to think about it.

- For those used to working with hexadecimal values, as is typical on the Information Superhighway, here's a useful helper function I found on Google:
Code:
-- Convert a hex color to integers r,g,b
local function hex2rgb(num)
	
	if string.sub(num, 1, 2) == "0x" then
		num = string.sub(num, 3)
	end

	local red = string.sub(num, 1, 2)
	local blue = string.sub(num, 3, 4)
	local green = string.sub(num, 5, 6)
	
	red = tonumber(red, 16)
	blue = tonumber(blue, 16)
	green = tonumber(green, 16)
	
	return red, green, blue
	
end

-- And you would call it like so:
local hex_color = 0xFF8080
local r, g, b = hex2rgb(hex_color)

gfx.set(r, g, b, 1)
"But wait!", I hear you yelling at your monitor, "what about the background color! It's just a black void and that terrifies me just like the ancient Babylonians were afraid of zero!" No sweat.

gfx.clear

Two things of note here - firstly, this is a variable and not a function. Second, the color format is completely different for reasons known only to God and Justin Frankel, assuming you aren't of a religious persuasion that considers them one and the same.

(I'm positive there's a Dalai Llama joke to be made here, but it just isn't happening)

Rather than 8-bit RGB or 0-1, this packs all three values together into one integer. The number itself is a direct translation of its hex equivalent, minus the "0x" prefix. Confused? I know I was for a while.

As the above description states, this number is equal to R + (G * 256) + (B * 256 * 256). To whit, another helper function:
Code:
-- Take discrete RGB values and return the combined integer
-- (equal to hex colors of the form 0xRRGGBB)
local function rgb2num(red, green, blue)
	
	green = green * 256
	blue = blue * 256 * 256
	
	return red + green + blue

end

-- Again, used like so:
local r, g, b = 255, 128 128
local color_int = rgb2num(r, g, b)
It should spit out 16744576, the same sexy shade of too-much-time-in-the-sun we previously expressed as 0xFF8080, 1, 0.5, 0.5, and 255, 128, 128. Who knew there were this many different ways to specify a color? Also, gfx.clear will default to black if you don't specify anything.

Anyway, back to the whole reason we needed this conversion to start with:

Code:
local function Main()
    ...defer() loop and maintaining the window go here...
end


-- A nice, respectable shade of grey.
local r, g, b = 64, 64, 64
gfx.clear = rgb2num(r, g, b)

gfx.init("My Window", 640, 480, 0, 200, 200)
Main()
So there you go. Colors. *cue "yaaayyy..." sound from Monty Python & The Holy Grail*

Last edited by Lokasenna; 08-03-2016 at 04:11 AM.
Lokasenna is offline   Reply With Quote
Old 05-09-2016, 06:53 PM   #4
Garrick
Human being with feelings
 
Garrick's Avatar
 
Join Date: Jul 2009
Location: Wellington
Posts: 4,242
Default

Sir you are the man!!!

Just what I was looking for. Xrayms vids are the perfect starter but this is where it gets tasty.

btw here's little intro to lua that I found helpful just laying out the basic mechanics of the language itself.
https://youtu.be/iMacxZQMPXs?list=PL...UFp_ODd86H0ZIY
Garrick is offline   Reply With Quote
Old 05-09-2016, 08:32 PM   #5
Lokasenna
Human being with feelings
 
Lokasenna's Avatar
 
Join Date: Sep 2008
Location: The vast, frozen wasteland of western Canadia.
Posts: 4,081
Default

Updated with a riveting chapter on turning colors into numbers. Up next... drawing a line! Ooooooooohhhhhhh! Aaaaaahhhhhhhhh!!
Lokasenna is offline   Reply With Quote
Old 05-09-2016, 08:32 PM   #6
WyattRice
Human being with feelings
 
WyattRice's Avatar
 
Join Date: Sep 2009
Location: Virginia
Posts: 1,754
Default

Vert excited about seeing this.
Many thanks for doing it.
Continue on.
Wyatt
WyattRice is online now   Reply With Quote
Old 05-09-2016, 09:00 PM   #7
Garrick
Human being with feelings
 
Garrick's Avatar
 
Join Date: Jul 2009
Location: Wellington
Posts: 4,242
Default

Quote:
gfx.set(r[,g,b,a,mode,dest])
cool banana.

What other destinations can there be? Or do you just mean another window you created earlier?
Garrick is offline   Reply With Quote
Old 05-10-2016, 12:36 AM   #8
X-Raym
Human being with feelings
 
X-Raym's Avatar
 
Join Date: Apr 2013
Location: France
Posts: 3,920
Default

@Lokasenna
Oh so cool, at least a tutorial about GUI :P
I am full into GFX these days, so timing is quite good !

--
I have almost the same hextorgb function than you but I have added an extra optional parameter for alpha channel (between 0 to 1). It is a bit more flexible.

--
Very important thing to precise :
  • Shapes are drawn every time the script loop (with the defer function). If a draw function get outside the defer loop, then it's shape isn't drawn.
  • Shapes are not object. You can't change their z-depth, rotation and color once they have already be drawn. Consider the GFX canvas as paint 1.0.
As my only experience with interface came from web design, this was not that easy to handle for me (as it is the total opposite).



--
Thanks for the initiative,
Waiting for the next part !


--

ps : thanks for the spotlight on my tutorial !
X-Raym is offline   Reply With Quote
Old 05-10-2016, 01:20 AM   #9
Sexan
Human being with feelings
 
Sexan's Avatar
 
Join Date: Jun 2009
Location: Croatia
Posts: 1,708
Default

Thank you!
Sexan is offline   Reply With Quote
Old 05-10-2016, 04:13 AM   #10
Lokasenna
Human being with feelings
 
Lokasenna's Avatar
 
Join Date: Sep 2008
Location: The vast, frozen wasteland of western Canadia.
Posts: 4,081
Default

Garrick:

Reaper has a bunch of off-screen spaces called "buffers" that you can draw into. They're really useful if, for instance, you have four or five separate shapes being drawn to make a knob or button or something. Rather than drawing each shape every time the script loops around, you can draw them into a buffer once and then have Reaper copy/paste that buffer onto your GUI window.

I only recently started using them, now that I'm fiddling with using .png files for graphics. Images have to be loaded from files into a buffer, which stores all of their animation frames. Par example:



-

X-Raym:

Cheers. I actually think it makes sense to think of ReaScript shapes as objects, since they're continually being redrawn. But yes, that's something I'll talk about (if I remember).
Lokasenna is offline   Reply With Quote
Old 05-10-2016, 12:38 PM   #11
Garrick
Human being with feelings
 
Garrick's Avatar
 
Join Date: Jul 2009
Location: Wellington
Posts: 4,242
Default

Cheers, yes that makes sense.

And yes I guess one of your buttons is to make sure we're paying attention
Garrick is offline   Reply With Quote
Old 05-10-2016, 02:19 PM   #12
Sju
Human being with feelings
 
Join Date: Jun 2015
Posts: 423
Default

I've been dreading the defer function, so thanks for clarifying that!
Sju is offline   Reply With Quote
Old 05-10-2016, 08:23 PM   #13
Lokasenna
Human being with feelings
 
Lokasenna's Avatar
 
Join Date: Sep 2008
Location: The vast, frozen wasteland of western Canadia.
Posts: 4,081
Default

3. Drawing (part 1)

ReaScript's graphics functions are fairly basic, which is both good and bad. Good because it's not very complicated to learn, bad because you have to do everything yourself. Let's have a look at some basic drawing tools:

gfx.line(x,y,x2,y2[,aa])
Draws a straight line from the first pair of coordinates to the second pair. The last parameter, aa, will apply antialiasing, and defaults to 1 if you don't specify it. Antialiasing keeps a line of pixels from looking like just a shitty line of pixels - by adding a faded pixels in the corners, the appearance is smoothed out. Odds are you want to leave it on, unless you have a very specific task in mind.

gfx.circle(x,y,r[,fill,antialias])
Draws a circle centered on the coordinates provided, with a radius of r pixels. Fill can be 1 or 0, and determines if the circle is colored in or not. Antialias, as above, can be safely ignored most of the time.

gfx.rect(x,y,w,h[,filled])
Draws a rectangle with its top-left corner at x, y as we discussed in a previous post. w and h provide the horizontal and vertical dimensions, and fill is once again whether or not the rectangle should be colored in.

gfx.roundrect(x,y,w,h,radius[,antialias])
A combination of the previous two, this will give you a rectangle with rounded-off corners. x, y, w and h should be obvious. radius in this case is the size of a circle that the rounded corners would form if you put them together. Alternatively, radius is telling you how many pixels away from a perfect corner your rounded ends will meet up with the rectangle's sides.

Note:
For simplicity's sake, from this point on I'm not going to include all of the initialization and function calls in my code examples; We're assuming that you can get a window open with the defer() loop running, and that the graphics code is being placed inside the main function I described previously. I'll specifically point out if and when we make changes to the existing code.

So, if we put the following into our script:
Code:
gfx.line(10, 20, 80, 120)

gfx.circle(40, 100, 20, 1)

gfx.rect(200, 200, 200, 200, 0)

gfx.roundrect(250, 420, 200, 50, 10)
...we should get:



Modern art at its finest.

You may notice that the gfx.roundrect() function doesn't have an option for being colored in, which sucks because coloring things in is why we're here. The good news is that can do it ourselves with a bit of effort and some ...dun dun dunnnn.... MATH. Yup, math. You knew it was coming sooner or later.

(This method is adapted from a post by mwe in this thread here.)

We already know that roundrect() is making a rectangle with circles instead of corners. We also know how to draw circles and rectangles ourselves, and that those functions CAN be colored in. By combining four circles and three rectangles we can easily emulate a filled-in roundrect()

Code:
-- Improved roundrect() function with fill, adapted from mwe's EEL example.
local function roundrect(x, y, w, h, r, antialias, fill)
	
	local aa = antialias or 1
	fill = fill or 0
	
	if fill == 0 or false then
		gfx.roundrect(x, y, w, h, r, aa)
	elseif h >= 2 * r then
		
		-- Corners
		gfx.circle(x + r, y + r, r, 1, aa)		-- top-left
		gfx.circle(x + w - r, y + r, r, 1, aa)		-- top-right
		gfx.circle(x + w - r, y + h - r, r , 1, aa)	-- bottom-right
		gfx.circle(x + r, y + h - r, r, 1, aa)		-- bottom-left
		
		-- Ends
		gfx.rect(x, y + r, r, h - r * 2)
		gfx.rect(x + w - r, y + r, r + 1, h - r * 2)
			
		-- Body + sides
		gfx.rect(x + r, y, w - r * 2, h + 1)
		
	else
	
		r = h / 2 - 1
	
		-- Ends
		gfx.circle(x + r, y + r, r, 1, aa)
		gfx.circle(x + w - r, y + r, r, 1, aa)
		
		-- Body
		gfx.rect(x + r, y, w - r * 2, h)
		
	end	
	
end
A quick rundown of what's going on:

1. See if antialias or fill were provided in the function call. If they weren't, default to 1 and 0 respectively, just like the original roundrect() function.
2. If fill was specified to be 0, then gfx.roundrect() is all we need, so we can just pass our input values along to that.
3. Check to make sure our dimensions are large enough that four circles are actually required. If so, we'll use that method.
4. Otherwise, h must be smaller than a single circle of radius r, so a simpler method will suffice.

Since we were careful to set up our function's inputs just like the original, calling it is as easy as not writing gfx. at the beginning of the line:

roundrect(x, y, w, h, r, antialias, fill)

Last edited by Lokasenna; 05-11-2016 at 04:36 AM.
Lokasenna is offline   Reply With Quote
Old 05-11-2016, 01:34 AM   #14
X-Raym
Human being with feelings
 
X-Raym's Avatar
 
Join Date: Apr 2013
Location: France
Posts: 3,920
Default

Can't wait to see the buffer thing you were talking about :P and any other tutorial of this serie hehe !

Indeed, everything has to be done manually, we can be really complex

Maybe you could share youe code snippets on ReaTeam GitHub ?
ReaScripts-Templates/GUI at master · ReaTeam/ReaScripts-Templates
We didn't put a lot of things in the GUI folder for the moment.

I can send you an invitation if you are interested.
X-Raym is offline   Reply With Quote
Old 05-11-2016, 01:12 PM   #15
Garrick
Human being with feelings
 
Garrick's Avatar
 
Join Date: Jul 2009
Location: Wellington
Posts: 4,242
Default

This awesome. I had some questions but I see you rewrote some posts to be more clear.

One thing I'm wondering, if gfx clear sets the background colour, can it also take an alpha parameter? I'm thinking of transparent window anchored to the arrange area.
If that's outside the scope of your tut, that's cool, thought I'd ask anyway
Garrick is offline   Reply With Quote
Old 05-11-2016, 05:46 PM   #16
Lokasenna
Human being with feelings
 
Lokasenna's Avatar
 
Join Date: Sep 2008
Location: The vast, frozen wasteland of western Canadia.
Posts: 4,081
Default

I had the same thought but no, gfx.clear is just RGB.
Lokasenna is offline   Reply With Quote
Old 05-11-2016, 07:05 PM   #17
Garrick
Human being with feelings
 
Garrick's Avatar
 
Join Date: Jul 2009
Location: Wellington
Posts: 4,242
Default

ahh right
Garrick is offline   Reply With Quote
Old 05-11-2016, 08:37 PM   #18
Lokasenna
Human being with feelings
 
Lokasenna's Avatar
 
Join Date: Sep 2008
Location: The vast, frozen wasteland of western Canadia.
Posts: 4,081
Default

4. Text

So now we know how to draw stuff. But what about printing text? I mean, lines and circles are pretty and all but at some point we're going to need to communicate some sort of information to the user. Right?

Well, we already know how to choose what color of crayon to use, so the next decision is what font to use.

gfx.setfont(idx[,"fontface", sz, flags])

ReaScript is nice enough to give us a few preset slots we can assign fonts to, determined by giving idx a number from 1 to 16. 0 will give you a default font of some sort, I think. fontface is the font name in text, followed by size and flags like italics, underline or bold.

Fonts can be set like so:
Code:
gfx.setfont(1, "Arial", 28)
...and recalled like so:
Code:
gfx.setfont(1)
Alternatively, you can just set the font manually every time. Up to you.

Edit: cyrilfb was nice enough to figure out how the flags parameter works:

Quote:
It should be the ascii number of the letter you want to use as a flag.

So in ascii:
Bold = 'b' = 98
Italic = 'i' = 105
Underline = 'u' = 117

gfx.setfont(1, "Arial", 28, 98) sets bold text.
gfx.setfont(1, "Arial", 28, 105) sets italic text.
gfx.setfont(1, "Arial", 28, 117) sets underlined text.

I cannot figure out how to combine flags. It would be nice if someone would tell us.
Edit the 2nd: Thanks to the fine gents at Cockos, we have an answer. Looks like the easiest way to do this is with another helper function (which is not in any of the files provided for these tutorials).

Code:
-- Font flag helper function, courtesy of Justin and Schwa
function fontflags(str) 
	
	local v = 0
	
	for a = 1, str:len() do 
		v = v * 256 + string.byte(str, a) 
	end 
	
	return v 

end

-- Used like so:
gfx.setfont(1,"Arial",20, fontflags('iu'))
Now that we have a font, drawing the string is easy as pie:

gfx.drawstr("str")

You can obviously pass it a variable with your string in it and all that as well. Now, unlike the other drawing functions where x and y were provided as parameters, drawstr prints your block of text with its top-left corner at gfx.x, gfx.y, so you have to set them manually beforehand.

Code:
local my_str = "This is a string"

gfx.setfont(1, "Arial", 28)

gfx.x, gfx.y = 100, 100

gfx.drawstr(my_str)
But what if you're drawing a button and you want to center the text inside a rectangle? Since this is Reaper, the do-it-yourself DAW, we'll have to figure out the appropriate coordinates ourselves:

gfx.measurestr("str")

As the name suggests, this will return the dimensions of a given string using the currently-set font. Note that this function returns TWO values, so you have to provide two variables instead of one:

Code:
local str_w, str_h = gfx.measurestr(my_str)
A little more math and we're all set:

Code:
local my_str = "This is a string"

local x, y = 100, 100
local w, h = 200, 80
local r = 10

roundrect(x, y, w, h, r, 1, 0)

gfx.setfont(1, "Arial", 28)
local str_w, str_h = gfx.measurestr(my_str)

gfx.x = x + ((w - str_w) / 2)
gfx.y = y + ((h - str_h) / 2)

gfx.drawstr(my_str)


Great, it's a picture of a button. But how do we show if the button has been clicked? To me, the easiest solution is to simply reverse the background and foreground colors. For this example, the first line can be set to true or false to simulate a user pressing the button. We'll learn how to actually do that in the next lesson.

Code:
local state = true
local my_str = "This is a string"

gfx.set(1, 0.5, 0.5, 1)

local x, y = 100, 100
local w, h = 200, 80
local r = 10

gfx.setfont(1, "Arial", 28)

local str_w, str_h = gfx.measurestr(my_str)
local txt_x = x + ((w - str_w) / 2)
local txt_y = y + ((h - str_h) / 2)

-- Unclicked
if state == false then 

	roundrect(x, y, w, h, r, 1, 0)

	gfx.x = txt_x
	gfx.y = txt_y

	gfx.drawstr(my_str)
	
-- Clicked	
else	
	
	roundrect(x, y, w, h, r, 1, 1)
	
	gfx.x = txt_x
	gfx.y = txt_y
	
	-- Store the current foreground color
	local r, g, b = gfx.r, gfx.g, gfx.b
	
	-- Set our text to the background color instead
	-- Remember the different color conventions; this is the equivalent of RGB 64, 64, 64
	gfx.set(0.25, 0.25, 0.25, 1)
	
	gfx.drawstr(my_str)
	
	-- Set the color back to what it was, before we forget
	gfx.set(r, g, b)
end


And now for something completely different, since you've been so well-behaved today.

Plain text on a plain background starts to get a little boring after a while, no? An easy way to spruce things up is by adding a shadow effect to your big "THIS IS MY AWESOME GUI" label.

For simplicity's sake, we're going to cast our shadows toward the lower-right. The simplest, but least-attractive method is to print a few copies of our text in black, offset a few pixels over and down.

Code:
local my_str = "THIS IS MY AWESOME GUI"
local r, g, b = 1, 0.5, 0.5
local shadow_dist = 4

gfx.setfont(1, "Arial", 28)

-- There's a new variable here, for those of you paying attention
local str_w, __ = gfx.measurestr(my_str)
local x = ((gfx.w - str_w) / 2)
local y = 50 


-- Draw the shadow first, so our main text will be printed on top of it.
gfx.set(0, 0, 0, 1)
for i = 1, shadow_dist do
	gfx.x, gfx.y = x + i, y + i
	gfx.drawstr(my_str)
end


-- Now the main text
gfx.x, gfx.y = x, y
gfx.set(r, g, b, 1)
gfx.drawstr(my_str)


Hawt.

Last edited by Lokasenna; 12-12-2016 at 08:11 PM.
Lokasenna is offline   Reply With Quote
Old 05-11-2016, 09:12 PM   #19
Garrick
Human being with feelings
 
Garrick's Avatar
 
Join Date: Jul 2009
Location: Wellington
Posts: 4,242
Default

Quote:
Originally Posted by Lokasenna View Post


Hawt.
Whoa!
Garrick is offline   Reply With Quote
Old 05-11-2016, 11:15 PM   #20
Spacemen Tree
Human being with feelings
 
Spacemen Tree's Avatar
 
Join Date: Mar 2013
Posts: 463
Default

Congrats on this initiative Lokasenna! Looking forward to the whole series
__________________
"After silence, that which comes nearest to expressing the inexpressible is music", Aldous Huxley
Spacemen Tree is offline   Reply With Quote
Old 05-12-2016, 01:41 AM   #21
X-Raym
Human being with feelings
 
X-Raym's Avatar
 
Join Date: Apr 2013
Location: France
Posts: 3,920
Default

Coool !

How would you make a text that is resize to always fit the size of a button ?
like a DrawTextInThisArea(text, x, y w, h) function where x,y is the center of the rectangle area ?
X-Raym is offline   Reply With Quote
Old 05-12-2016, 04:17 AM   #22
Lokasenna
Human being with feelings
 
Lokasenna's Avatar
 
Join Date: Sep 2008
Location: The vast, frozen wasteland of western Canadia.
Posts: 4,081
Default

So you want to draw a rectangle at x,y,w,h and have the text automatically sized to fit? The best solution I can think of would be a loop to adjust the font size:

Code:
Deleted because it didn't work properly. See two posts down.

Last edited by Lokasenna; 05-12-2016 at 07:29 PM.
Lokasenna is offline   Reply With Quote
Old 05-12-2016, 05:55 AM   #23
X-Raym
Human being with feelings
 
X-Raym's Avatar
 
Join Date: Apr 2013
Location: France
Posts: 3,920
Default

it doesn't work for me =/

In my Region clock script I already face this issue, it was for the whole gfx.window, and I came out with this solution

Code:
function CenterAndResizeText(string)
  gfx.setfont(1, font_name, 100)

  str_w, str_h = gfx.measurestr(string)
  fontsizefit=(gfx.w/(str_w+50))*100 -- new font size needed to fit.
  fontsizefith=((gfx.h-gfx.y)/(str_h+50))*100 -- new font size needed to fit in vertical.

  font_size =  math.min(fontsizefit,fontsizefith)
  gfx.setfont(1, font_name, font_size)

  str_w, str_h = gfx.measurestr(string)
  gfx.x = gfx.w/2-str_w/2
  gfx.y = gfx.y
end
The process is a bit different as you can see there is no loops.

It works pretty fine but the text is not exactly centered vertically and horizontally in most case.
X-Raym is offline   Reply With Quote
Old 05-12-2016, 07:27 PM   #24
Lokasenna
Human being with feelings
 
Lokasenna's Avatar
 
Join Date: Sep 2008
Location: The vast, frozen wasteland of western Canadia.
Posts: 4,081
Default

Font size isn't measured in pixels; Arial @ 10 will be a different size than Calibri @ 10 will be a different size than Comic Sans @ 10, etc, etc. So if your code is working, I think it's by accident.

I had a look at my code above and it turns out I just forgot to re-set gfx.x and gfx.y for drawing the string. The following is working fine for me:

Code:
	local my_str = "this is kind of a long string"
	
	local font_sz = 30
	local pad = 8

	gfx.setfont(1, font, font_sz)

	gfx.rect(x,y,w,h,0)

	local str_w, str_h = gfx.measurestr(my_str)

	-- We don't want to run the text right to the edge of the button
	local max_length = w - (2 * pad)

	-- See if my_str at the current size will fit in the rectangle
	-- If it does, break the loop and carry on
	-- If it doesn't, reduce the font size by 1 and check again

	for i = font_sz, 0, -1 do
		
		if str_w <= max_length then break end

		gfx.setfont(1, font, i)
		str_w, str_h = gfx.measurestr(my_str)
	end
	
	gfx.x = x + (w - str_w) / 2
	gfx.y = y + (h - str_h) / 2
	gfx.drawstr(my_str)

Last edited by Lokasenna; 05-12-2016 at 07:40 PM.
Lokasenna is offline   Reply With Quote
Old 05-12-2016, 08:22 PM   #25
Garrick
Human being with feelings
 
Garrick's Avatar
 
Join Date: Jul 2009
Location: Wellington
Posts: 4,242
Default

Sir this is my first crack at Lua so easy on the detentions

It kinda got away on me a bit and I forgot to make it dependant on the size of the string, duh.
But you learn by doing so any tips would be great

Code:
local function Main()

    local char = gfx.getchar()
    if char ~= 27 and char ~= -1 then
        reaper.defer(Main)
    end
    
end

function ResizeText(myString, winWidth, winHeight,rad) -- rad is radius 
  
  -- rectangle size and position
  rectWidth = winWidth/2
  rectHeight = winHeight/2
  rectX = (winWidth/2) - (rectWidth/2)
  rectY = (winHeight/2) - (rectHeight/2)
  
  --Size of font
  xOrY = rectHeight < rectWidth and rectHeight or rectWidth
  fontSize = math.floor(xOrY/6)
  
  
-- initialising stuff
  gfx.init("My Window", winWidth, winHeight, 0, 200, 200)
  gfx.update()
  gfx.roundrect(rectX, rectY, rectWidth, rectHeight, rad)

-- something something something
  gfx.setfont(1, "Arial", fontSize)

-- Position of string
  str_w, str_h = gfx.measurestr(myString)
  x = rectX + (rectWidth - str_w)/2
  y = rectY + (rectHeight - str_h)/2
  gfx.x, gfx.y = x, y
  
  -- drawing string
  gfx.drawstr(myString)

end

ResizeText("Text to Resize",600,700,5)

Main()
Garrick is offline   Reply With Quote
Old 05-12-2016, 08:39 PM   #26
Lokasenna
Human being with feelings
 
Lokasenna's Avatar
 
Join Date: Sep 2008
Location: The vast, frozen wasteland of western Canadia.
Posts: 4,081
Default

Not quite, you've got the order all wrong. Have a look at what I did for that task:

Code:
local function Main()
    
	local char = gfx.getchar()
	if char ~= 27 and char ~= -1 then
		reaper.defer(Main)
	end	
	
	gfx.set(1, 0.5, 0.5, 1)
	
	local x, y = 80, 100
	local w, h = 100, 80
	
	local my_str = "this is kind of a long string"
	
	local font_sz = 30
	local pad = 8

	gfx.setfont(1, font, font_sz)

	gfx.rect(x,y,w,h,0)

	local str_w, str_h = gfx.measurestr(my_str)

	-- We don't want to run the text right to the edge of the button
	local max_length = w - (2 * pad)

	-- See if my_str at the current size will fit in the rectangle
	-- If it will, end the loop and carry on
	-- If it won't, reduce the font size by 1 and try again
	for i = font_sz, 0, -1 do
		
		if str_w <= max_length then break end

		gfx.setfont(1, font, i)
		str_w, str_h = gfx.measurestr(my_str)
	end
	
	gfx.x = x + (w - str_w) / 2
	gfx.y = y + (h - str_h) / 2
	gfx.drawstr(my_str)
	
	gfx.update()
	
end


-- A nice, respectable shade of grey.
local r, g, b = 64, 64, 64
gfx.clear = rgb2num(r, g, b) 

gfx.init("My Window", 400, 300, 0, 200, 200)
Main()
1. Notice that pretty much everything is being done inside the Main() function. You could definitely take a bunch of that and put it inside a separate function, for instance MaxFontSize(x, y, w, h, font, str), if you wanted to.

2. The gfx.init() call has to be outside the function you're deferring, or you'll just keep opening and closing a window over and over and erasing whatever you draw in it.
Lokasenna is offline   Reply With Quote
Old 05-12-2016, 08:43 PM   #27
Lokasenna
Human being with feelings
 
Lokasenna's Avatar
 
Join Date: Sep 2008
Location: The vast, frozen wasteland of western Canadia.
Posts: 4,081
Default

5. Getting user input (part 1)

So far we've learned how open a window, keep it there, pick crayons out of a box, draw funny squiggles with the crayons, and write our names in big fancy letters. Given that this ia a GUI tutorial, I have to assume some of you are wondering "man, when is this fucking guy gonna get to the point?"

To you I say "Get lost. You're not the boss of me." To everyone else, let's learn how to let the user do stuff.

gfx.mouse_cap

Yet another variable, although this one might be a bit weird if you haven't programmed much before. gfx.mouse_cap stores the current state of the mouse; position, all the buttons, modifiers like Ctrl, Shift, etc, all in one binary number called a bitfield. The values you can access are as follows:

Code:
1   Left mouse button
2   Right mouse button
4   Control key
8   Shift key
16  Alt key
32  Windows key
64  Middle mouse button
To check them, you have to AND-compare gfx.mouse_cap with the value you're looking for, like so:

Code:
-- Is the left button down?
if gfx.mouse_cap & 1 == 1 then
	...do stuff...
else
	...do other stuff?...
end

-- How about Shift + Right button?
if gfx.mouse_cap & 8 == 1 and gfx.mouse_cap & 2 == 1 then
	...do stuff...
else
	...do other stuff...
end
We also need to know WHERE the mouse is so we can tell if the user is clicking on our button or just flailing away at empty space like those metalcore kids fighting invisible ninjas in the mosh pit.

gfx.mouse_x, gfx.mouse_y

Such imaginative names. Two variables again, which return the coordinates of the mouse cursor within your graphics window. The top-left corner is 0, 0, the bottom-right corner will be 0 + gfx.w, 0 + gfx.h. I shouldn't have to explain what those two are.

Determining if the mouse is on top of our button is just a matter of "Is x between this and this? Is y between this and this? Cool." As with most of the other stuff we're dealing with in this thread, it's the sort of task that just screams for a helper function.

Code:
-- Are these coordinates inside the given area?
local function IsInside(x, y, w, h)
	
	local mouse_x, mouse_y = gfx.mouse_x, gfx.mouse_y
	
	local inside = 
		mouse_x >= x and mouse_x < (x + w) and 
		mouse_y >= y and mouse_y < (y + h)
		
	return inside
	
end
For those not familiar with programming, that weird-looking set of comparisons is a much simpler way to say "if ___ then if ____ then if ____ then if ____". Because we only need IsInside() to return true or false, setting a variable equal to the result of all those comparisons shortens things nicely.

Now that we're able to check the mouse position and the state of its buttons, we're ready to make our button clickable. What we want do is something like this:

Code:
Is the left button down?
If so, is the cursor inside our button?
If so, toggle the button's state
However, there's a wee problem with that; since this code is run every time the window updates, it will also toggle states if you simply haven't let go of the mouse button yet. Let's add a couple of things to that list:

Code:
Is the left button down?
Was it down the last time we checked?

If it wasn't, then the user has clicked, so:
	Is the cursor inside our button?
		If so, toggle the button's state
		
Store the mouse button's current state so we can check it next time
Translated into grown-up language, that works out to something like this:

Code:
	-- If the left button is down
	if gfx.mouse_cap & 1 == 1 then
	
		-- If the cursor is inside the rectangle AND the button wasn't down before
		if IsInside(x, y, w, h) and not mouse_btn_down then
			
			mouse_btn_down = true
			state = not state
			
		end

	-- If the left button is up
	else

		mouse_btn_down = false
	
	end
Throwing that in with the code from the previous lesson should give you this:



There are, however, a couple of easy mistakes to watch out for:

- state needs to hang around between Main() loops, so it has to be either a global variable or declared locally outside the Main() function.

- This code needs to be pasted somewhere after the x,y,w,h variables are declared so that it can access them.

We've got a few other things to cover before we get around to making this button actually do stuff, but I'll leave you something easy to practice with:

Code:
retval = reaper.MB("Are you a human being?", "Voight-Kampff Test", 4)
That function will pop up a Yes/No message box, with the user's answer stored in retval. If you wanted to, you could take that number and have it determine the text of another string in the window. I'm just saying, that might be a useful thing to know how to do...

Last edited by Lokasenna; 05-14-2016 at 07:58 PM.
Lokasenna is offline   Reply With Quote
Old 05-12-2016, 09:03 PM   #28
Garrick
Human being with feelings
 
Garrick's Avatar
 
Join Date: Jul 2009
Location: Wellington
Posts: 4,242
Default

Quote:
Originally Posted by Lokasenna View Post
Not quite, you've got the order all wrong. Have a look at what I did for that task:...................

............
1. Notice that pretty much everything is being done inside the Main() function. You could definitely take a bunch of that and put it inside a separate function, for instance MaxFontSize(x, y, w, h, font, str), if you wanted to.

2. The gfx.init() call has to be outside the function you're deferring, or you'll just keep opening and closing a window over and over and erasing whatever you draw in it.
ahhh sweet, I see what your're doing now.
thanks for the reply, I really appreciate it.
Garrick is offline   Reply With Quote
Old 05-13-2016, 02:52 AM   #29
X-Raym
Human being with feelings
 
X-Raym's Avatar
 
Join Date: Apr 2013
Location: France
Posts: 3,920
Default

Quote:
We also need to know WHERE the mouse is so we can tell if the user is clicking on our button or just flailing away at empty space like those metalcore kids fighting invisible ninjas in the mosh pit.
OMG

Thanks again for the tut !!!
X-Raym is offline   Reply With Quote
Old 05-13-2016, 08:22 AM   #30
Sexan
Human being with feelings
 
Sexan's Avatar
 
Join Date: Jun 2009
Location: Croatia
Posts: 1,708
Default

this is gold!Thank you very much!
Can I ask if you can at the end of tutorial post the whole code,I don't know what order it needs to be,I'm stuck at user input.cannot get the button to be on off

Last edited by Sexan; 05-13-2016 at 08:52 AM.
Sexan is offline   Reply With Quote
Old 05-13-2016, 03:01 PM   #31
Lokasenna
Human being with feelings
 
Lokasenna's Avatar
 
Join Date: Sep 2008
Location: The vast, frozen wasteland of western Canadia.
Posts: 4,081
Default

I often don't have a complete example to post, since I'm just throwing the lesson code into an existing script. Don't worry though - the next one will have a HUGE pile of code you can look at. In the meantime:

- Are you getting any error messages?

- Is your state variable declared in the right place? If you put "local state" inside the function you're deferring, it will reset on every loop. As I mentioned above it has to be either global or declared outside the deferred function.

- Are the mouse position and button state being detected correctly?

Here's a function from X-Raym's tutorials that I've found really, really handy; it's seriously the first thing I put in every script.

Code:
-- Print stuff to the Reaper console, for debugging purposes
function Msg(message)
	
	reaper.ShowConsoleMsg(tostring(message).."\n")
	
end
If your code isn't working and you can't figure out where the problem is, you can throw in a couple of calls to Msg() to tell the value of a variable, or simply let you know if a certain if/then/else structure is working correctly.

In your case, you could try putting it in your IsInside function to see if it's giving the right answer:
Code:
-- Are these coordinates inside the given area?
local function IsInside(x, y, w, h)
	
	local mouse_x, mouse_y = gfx.mouse_x, gfx.mouse_y
	
	local inside = 
		mouse_x >= x and mouse_x < (x + w) and 
		mouse_y >= y and mouse_y < (y + h)

        Msg(inside)
		
	return inside
	
end
or put a couple of calls inside the "if the mouse button is down" structure to tell you which branch is being run:

Code:
	-- If the left button is down
	if gfx.mouse_cap & 1 == 1 then

                Msg("Mouse down")
	
		-- If the cursor is inside the rectangle AND the button wasn't down before
		if IsInside(x, y, w, h) and not mouse_btn_down then
			
                        Msg("Clicked")

			mouse_btn_down = true
			state = not state
			
		end

	-- If the left button is up
	else

                Msg("Mouse up")

		mouse_btn_down = false
	
	end
Now, obviously these are going to be running every time the script loops, so the Reaper console will be a constant stream of messages, but if you're clicking away and you never see "Clicked" coming up then obviously the script isn't getting to that section.
Lokasenna is offline   Reply With Quote
Old 05-14-2016, 06:05 AM   #32
Sexan
Human being with feelings
 
Sexan's Avatar
 
Join Date: Jun 2009
Location: Croatia
Posts: 1,708
Default

There are no errors,just the button does not change after clicking it
Code:
local function IsInside(x, y, w, h)
  
  local mouse_x, mouse_y = gfx.mouse_x, gfx.mouse_y
  
  local inside = 
    mouse_x >= x and mouse_x < (x + w) and 
    mouse_y >= y and mouse_y < (y + h)
    
  return inside
  
end

local function roundrect(x, y, w, h, r, antialias, fill)
  
  local aa = antialias or 1
  fill = fill or 0
  
  if fill == 0 or false then
    gfx.roundrect(x, y, w, h, r, aa)
  elseif h >= 2 * r then
    
    -- Corners
    gfx.circle(x + r, y + r, r, 1, aa)    -- top-left
    gfx.circle(x + w - r, y + r, r, 1, aa)    -- top-right
    gfx.circle(x + w - r, y + h - r, r , 1, aa)  -- bottom-right
    gfx.circle(x + r, y + h - r, r, 1, aa)    -- bottom-left
    
    -- Ends
    gfx.rect(x, y + r, r, h - r * 2)
    gfx.rect(x + w - r, y + r, r + 1, h - r * 2)
      
    -- Body + sides
    gfx.rect(x + r, y, w - r * 2, h + 1)
    
  else
  
    r = h / 2 - 1
  
    -- Ends
    gfx.circle(x + r, y + r, r, 1, aa)
    gfx.circle(x + w - r, y + r, r, 1, aa)
    
    -- Body
    gfx.rect(x + r, y, w - r * 2, h)
    
  end  
  
end

local function rgb2num(red, green, blue)  
  green = green * 256
  blue = blue * 256 * 256  
  return red + green + blue
end
state = 1
local function Main()
    
  local char = gfx.getchar()
  if char ~= 27 and char ~= -1 then
    reaper.defer(Main)
  end 
  
  local my_str = "This is a string"
  
  gfx.set(1, 0.5, 0.5, 1)
  
  local x, y = 100, 100
  local w, h = 200, 80
  local r = 10
  
  gfx.setfont(1, "Arial", 28)
  
  local str_w, str_h = gfx.measurestr(my_str)
  local txt_x = x + ((w - str_w) / 2)
  local txt_y = y + ((h - str_h) / 2) 
 
  -- Unclicked
  if state == 0 then 
  
    roundrect(x, y, w, h, r, 1, 0)
  
    gfx.x = txt_x
    gfx.y = txt_y
  
    gfx.drawstr(my_str)
    
  -- Clicked  
  else  
    
    roundrect(x, y, w, h, r, 1, 1)
    
    gfx.x = txt_x
    gfx.y = txt_y
    
    -- Store the current foreground color
    local r, g, b = gfx.r, gfx.g, gfx.b
    
    -- Set our text to the background color instead
    -- Remember the different color conventions; this is the equivalent of RGB 64, 64, 64
    gfx.set(0.25, 0.25, 0.25, 1)
    
    gfx.drawstr(my_str)
    
    -- Set the color back to what it was, before we forget
    gfx.set(r, g, b)
  end
  
  if gfx.mouse_cap & 1 == 1 then
      
        -- If the cursor is inside the rectangle AND the button wasn't down before
        if IsInside(x, y, w, h) and not mouse_btn_down then
          
          mouse_btn_down = true
          state = not state
          
        end
    
      -- If the left button is up
      else
    
        mouse_btn_down = false
      
      end 
  
  gfx.update()
  
end


-- A nice, respectable shade of grey.
local r, g, b = 64, 64, 64
gfx.clear = rgb2num(r, g, b) 

gfx.init("My Window", 400, 300, 0, 200, 200)
Main()
edit:after changing this line
Code:
if state == 0 then
to
Code:
if state == false then
everything works,are we supposed to change this or it should work as it is?

Last edited by Sexan; 05-14-2016 at 06:16 AM.
Sexan is offline   Reply With Quote
Old 05-14-2016, 09:33 AM   #33
Lokasenna
Human being with feelings
 
Lokasenna's Avatar
 
Join Date: Sep 2008
Location: The vast, frozen wasteland of western Canadia.
Posts: 4,081
Default

Oh, my bad. Change the declaration to "state == true" and the if statement to "if state == false", should work fine after that. I noticed that while I was testing the code for the clickable-button example but I forgot to update the button-drawing example while I was at it. It's fixed now.

Last edited by Lokasenna; 05-14-2016 at 10:31 AM.
Lokasenna is offline   Reply With Quote
Old 05-14-2016, 10:45 AM   #34
Lokasenna
Human being with feelings
 
Lokasenna's Avatar
 
Join Date: Sep 2008
Location: The vast, frozen wasteland of western Canadia.
Posts: 4,081
Default

6. Introducing the LS GUI library



Ta daaaaaaaa!!!!

The remainder of our lessons will make use of this handy-dandy GUI library I put together, rather than me having to write out fresh code for every example. Features include:

- A bare minimum of coding on the end-user's part. The GUI in that image only needed about 25 lines of code to create; the rest is all tucked away in a separate file for safekeeping.

- An excessive number of comments in the script, along with more spacing and indenting than a Tab button factory.

- Color and font presets for a consistent visual appearance.

Just grab these two files, put them in the same folder, and run the usage demo from Reaper. Don't worry about the library demo, it gets loaded automatically.

GUI Tutorial - LS GUI usage demo.lua
GUI Tutorial - LS GUI library demo.lua

Please note that this is intended only for use with this tutorial - it's a little sloppy, probably has a few bugs, and could definitely be more efficient about drawing things. I wouldn't recommend using it in scripts intended for public use.

Last edited by Lokasenna; 03-31-2017 at 05:50 AM.
Lokasenna is offline   Reply With Quote
Old 05-14-2016, 12:59 PM   #35
Sexan
Human being with feelings
 
Sexan's Avatar
 
Join Date: Jun 2009
Location: Croatia
Posts: 1,708
Default

awesome!
still looking forward to next tutorial how to make button do something (from scratch)
Sexan is offline   Reply With Quote
Old 05-14-2016, 01:28 PM   #36
Lokasenna
Human being with feelings
 
Lokasenna's Avatar
 
Join Date: Sep 2008
Location: The vast, frozen wasteland of western Canadia.
Posts: 4,081
Default

Well, once the button is able to clicked, you really just have to put whatever function you want inside the "clicked" part of the if/then statement.

If you look at the library demo in my post, and scroll down to Btn:onmouseup() you'll see that, in this case, it's just taking whatever function the user specified and running it. So if you knew the command ID from Reaper's action list you could use reaper.Main_OnCommand() to run it, etc.

Last edited by Lokasenna; 05-14-2016 at 09:30 PM.
Lokasenna is offline   Reply With Quote
Old 05-14-2016, 02:18 PM   #37
Garrick
Human being with feelings
 
Garrick's Avatar
 
Join Date: Jul 2009
Location: Wellington
Posts: 4,242
Default

Lokasenna that looks seriously cool.
Garrick is offline   Reply With Quote
Old 05-14-2016, 03:50 PM   #38
WyattRice
Human being with feelings
 
WyattRice's Avatar
 
Join Date: Sep 2009
Location: Virginia
Posts: 1,754
Default

Very Nice.

Hope you don't mind a very noob question.
I have the button click events working ok.

How can I get a typed string out of the TxtBox, and on a button click event, output the text to something e.g. Messagebox, or something like:

local myStr = TxtBox.text

reaper.AddProjectMarker(0, false, CursorPosition, 0, myStr, -1)

Thanks, Wyatt
WyattRice is online now   Reply With Quote
Old 05-14-2016, 05:15 PM   #39
X-Raym
Human being with feelings
 
X-Raym's Avatar
 
Join Date: Apr 2013
Location: France
Posts: 3,920
Default

@Lokasenna
LOOL that script is awesome
So hilarious ^^

And actually nice GUI interactive element and code optimization too !
X-Raym is offline   Reply With Quote
Old 05-14-2016, 09:28 PM   #40
Lokasenna
Human being with feelings
 
Lokasenna's Avatar
 
Join Date: Sep 2008
Location: The vast, frozen wasteland of western Canadia.
Posts: 4,081
Default

Fine, since you guys seem really keen on making buttons do stuff, I can shuffle my lesson plan around a little.

7. Example class - Button

Note: This lesson refers to the LS GUI library provided in part 6. If you haven't downloaded those files yet, see this post: 6. Introducing the LS GUI library

Alright, now that we've had some time to fiddle with the elements and drool over the amazing three-color, Windows 3.1-quality graphics, let's dive right in and have a look at what makes these buttons tick. The next couple of lessons will be more in-depth on the topic of tables, classes, and methods, so I'll be skimming over that stuff here.

Unlike the other elements, which all exist to display information and let you adjust it (turning a knob, checking a new box), buttons are there to perform an action. Maybe it's loading a file, maybe it's pressing Play because you're too lazy to move the mouse over to the transport bar, maybe it's doing something complex that the other elements were setting up values for. Because of this, the internal code for a button is comparatively simple; it doesn't need to give a shit about where you clicked, or how far you're dragging. Just wait for a click and do something.

Our Button's parameters are as follows:
Code:
x, y, w, h		Coordinates of top-left corner, width, height
caption			Label / question
func			Function to perform when clicked
...				If provided, any parameters to pass to that function.
The last two are really neat, in my opinion:

func: Lua will let you take a function and its parameters and pass them to ANOTHER function. This means that, when creating the button, you can put basically any legal function in there and the Button will run it when clicked.

(I'm sure there must be at least one function out there that will break it)

...: Another cool thing about Lua that not all languages have is the ability to pass an unspecified number of parameters to a function. You can see a good example of this in gfx.triangle(), which lets you specify as many pairs of coordinates as you want and it will draw a polygon between them.

When the button is first created, the arguments provided in ... are stored in a table for the time being.
Code:
btn.params = {...}
If the button is clicked (specifically, when the button is released), the contents of that table are dumped out for func to use.
Code:
self.func(table.unpack(self.params))
That's basically it.

table.unpack() is great for this because it simply takes each entry in the table and spits them out as separate values. Consider the hex2rgb() function we made in the Colors lesson:
Code:
local r, g, b = hex2rgb(hex_color)
The unpacked values are returned in the same way, only here we're returning them inside func's parentheses, so the values all just grab a seat and buckle in.

Referring back to our hypothetical list of things you might need a button to do:

1) Loading a file
The function reaper.Main_OnCommand() lets us call any action in Reaper's Action List by providing a unique ID number for it. In this case, we'll prompt the user to open a new project. Why? I don't have a clue. The command ID for "File: Open project" is 40025.
Code:
reaper.Main_OnCommand(40025, 0) -- Don't worry about the 0, just make sure it's there.
To have our button do that, we just include the function and its parameters (the ID number and 0) in the button's initial settings:
Code:
my_btn = GUI.Btn:new(100, 100, 200, 75, "Open a project", reaper.Main_OnCommand, 40025, 0)
2) Pressing Play
reaper.OnPlayButton() is all we need here.
Code:
my_btn = GUI.Btn:new(100, 100, 200, 75, "Play", reaper.OnPlayButton)
3) Doing something more complex
Odds are the thing you want to do will require more than one step. In this case, you'll have to write your own function and then tell the button to give it a holler.

We'll use WyattRice's question above as an example:
Quote:
How can I get a typed string out of the TxtBox, and on a button click event, do something like:

local myStr = TxtBox.text

reaper.AddProjectMarker(0, false, CursorPosition, 0, myStr, -1)
There's not much more to it than that. Functions and elements can refer to each other's parameters, and most of the time you'll want retval. Every element stores its current state/value/text there. Notice how we access element parameters:

Code:
local function Add_Marker()

	local myStr = GUI.elms.TxtBox.retval
	local CursorPosition = reaper.GetCursorPosition()
	
	reaper.AddProjectMarker(0, false, CursorPosition, 0, myStr, -1)
	
end


GUI.elms = {

	TxtBox = GUI.TxtBox:new(100, 100, 200, 50, "Marker name:", 4),
	my_btn = GUI.Btn:new(100, 200, 200, 75, "Add marker", Add_Marker)
	
}
GUI.elms is the table all of our elements are stored in, so element parameters are found via GUI.elms.element_name.parameter_name

And now it's time for bed.
Lokasenna is offline   Reply With Quote
Reply

Thread Tools
Display Modes

Posting Rules
You may not post new threads
You may not post replies
You may not post attachments
You may not edit your posts

BB code is On
Smilies are On
[IMG] code is On
HTML code is Off

Forum Jump


All times are GMT -7. The time now is 05:29 AM.


Powered by vBulletin® Version 3.8.9
Copyright ©2000 - 2017, vBulletin Solutions, Inc.