Cockos Incorporated Forums Tutorial: Building a GUI in Lua.
 Register Track Bugs/Feature Requests Search Today's Posts Mark Forums Read

 05-15-2016, 02:51 PM #41 WyattRice Human being with feelings     Join Date: Sep 2009 Location: Virginia Posts: 1,913 Lokasenna, Many thanks! This is a great tutorial. I have so many questions. Anyway, thanks for the demo of the textbox. Later, Wyatt __________________ Advanced Snap/Grid Settings. | Custom Buttons With Actions. | DDP To Cue Writer.
 05-15-2016, 05:39 PM #42 WyattRice Human being with feelings     Join Date: Sep 2009 Location: Virginia Posts: 1,913 Just one more question for now. How can I get a return on the "Enter Key" when pressed From what I read it's char 13. e.g. local function Add_Marker() local myStr = GUI.elms.TxtBox.retval if char == 13 then ? local CursorPosition = reaper.GetCursorPosition() reaper.AddProjectMarker(0, false, CursorPosition, 0, myStr, -1) end __________________ Advanced Snap/Grid Settings. | Custom Buttons With Actions. | DDP To Cue Writer.
 05-15-2016, 07:08 PM #43 Lokasenna Human being with feelings     Join Date: Sep 2008 Location: Calgary, AB, Canada Posts: 6,356 At the moment you can't; if you have a look at TxtBox:ontype() you'll see that it doesn't list character 13 anywhere. You could easily add an "elseif char == 13 then" function yourself, and it's on my list eventually. However, in the interest of not altering the library code itself, I think this could be a good use for one of LS GUI's other features. - If you read the comments in the usage demo, you'll see that it's possible to have the Main() loop run a user function every time it comes around. - In the list of TxtBox's parameters, notice that there's a property called focus, which is set to true when the text box is active. You can check it the same way we got text previously. - In the library's Main() function, the keyboard state for each loop is saved in GUI.char I haven't tried, but I'm pretty sure you could write a function that would check if the textbox is in box AND the enter key has been pressed and then perform your action, tell Main() to run that function, and Bob's your uncle. Note, however, that checking the state of the Enter key would have to be done just like we checked the mouse button in the Button lesson, otherwise it would also be picking up the Enter key if it's still down and hasn't been let up yet.
 05-16-2016, 11:03 AM #44 eugen2777 Human being with feelings     Join Date: Aug 2012 Posts: 271 Lokasenna, It looks very good, thank you. __________________ ReaScripts
 05-16-2016, 02:32 PM #45 Lokasenna Human being with feelings     Join Date: Sep 2008 Location: Calgary, AB, Canada Posts: 6,356 8. Tables, classes, and methods 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 Let's take a closer look at how everything in LS GUI is set up. I'm not going to have much code in this lesson, since it's all in the library file, so you may want to have both this post and the library open at the same time. For those with at least Windows 7 (I think), you can drag a window all the way to left or right edge of the screen and it will be resized to take up only that side - it's really handy. Tables (For those familiar with other programming languages, tables are Lua's version of arrays. Sort of. They have slightly different rules, and are often much more flexible.) Tables, as the name suggests, are an easy way to store multiple things in one variable, perform operations on all of them, sort them, etc, while keeping things really tidy. Here's a good article covering how tables are created, how to fill them, and how to work with them: lua-users.org Table Tutorial As you'll see, everything in LS GUI relies on tables. Seriously, the entire file is one big table - the first line of code is this: Code: local GUI = {} and the last line is this: Code: return GUI Everything we do in between - the color presets, the miscellaneous functions, the Main loop, and call of the GUI elements - is stored in that table: Code: local function do_stuff() end GUI.do_stuff = do_stuff This creates a perfect copy of do_stuff within the table. By placing the entire library in a table and returning it to the parent script we accomplish two important things: 1. The parent script can use LS GUI's functions and access its variables. 2. Nothing in LS GUI will affect the parent script or be affected by it, so there are no questions of variable scope or having to write the parent script in a specific way to have everything work properly. If we think of the parent script as a lady sitting at a desk, LS GUI is a coworker under her supervision. She says "draw me a button" and LS GUI just makes it happen, no fuss. For comparison, if we remove the table and just place everything in the library at "top level"... Code: GUI.fonts --> fonts GUI.mouse --> mouse GUI.Main --> Main GUI.Lbl --> Lbl ...those are common enough names for variables and functions that we'd probably interfere the parent script, so the user would to be very careful not to duplicate any of them. Even worse, anything the library does to mouse would happen to the parent script's mouse as well, so you're not only making the user do things your way but you're potentially screwing with their data at the same time. In this case, LS GUI is more like one of those coworkers that's always using your stuff, never puts it back in the right place, spills coffee on an important report and then says "wow, you really screwed up this time" before walking away happily back to his own desk. Nobody likes that guy. Classes Declaring variables all the time gets really, really old, especially when all you want is five or six of basically the same thing. The same goes for writing functions in many languages, where working with normal variables might force you to write five of the same function to work with each of those five things. If only there was a way to simplify things... oh wait, there is. It's known as "object-oriented programming" - rather than working simply with variables, we create objects that contain multiple variables. For instance, a person would contain variables like their name, height, weight, favorite shitty reality TV show, whether or not they've paid for their Reaper license, etc. By creating what's known as a "class", we can build a prototype of one thing and then produce copies of it on-demand, each with their own name, rank, and serial number. As a result, rather than five functions doing the same thing, you just write it once and provide it the name of the object you'd like it to work on. All of this fancypants stuff happens in the various :new() functions. Here's a simple one, for the Lbl element: Code: local Lbl = {} Just like everything else in LS GUI, we'll start by making ourselves a table. Code: function Lbl:new(x, y, caption, shadow) We'll call this function every time we want to create a new Lbl, giving it the values provided just like a rich parent giving their kid a Ferrari when they turn 16. Calling it Lbl:new rather than, say, new_label tells Lua to store the function inside Lbl{}. The function's name uses that : to do another neat Lua trick. As I said above, functions written for use with objects from a class are always going to want the object's name. You could include the name as a parameter in all your functions if you wanted to: Code: function Lbl.new(lbl_name, x, y, caption, shadow) ...but the : serves as a shortcut to do the same thing. Lbl:new creates a function in Lbl{} that will automatically check the object's ID and store it in a variable called self. If you scroll down to some of the other functions in LS GUI, you'll see that self shows up all over the place. It's just a timesaver, really, like tapping your credit card on the machine rather than swiping it, typing in your PIN, waiting, etc. The function then creates its own temporary table, label{} and stores the given parameters in it, along with a couple of other values that we'll need down the road. It's worth noting what one line is doing: Code: label.shadow = shadow or 0 This is an easy way to see if shadow was specified, and setting it to 0 if it wasn't. I'm not sure exactly how it works, but it does, so you'll see it here and there in the library. It's a nice time-saver. The magic that turns our wooden doll of a function into a real live boy is at the end: Code:  setmetatable(label, self) self.__index = self return label Again, my understanding of this bit is a little shaky, but here's a rough idea of what it's doing: 1. setmetatable tells label{}, to use self as its blueprint. Remember that self is standing in for Lbl{}. 2. Normally, if you look a value up in a table and there's nothing there Lua will return nil. __index tells the table to, instead, try looking in its metatable to see if there's an appropriate value there. 3. By having the function return label{}, which includes the metatable and __index stuff, we can create a variable, direct it to the :new function, and that variable will become a "child" of the "parent" object, inheriting all of its properties and functions: Code: my_new_label = Lbl:new(50, 50, "I'm a label", 0) Because my_new_label is only a copy we can do things to it without affecting the parent, but if it needs help it can still phone up Mommy to ask "hey, so how do I do my own laundry?" Methods Moving on in the library file, you can see Lbl has the following functions: Code: :new() :draw() :onmousedown() :onmouseup() :ondoubleclick() :ondrag() :ontype() Functions like these that are meant to work with a class of objects are known as "methods" of that class, and as their names suggest they're a list of things our sweet lil' Lbl has been taught how to do. Again, they're all stored in Lbl{}, so all of our child objects know how to use them, and because they all have that : in their names they'll have a self parameter that can be used to get information about the object that called them. i.e. Code: my_new_label:print_your_name() would theoretically spit out Code: "Hi guys, I'm my_new_label. Does anyone here like Pokemon? I LOVE Pokemon. Guys? Hello?" Lastly, we copy Lbl{} into GUI{}: Code: GUI.Lbl = Lbl After all of that, my_new_label now exists with the values we specified in its blueprint, and has been sent off into the world with a little instruction booklet covering what to do if someone wants to draw it, or click on it, or type at it. *sigh*....they grow up so fast. Last edited by Lokasenna; 05-16-2016 at 02:40 PM.
 05-16-2016, 03:58 PM #46 heda Human being with feelings     Join Date: Jun 2012 Location: Spain Posts: 5,481 Lokasenna, where where you when I was learning one year ago? I suffered a lot! haha Very good! Thank you __________________ HeDaScripts for REAPER - HeDaScripts discord chat support
 05-16-2016, 06:53 PM #47 run_esc Human being with feelings   Join Date: Mar 2016 Location: Victoria, BC Posts: 197 This is bloody awesome for a relative newb like me. Thanks! (from a fellow Western Canadian!)
 05-16-2016, 11:40 PM #48 X-Raym Human being with feelings     Join Date: Apr 2013 Location: France Posts: 6,061 Objects... It's getting serious now :P ------ Code: label.shadow = shadow or 0 I didn't know that ! I would have made this : Code:  if shadow then lbl.shadow = shadow else lbl.shadow = 0 end and before that, I was also making comparaison with nil, which is complelty unecessary in most cases in lua ^^. Nice ! __________________ My ReaScripts - Premium Scripts - X-Raym Theme - Stash Files - Feature Requests - Learn ReaScript - Website - Donation - ReaComics - Scripts Custom Dev
 05-17-2016, 04:11 AM #49 Lokasenna Human being with feelings     Join Date: Sep 2008 Location: Calgary, AB, Canada Posts: 6,356 You can also do this: Code: a = b and c or d instead of this: Code: if b then a = c else a = d end Looks like it will misbehave if you give it nil arguments though.
 05-17-2016, 03:58 PM #50 Lokasenna Human being with feelings     Join Date: Sep 2008 Location: Calgary, AB, Canada Posts: 6,356 9. Working with strings 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 Just a short lesson today. We're going to look at two classes that are almost, but not quite, doing the same thing: OptLst, commonly known as "radio buttons", and ChkLst, a series of checkboxes. OptLst lets you choose one of the listed options, whereas ChkLst can have each option turned on or off separately. As you can see in the library script they use the same parameters for creation - our old buddies x,y,w,h, and three new ones: Code: caption Title / question opts String separated by commas, just like for GetUserInputs(). ex: "Alice,Bob,Charlie,Denise,Edward" pad Padding between the caption and each option opts is the one we'll be focusing on here. By using some of Lua's built-in features for doing work on strings, a list of options, or values, or whatever can be stored as one continuous string. The same thing could be accomplished with a table, but for educational purposes I've opted to do things this way. As you can see from the example in the code box above, the list of options is simply written out in the form "Option A,Option B,Option C,...". Comma-separated strings, or CSVs, are fairly common in programming and databases, so it's good to be familiar with them; for instance the Reaper function reaper.GetUserInputs() includes two CSVs in its parameters and returns the user's responses in a third. The first thing we do with our options is - surprise! - put them into a table: Code:  -- Parse the string of options into a table opt_lst.optarray = {} local tempidx = 1 for word in string.gmatch(opts, '([^,]+)') do opt_lst.optarray[tempidx] = word tempidx = tempidx + 1 end opt_lst.numopts = tempidx - 1 string.gmatch() is really powerful. Almost... TOO powerful... . That crazy jumble of symbols that looks like someone was holding the Shift key down while they typed a number? It's a pattern that Lua will look for in our string (opts). As you can probably guess, that pattern translates to "Find a group of characters that are before a comma", and in this case we're going to use that group in a for loop: Code: for each group of characters, which we'll call word, do this: put the word into our table using the current index number ChkLst only: use the same index number to set that option's initial state increase the index number repeat with the next word The index number is then used to figure out how many options there are, and therefore how many buttons/boxes to draw. The :draw() methods for both are pretty similar aside from one drawing circles and the other drawing squares. OptLst also only has one value telling it which option is selected, while ChkLst uses a table to store each option's state individually. Likewise, their mouse methods are virtually identical. OptLst has a little extra code in :onmouseup so that the user can drag over the options and have the bubble follow their mouse, but reset to the initial state if the mouse is moved off the option list before they let the button up. Just a visual thing, not necessary by any means. As with the other interactive elements, both OptLst and ChkLst store their current selection in the variable retval. For ChkLst, retval is a table storing option 1's status at index 1, etc. We can find a few more string functions further down, in the TxtBox class. For instance, it's a fairly standard convention that double-clicking on a text box will select all of its contents. To do that, our TxtBox:ondoubleclick() method needs to know how long the string is: Code: local len = string.len(self.retval) string.len() simply returns the length in characters of a given string. Child's play. Moving on to :ontype() we can find another new function, in the code for Backspace: Code: text = string.sub(text, 1, caret - 1)..(string.sub(text, caret + 1)) string.sub(text, start, end) takes a string, text, and returns a portion of it from start to end. As you can see here, we're using it twice to get the text before and after the character we just backspaced. The .. in between tells Lua to join two strings together. A little further down, in the code for typing a character, we can see strings being joined in a different way: Code: text = string.format("%s%c%s", string.sub(text, 1, caret), char, string.sub(text, caret + 1)) string.format(formatting, str1, str2...) uses some fancy symbols in its first argument to make sure the remaining arguments fit into a certain format. %s is just a string, while %c converts the char value that was typed from an ASCII number into a character. We'll take a longer look at the TxtBox in another lesson. For a more thorough explanation of patterns, capture characters, and string stuff in general, see here, and here. Last edited by Lokasenna; 05-18-2016 at 03:33 PM.
 05-18-2016, 02:52 PM #51 Sexan Human being with feelings     Join Date: Jun 2009 Location: Croatia Posts: 2,452 Learning from start,can someone tell me whats wrong with this picture: Code: Template = {} Button = {} function Template.new(x,y,h,w) local self = {} self.x, self.y = x , y self.w, self.h = w , h return self end function Template.draw() gfx.rect(x,y,w,h) end function DRAW(tbl) for key,btn in pairs(tbl) do Template.draw() end end function main() w,h = 20,20 y,x = 10,10 for i = 1 , 5 do Button[i] = Template.new(x,y,h,w) x = x + 10 end DRAW(Button) 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() why there are no 5 buttons but only 1 ? Last edited by Sexan; 05-18-2016 at 03:02 PM.
05-18-2016, 03:50 PM   #52
cyrilfb
Human being with feelings

Join Date: Apr 2012
Location: Denver, CO
Posts: 247
How to set a text flag

Quote:
 Originally Posted by Lokasenna 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. Note: I can't figure out how to set the flags. If anyone knows, let me know and I'll edit it in here.
Lokasenna, After a bit of research, I found that the flags were actually expected to be a number, not a string. 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.
__________________
Cy Ball
Reaper + inspiration = music.
soundcloud.com/cyball or cyball.bandcamp.com/ or cyrilfb.com

 05-18-2016, 04:46 PM #53 Lokasenna Human being with feelings     Join Date: Sep 2008 Location: Calgary, AB, Canada Posts: 6,356 (Edited the String lesson with a couple more functions I forgot to include) cyrilfb, awesome. I'll add that to the lesson. Sexan, I see a few problems: - Your Template.draw function doesn't look at the current button's coordinates, it's just reading the x,y,w,h you specify in your main function. As a result, I think it's drawing five buttons in the same spot. - Your DRAW function doesn't actually pass the current button to Template.draw anyway. - Because the buttons are being created inside your main function, and then you're deferring main, the buttons are being created every single time it loops. They should be created separately - look at the LS GUI usage demo, and notice that all of the elements are created at the top-level, before any functions are called. See if this works: Code: Template = {} Button = {} function Template.new(x,y,h,w) local self = {} self.x, self.y = x , y self.w, self.h = w , h return self end function Template.draw(btn) gfx.rect(btn.x,btn.y,btn.w,btn.h) end function DRAW(tbl) for key,btn in pairs(tbl) do Template.draw(btn) end end w,h = 20,20 y,x = 10,10 local temp_x = x for i = 1 , 5 do Button[i] = Template.new(temp_x,y,h,w) temp_x = temp_x + 30 end function main() DRAW(Button) 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-19-2016 at 03:22 PM.
 05-19-2016, 07:27 AM #54 Sexan Human being with feelings     Join Date: Jun 2009 Location: Croatia Posts: 2,452 Can anyone of you translate this script above to ":". I have trouble understanding how and when to use ":" and ".".I know when using Template:new() it uses self reference (so Template:new() is equal to Template.new(Template) but I still dont get it.If I go with ":" am I in metatable domain? because I want to build this without metatable? I will have better understanding whats going on if the script above is translated.
 05-19-2016, 03:17 PM #55 Lokasenna Human being with feelings     Join Date: Sep 2008 Location: Calgary, AB, Canada Posts: 6,356 Code: Template = {} function Template:new(x,y,h,w) local temp = {} temp.x, temp.y = x , y temp.w, temp.h = w , h setmetatable(temp, self) self.__index = self return temp end function Template:draw() gfx.rect(self.x, self.y, self.w, self.h) end function DRAW(tbl) for key, btn in pairs(tbl) do btn:draw() end end w,h = 20,20 y,x = 10,10 Button = {} local temp_x = x for i = 1 , 5 do Button[i] = Template:new(temp_x,y,h,w) temp_x = temp_x + 30 end function main() DRAW(Button) 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() I had to increase the value for temp_x to make the individual buttons visible. If your buttons are 20 pixels wide and you only increase x by 10 each time, they just make one big rectangle. Further reading... Lua reference manual - Object-oriented programming. There's an explanation of using the : syntax about halfway down Lua reference manual - Classes
 05-21-2016, 09:21 AM #57 Lokasenna Human being with feelings     Join Date: Sep 2008 Location: Calgary, AB, Canada Posts: 6,356 11. Example classes - TxtBox, Sldr, and Knb 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 TxtBox Typing. Everyone does it. Your script might want to do it too. Here's how. (Cheers to schwa for his text box example, which this class and methods were largely copied from) As we saw last time, GUI.Update() has each element check to see if it's been typed in. This requires a) the user to have pressed a key and b) the element to be in focus. focus is set when the element detects a left-click, and here it will activate the TxtBox for typing. In the :onmousedown() method, we place the typing caret (the little blinking line) using another method that's exclusive to TxtBox - :getcaret(): Code: self.caret = self:getcaret() This is done by looping through the textbox's contents, one character at a time, and seeing if the x position of that character matches the position of the mouse cursor. Code:  for i = 1, len do w = gfx.measurestr(string.sub(self.retval, 1, i)) if GUI.mouse.x < (self.x + self.pad + w) then return i - 1 end end As you would expect, :ondrag() lets you select part of the text and :ondoubleclick() selects all of it. The selection is stored as a position relative to the caret. :ontype() is, surprise surprise, significantly larger than other methods. However, after deleting any selected text, the bulk of the function is just one big "if/then, elseif, elseif, elseif..." statement because we have to tell Lua what to do for each keyboard key. For this example I've programmed in the arrow keys, backspace, and delete. All that's left, then, is to draw everything. The frame and text are drawn as you would expect, and for selected text we draw a rectangle on top and then redraw that portion of the text in a different color. If the box is focused, we also need to draw the caret; to make it blink, the text box uses a counter to keep track of how many times the Main() loop runs and only draws the caret half of the time. Code: self.blink = (self.blink + 1) % 16 For those not familiar, the % symbol is for the mathematical operation "modulo", which gives you the remainder after subtracting as many 16s (in this case) as you can from the original number. Stated another way, this line of code just adds 1 to the current total and loops back to 0 when it hits 16, over and over. Sldr Now that we're all experts on clicking and typing, how about dragging? Because the Sldr class also has a few new parameters, we'll start right at the beginning: Code: function Sldr:new(x, y, w, caption, min, max, steps, default) The first thing you'll notice is that it's missing a parameter for height. Why? Because width as all we care about; we're hardcoding the size of the handle, so it wouldn't make sense to let users adjust the height of the "track". You could rewrite it to include height if you wanted to, you weirdo. We've also got the slider's minimum and maximum values, how many steps to include between them, and a default step to start the slider on. Sldr only deals with steps when it's communicating with the user, with all of the internal stuff handled as decimal values between 0 and 1, so we need to convert the :new() parameters from steps to decimals: Code:  sldr.curval = sldr.curstep / steps sldr.retval = ((max - min) / steps) * sldr.curstep + min Notice that curval is our 0-1 value, and retval is adjusted for the min, max, and step parameters. The :onmousedown() method is very similar to TxtBox's - it checks the position of the mouse cursor within the slider and sets the slider to the closest value. :ondrag(), however, takes a little more effort, as it needs to know a) how far it's been dragged and b) if the Ctrl key is being held down. Ctrl is a fairly common modifier for sliders and knobs that allows for finer control. To track how far the slider has been dragged, GUI.Update() stores the mouse position from the previous Update loop in GUI.mouse.lx and ly. Back to Sldr:ondrag(), you can see that we're just comparing the current position of the mouse to where it was in the last loop, and multiplying the difference by a constant value. To allow Ctrl as a modifier, it's just a matter of choosing between two different constants based on the Ctrl state. Both methods also check to make sure the new slider value is still inside the slider, and then they round the value to the nearest step. As far as drawing the Sldr there isn't too much going on that we haven't previously covered. A roundrect() provides the track, gfx.circle() for the handle, and then the caption and current value. Knb Knobs are pretty much identical to sliders. :ondrag() looks at changes in the y value of the mouse rather than x, and knobs turn instead of moving, so each step is an angle instead of a horizontal offset. In Knb:new() we figure out the size of each step: Code:  -- Determine the step angle knb.stepangle = (3 / 2) / knb.steps This part might be really upsetting for anyone who didn't enjoy high school math class. One word for you: radians. Here's a brief introduction for those who need to brush up. *pause for the screaming and rioting to settle down* Radians are a tidier way of measuring things on a circle than degrees, and ReaScript has a few functions that require you to use radians as well. In short, 2pi radians is a whole circle - 360 degrees, and pi radians is 180. For a knob, the minimum and maximum positions are often at the southwest and southeast positions, equal to three quarters of the circle or 3/2pi. Moving on to :draw() we first convert the x,y,w values to things that gfx.circle() can use - coordinates for the center of the circle, and a radius. Next we figure out the current angle (in radians) based on which step the knob is on. Code:  local curangle = (-5 / 4) + (curstep * stepangle) Things worth noting: - Radian math uses east as its 0. That -5/4 is just an offset so that our minimum is where we want it to be. - I've specifically written the code to avoid using pi directly. It's not necessary, but I felt that this way was neater and more readable. It might just be me. - :draw() makes good use of another helper function, polar2cart(), which takes a position in radians, along with your circle's center coordinates, and returns the equivalent x,y coordinates. This is also where pi gets factored in. Code: local x, y = polar2cart(angle, r, o.x, o.y) After the tick marks and values are drawn, it's time for a new function. Oh boy oh boy oh boy oh boy! gfx.triangle(x1, y1, x2, y2, x3, y3) I really shouldn't have to explain how that one works. The built-in triangle function will only draw them filled in, so our library includes yet another wrapper function to provide us with a fill option - GUI.triangle(). It adds an additional parameter at the beginning, a 1 or 0 to determine the fill state. Code:  GUI.triangle(0, Ax, Ay, Bx, By, Cx, Cy) The knob itself is made of a circle and a triangle smushed together. The following block of code determines three points for the triangle - one sticking out of the circle toward the current value, and two more 90 degrees (1/2pi radians) to either side of it, just inside the circle. Put them together, throw in a shadow, and you've got yourself on sexy knob. As with Sldr, Knb works with the steps as values from 0 to 1 and then converts them to the correct format when drawing text and returning a value.
 05-22-2016, 06:01 AM #58 Sexan Human being with feelings     Join Date: Jun 2009 Location: Croatia Posts: 2,452 One question since I can't get any response elsewhere,how can you make button do something when alt/ctrl/shift clicking?In my case I want to remove the button when alt clicked
 05-22-2016, 10:06 AM #59 Lokasenna Human being with feelings     Join Date: Sep 2008 Location: Calgary, AB, Canada Posts: 6,356 - Add code in the button's :onmousedown() method to check for the state of Alt/Shift/whatever. See Lesson 5. - For removing an element, you just need to set that element to nil: Code: local function remove_elm(elm) for key, value in pairs(GUI.elms) do if key == elm then GUI.elms[key] = nil end end end For example, here's a button that just removes itself and nothing else: Code: my_btn = GUI.Btn:new(100, 200, 200, 75, "Delete the button", remove_elm, "my_btn") Things worth noting: - The name you give has to be a string, i.e. "my_btn" and not my_btn - If the button is the only element in GUI.elms, it will delete the button but it won't be erased from the window. However, you probably aren't going to just have a button that deletes itself and nothing else. (Obviously these examples are written for LS GUI, so you'd have to adapt them to whatever you've got set up) Last edited by Lokasenna; 05-22-2016 at 04:39 PM.
 05-22-2016, 11:44 AM #60 cyrilfb Human being with feelings     Join Date: Apr 2012 Location: Denver, CO Posts: 247 Thanks Lokasenna, thanks for this tutorial. Just the right level for me. Your teaching techniques are great. Keep up the good work! __________________ Cy Ball Reaper + inspiration = music. soundcloud.com/cyball or cyball.bandcamp.com/ or cyrilfb.com
 05-22-2016, 04:35 PM #61 Lokasenna Human being with feelings     Join Date: Sep 2008 Location: Calgary, AB, Canada Posts: 6,356 12. Using images Today we're going to learn how to work with images: Dude. It's just like the first time I saw someone playing a Super Nintendo. Pants were crapped that day, let me tell you. First off, I guess we're going to need some images. If you're like me and couldn't draw a picture of the broad side of a barn even if you had a photo to trace over, allow me to direct you here: Knobman This is a really handy little app that makes the process of drawing knobs, sliders, and buttons much easier. There's a bit of a learning curve, though. More Knobman resources: https://www.youtube.com/watch?v=AXn5hD1ncyg https://www.youtube.com/watch?v=NuG72vQ8JCE (ignore the bits about Reaktor) Many people find Photoshop easier for making buttons, as well. To help you get started, I recommend grabbing Leslie Sanford's Knobman files. You just download the file you want, open it in Knobman, and export it with the appropriate number of frames. Alternatively, I've already converted a few of his files to .png form for this lesson's example, so you could just use those if you wanted. Speaking of which, this lesson is NOT going to use the LS GUI library we've been working with previously. Because images are such a different beast, I've rewritten a portion of the library specifically for today's examples. Just download the .zip below, unzip it your scripts folder, and run the "images usage demo" script. https://www.dropbox.com/s/x2ypv93guq...mages.zip?dl=1 Images Reaper uses a very common technique to draw images, known as blitting. Blitting is, basically, telling a program to copy/paste a particular section of one image into another. Images for blitting are typically laid out with all of their frames in one big row or column - in this case, I've opted for columns. If you open knob.png in the lesson files, for instance, you'll see that contains 64 frames of a knob slowly turning around. Accessing these in our script is relatively easy: gfx.loadimg(buffer, file) "I know what a file is, Lokasenna, but what the hell is a buffer?" Easy. Buffers are a bunch of off-screen places for us to store our images. We can manipulate them too, rotating, scaling, drawing new stuff on them, but for this lesson we're just going to put each image in its own buffer and leave it that way. In the images library demo script, you'll see a new table with all of our file names in it: Code: GUI.images = { bg = "\\tutorial images\\bg.png", btn = "\\tutorial images\\button.png", knb = "\\tutorial images\\knob.png", sldr = "\\tutorial images\\slider.png", opt = "\\tutorial images\\opt.png", lbl = "\\tutorial images\\label.png", } The \\s are necessary because Lua has a few special commands that are written as \n, \s, etc. \\ tells Lua that no, this really is supposed to be a \. Code: -- Load all our images into buffers local function load_images() local info = debug.getinfo(1,'S') script_path = info.source:match[[^@?(.*[\/])[^\/]-\$]] local idx = 0 for key, file in pairs(GUI.images) do retval = gfx.loadimg(idx, script_path..file) GUI.images[key] = idx idx = idx + 1 end end GUI.load_images = load_images - First we use the same functions for loading a library from a user script, in this case to find out where the script is being run from. - Next we loop through each item in the table above. The script path and file name are combined and loaded into sequentially-numbered buffers. - Since we don't need the file names anymore, each entry in the image table is instead used to save its buffer number. Once images are loaded, another new function handles drawing them: gfx.blit(source, scale, rotation[, srcx, srcy, srcw, srch, destx, desty, destw, desth, rotxoffs, rotyoffs]) The parameters: - A buffer number - How much to scale the image up or down - How much to rotate the image - Coordinates and dimensions in the buffer to copy from - Coordinates and dimensions in the GUI window to copy to - A couple of optional parameters that let you do fancier transformations with the image. We can ignore them. The source coordinates are of particular importance here, because that's how we specify what frame to grab. If a knob is 64 pixels tall, the first frame would be at y = 0, the second at y = 64, and so on. I think we can agree that gfx.blit() is pretty scary, and it would be nice if we could avoid ever having to call it directly. Good thing some enterprising young (fine, I'm 32) gentleman wrote a helper function for all of this: Code: -- Draw the specified frame from a given image buffer local function img(x, y, w, h, step, buffer) gfx.mode = 0 local img_y = h * GUI.round(step) gfx.blit(buffer, 1, 0, 0, img_y, w, h, x, y, w, h) end GUI.img = img - gfx.mode gives you a few options for how to combine the copied image with whatever's already been drawn in the GUI window. We're making sure it stays set to 0, for normal drawing. - Using the element height h and frame number step, we figure out a y-coordinate in the buffer to get our current frame from. Note that all of the classes in this example have been rewritten so that h specifies the image height, not the element as a whole. For instance, specifying h = 50 for the OptLst class tells it that each individual button in the list is 50 pixels tall, not the entire group. - Finally we blit from the source buffer to the window. Drawing our elements with images Before we do anything else, I suppose we need to have a background. This gets done in the Main() loop, prior to drawing the individual elements: Code:  GUI.img(0, 0, GUI.w, GUI.h, 0, GUI.images.bg) Well that was thrilling. As you can see in the images library demo, each class's :draw() method has been rewritten. Let's start with the Knob and Slider, since they're drawn the same way: Code:  local frame = (curstep / steps) * 63 GUI.img(x, y, w, w, frame, GUI.images.knb) Rather than having to muck around with tick marks, shadows, circles, or polar coordinates, life here has gotten WAY easier. Some basic math tells us what frame we want, and then we phone up GUI.img to get things done. Simple as that. Btn is pretty much the same; its image file only has two frames, not-pushed and pushed, so we just draw whichever one matches the button's current state. The Lbl up top uses frames to do a nice visual trick. The image is, again, two frames - the background, and a sort of frosted-over cover. I was trying to make it look like a little LED readout, but it didn't quite work. Oh well. To create this effect, we just draw the first frame, print our text on top of it, and draw the second on top of that. Because the second frame's pixels are mostly transparent, you can still read the text. Lastly we've got OptLst, which coincidentally requires the most work to draw. Almost like I planned it this way, right? The first thing you might notice if you compare the source image to the functioning script is that, in the source, there's a lot of empty space around the button. This wasn't my choice, it's just how the button came when I grabbed it from the link above. So why isn't there any empty space around the options in our GUI? Simple - :new() takes an extra parameter, img_pad, that lets us control the spacing between the options. For this image, setting the image pad to -12 sticks them all together. :new() also keeps track of the individual option height separately from their collective height, since we'll need to know both. Moving on to :draw(), we just have a loop drawing each button and then hopping down by one button height for the next one. It also checks if the current button is pushed and grabs the second frame when needed. The button text also moves over when it's being pushed, both here and in the original LS GUI library. To do so, we just add 2 to the x and y coordinates before drawing the text. I think it looks nice. And with that, we've come to the end of my lesson plan. Hopefully you learned something. Last edited by Lokasenna; 04-16-2018 at 07:16 AM.
 05-22-2016, 04:43 PM #62 X-Raym Human being with feelings     Join Date: Apr 2013 Location: France Posts: 6,061 I never understood loading image in GFX before this lesson. ^^ __________________ My ReaScripts - Premium Scripts - X-Raym Theme - Stash Files - Feature Requests - Learn ReaScript - Website - Donation - ReaComics - Scripts Custom Dev
 06-04-2016, 06:43 AM #63 Sumalc Human being with feelings   Join Date: Oct 2009 Location: France Posts: 561 I think this topic should remain in the beginning and have "sticky" status to prevent its valuable content disappears in these pages, isn't it?
 06-10-2016, 09:50 PM #64 Lokasenna Human being with feelings     Join Date: Sep 2008 Location: Calgary, AB, Canada Posts: 6,356 Surprise! I came up with another lesson. 13. Having your script interact with the GUI elements Note: The tutorial version of my GUI library we've been using up to this point won't work for this lesson, so you'll have to grab the release version found here: Lokasenna_GUI on the ReaTeam Github repository. Be aware that since the library file's name has changed, your script's reference to it will have to change as well. Have a look at the included example script for an, um, example. In exchange for going through this herculean effort, you may be pleased to know that there are a couple of new elements to play with (Range slider and Menu box) as well as even more thorough commenting and documentation for each class. -------------------------------- Anyway, I realized today that aside from drawing pretty pictures, all we've actually learned how to *do* so far is run a function when someone clicks your Button. Boooooorrriiiiinnnnngggggg. Luckily I designed the library with this exact situation in mind. As part of the reaper.defer() loop that keeps everything going, you can have Lokasenna_GUI run a function of your own by passing that function to GUI.func: Code: local function do_stuff_on_each_loop() ...blah blah blah... end GUI.func = do_stuff_on_each_loop GUI.freq = 0 GUI.Init() GUI.Main() The other variable there, GUI.freq, sets how often (in seconds) to run your function. Setting it to 0 as we have here will call your function on every loop, which is fine for most things. However, if your GUI is interacting with the Reaper window in any significant way - changing the time selection to match a pair of sliders, for instance - running it too often will annihilate your CPU usage in the same way a hangover breakfast at Taco Bell will annihilate your apartment's plumbing. What sorts of exciting things can we put in this function? Glad you asked. Linking two elements together Let's say you're making a track inspector window - everyone loves those. As part of this project, you want to have both a pan knob (which we'll call "knb_pan" for obvious reasons) and a textbox ("txt_pan") where the user can type in a value themselves. When they adjust the knob the textbox needs to change as well, and vice versa. Code:  local focus = GUI.elms.text.focus local knb = GUI.Val("knb_pan") if last_focus == true and focus == false and GUI.char == 13 then GUI.Val("knb_pan", tonumber(GUI.Val("text"))) elseif last_knb ~= knb then GUI.Val("text", knb) end last_knb = knb last_focus = focus - We find out if the textbox is focused or not. - GUI.Val() is a new function in this version of the library, used here to grab the knob's value. GUI.Val(elm[, newval]) - returns the given element's value or, if a new value is given, sets the element's value instead - If the textbox was previously in focus, but isn't anymore, and the user pressed Enter, we use GUI.Val() again to update the knob. - Otherwise, if the knob has been changed, we update the textbox instead. - Lastly, we store both elements' current values to compare with on the next loop. Re-labeling a knob If you're using a knob to let the user choose note lengths or grid sizes, you probably want the options listed in note lengths. Right? Right. Code: GUI.elms.note_len.output = { [0] = "1/64", "1/32", "1/16", "1/8", "1/4", "1/2" } This has to be placed after you've created the knob, or it will be erased by the :New method. If you look at Knobraw(), you'll see that it checks for the existence of an output table prior to drawing each value. If the table is there, it uses the strings from there instead of numbers. And what if you want to know which value, in note lengths, the knob is set to? Just place the following in your user function and then do whatever you want with it: Code: local value = GUI.elms.note_len.output[GUI.Val("note_len")] GUI.Val() tells us what the knob is set to, which we hand right over to the output table to get the matching string. Appending text to a Slider Numbers really are boring by themselves, aren't they? This example adds "db" to the displayed value on a volume slider. Code:  local vol = GUI.Val("vol_sldr") if vol ~= last_vol then GUI.elms.vol_sldr.output = vol.."db" end last_vol = vol Just like the Knob, the Slider has an optional output parameter that will override the internal value (Range sliders have output_a and output_b). We grab the value, see if it's changed, and update output accordingly. It's worth noting that both here and in the element-linking example we're only changing the target value if we need to; if the slider hasn't moved, we don't need to change the output because it's already been set to what we want. It doesn't save a ton of resources here, but if you had a script automatically setting track layouts based on their name it would literally crash Reaper unless you performed the same check. Seriously, I tried it once. As you were.
 07-05-2016, 01:03 PM #65 me2beats Human being with feelings     Join Date: Jul 2015 Location: Yekaterinburg, Russia Posts: 400 wow. thank you very much!
 08-02-2016, 09:21 PM #66 woodslanding Human being with feelings     Join Date: Mar 2007 Location: Denver, CO Posts: 402 I'm trying part 2 of the tutorial, but the IDE is complaining. Here's my code: Code: local function Main() local char = gfx.getchar() if char ~= 27 and char ~= -1 then reaper.defer(Main) end gfx.update() end local function rgb2num(red, green, blue) green = green * 256 blue = blue * 256 * 256 return red + green + blue 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() When I run it, it gives the error: attempt to call a number value (field 'clear') Can't see what I might be doing wrong.... __________________ eric moon Very Stable Genius https://gogolab.com/
 08-03-2016, 04:11 AM #67 Lokasenna Human being with feelings     Join Date: Sep 2008 Location: Calgary, AB, Canada Posts: 6,356 That's a whoops on my part. gfx.clear is a variable, not a function, so the line should look like: Code: gfx.clear = rgb2num(r, g, b) I've corrected the original example.
 08-03-2016, 01:26 PM #68 mpl Human being with feelings     Join Date: Oct 2013 Location: Moscow, Russia Posts: 2,542 What I noticed from this thread is even if I try to draw nice GUI, there is no way to run from old noobie coding habbits to use functions and tables instead any elegant solutions explained here. Any synthax with : or :: or lua metatables looks very complex and so hard to understand for me. Hope Ill come back to this amazing tutorial later with more knowledges. __________________ SoundCloud | MPL Scripts discussion | ReaPack | Donate
 08-03-2016, 04:49 PM #69 Nz0 Human being with feelings     Join Date: May 2016 Location: Switzerland Posts: 19 require ("Lokasenna_GUI Library") Hello Lokasenna Thanks a lot for putting this helpful library together. Do you know that you can create a lua folder in the Reaper folder (i.e: C:\Program Files\REAPER (x64)\lua), save your library there and call it from any script using the following? Code:  require ("Lokasenna_GUI Library") No need to copy your code over and over for each GUI you develop. HTH Nz0
 08-03-2016, 06:19 PM #70 Lokasenna Human being with feelings     Join Date: Sep 2008 Location: Calgary, AB, Canada Posts: 6,356 That's good to know - I got really frustrated trying to come up with a more reasonable way to do that.
08-04-2016, 04:57 PM   #71
woodslanding
Human being with feelings

Join Date: Mar 2007
Location: Denver, CO
Posts: 402

Quote:
 Originally Posted by Lokasenna That's a whoops on my part. gfx.clear is a variable, not a function, so the line should look like: Code: gfx.clear = rgb2num(r, g, b) I've corrected the original example.
Ahh, so much more satisfying!

I remember reading it was a variable, and thinking (with my beginner LUA brain) how can you tell??
__________________
eric moon
Very Stable Genius
https://gogolab.com/

08-04-2016, 08:51 PM   #72
Lokasenna
Human being with feelings

Join Date: Sep 2008
Posts: 6,356

You can't, really, except that the section of the API that gives you gfx.clear starts with this:

Quote:
 The following global variables are special and will be used by the graphics system
It took me a while at first to notice it.

08-04-2016, 09:18 PM   #73
cfillion
Human being with feelings

Join Date: May 2015
Posts: 2,831

Quote:
 Originally Posted by woodslanding how can you tell??
Lua often give useful hints in the error messages:

Quote:
 somefile:10: attempt to call a number value (field 'clear')
With this you can discover that:
1. the "field" clear used at line 10 is a number
2. the script attempted to use it like a function

Another way to check:
Code:
reaper.ShowConsoleMsg(type(gfx.clear) .. "\n") --> number
reaper.ShowConsoleMsg(type(gfx.line) .. "\n") --> function

Last edited by cfillion; 08-04-2016 at 09:25 PM.

 08-05-2016, 12:15 PM #74 woodslanding Human being with feelings     Join Date: Mar 2007 Location: Denver, CO Posts: 402 Rereading.... Woah, so there is actually no necessary syntactical difference between a function and a variable?? I'll get used to it THANKS! __________________ eric moon Very Stable Genius https://gogolab.com/
08-06-2016, 06:52 PM   #75
Nz0
Human being with feelings

Join Date: May 2016
Location: Switzerland
Posts: 19

Quote:
 Originally Posted by Lokasenna That's good to know - I got really frustrated trying to come up with a more reasonable way to do that.
I think that I found a way to reduce your frustration even more

I came across a lua library called "lupy", a simple Python-like OO implementation for lua. It does a lot of things that I don't even understand, nevertheless I made a toy example with encapsulation.

You can define virtual functions, not sure if the term is correct. I mean you define the function in the object file and then override it in your main script.

Extract the Zip-File under "C:\Program Files\REAPER (x64)". For simplicity I also included the lupy code. Load AKAI_MPK25_Track_Control.lua in Reaper as usual. Try left and right clicking on the pads.

lupy: https://github.com/uleelx/lupy
other OO libs: http://lua-users.org/wiki/ObjectOrientedProgramming

HTH
Nz0
Attached Files
 AKAI_MPK25_Track_Control.lua (965 Bytes, 131 views) Pad.zip (11.8 KB, 40 views)

 12-12-2016, 10:13 AM #76 Spacemen Tree Human being with feelings     Join Date: Mar 2013 Posts: 501 Just dropped by to say thanks again for this. This tutorial is great and has helped me quite a bit. Plus I'm having fun mangling your LS GUI script. I wish I could code like that. Very well thought out and it's easy to follow. Could I interest you in updating this with a how to make a scroll bar? Would be super. Thanks __________________ "After silence, that which comes nearest to expressing the inexpressible is music", Aldous Huxley
12-12-2016, 12:45 PM   #77
schwa

Join Date: Mar 2007
Location: NY
Posts: 10,316

Lokasenna, this is a terrific tutorial! I don't know how I haven't seen it before.

Quote:
 Originally Posted by Lokasenna 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.

Code:
BOLD=98
ITALIC=105
UNDERLINE=117

gfx.setfont(1,"arial",20,(ITALIC<<8)|UNDERLINE)
-- equivalent:
gfx.setfont(1,"arial",20,tonumber(string.format("0x%02X%02X",ITALIC,UNDERLINE)))

 12-12-2016, 12:47 PM #78 Lokasenna Human being with feelings     Join Date: Sep 2008 Location: Calgary, AB, Canada Posts: 6,356 We'll see, but I doubt it. My spare time is extremely limited right now, so what I do get ends up being spent on guitar. As for the scrollbar, you mean for working with more stuff than the window can fit? I'm not sure LS GUI can do that as is - you'd have to add an offset value for where the "current" top-left corner of the window is, and use that in the other elements' drawing routines to determine where they should show up. I'll add it to my to-do list anyway, though.
 12-12-2016, 12:51 PM #79 schwa Administrator     Join Date: Mar 2007 Location: NY Posts: 10,316 FWIW the lyrics editor script (lyrics.lua) that will be included with REAPER 5.30 includes a text entry box and scrollbar, which may be useful as coding examples (and could certainly be improved further!).
12-12-2016, 01:38 PM   #80
Justin

Join Date: Jan 2005
Location: NYC
Posts: 12,485

Quote:
 Originally Posted by schwa Code: BOLD=98 ITALIC=105 UNDERLINE=117 gfx.setfont(1,"arial",20,(ITALIC<<8)|UNDERLINE) -- equivalent: gfx.setfont(1,"arial",20,tonumber(string.format("0x%02X%02X",ITALIC,UNDERLINE)))

Or:
Code:
gfx.setfont(1,"Arial",20, (string.byte('i')<<8)|string.byte('u'));
The ordering of the various bytes is not important. This is a carryover from EEL, which has a convenient multibyte character syntax, e.g. gfx_setfont(1,"Arial",20,'iu');

Another bit of helper code:
Code:
function fontflag(str) local v=0; for a=1,str:len() do v=v*256+string.byte(str,a) end return v end

gfx.setfont(1,"Arial",20, fontflag('iu'));`

Last edited by Justin; 12-12-2016 at 01:47 PM.

 Thread Tools Display Modes Linear Mode

 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 Rules
 Forum Jump User Control Panel Private Messages Subscriptions Who's Online Search Forums Forums Home General Discussion     General Discussion (aka spam trap) REAPER Forums     REAPER General Discussion Forum     newbieland     REAPER Q&A, Tips, Tricks and Howto     Recording Technologies and Techniques     REAPER Compatibility     REAPER Color Themes and Icon Sets     MIDI Hardware, Control Surfaces, and OSC     REAPER Non-English Speaking User Forums         Forum de REAPER en français         Foro de REAPER en Español         Fórum do REAPER em português         Forum di REAPER in italiano         Deutschsprachiges REAPER Userforum         Pyccкоязычный фopyм REAPER     REAPER Bug Reports     REAPER Feature Requests     Dstruct's Casa De Nitpicks     REAPER for Live Use     REAPER for Video Editing/Mangling     REAPER for Ambisonic and 3D positional audio uses     ReaScript, JSFX, REAPER Plug-in Extensions, Developer Forum     REAPER for macOS X     REAPER for Linux     REAPER Pre-Release Discussion     REAPER Music/Collaboration Discussion     REAPER lounge NINJAM Discussion     NINJAM User Discussion     NINJAM Developer Discussion Other Software Discussion     WDL users forum     LICEcap Discussion     OSCII-bot forum     Old Cockos Products Forum

All times are GMT -7. The time now is 02:32 PM.

 -- Cockos ---- REAPER 5 ---- Reaper 3 ---- Reaper 2 ---- Reaper 1 Contact Us - Çockos Incorporated - Archive - Top