Posted 22 July 2017 - 02:40 AM
TL;DR: Scroll to the bottom to see the final code, but I encourage you to read the background to understand why this is a confusing problem in the first place and how the solution came to be.
Early 2017, I created a program for which I wrote my own GUI. I wanted the user to be able to write some data – for example, a name – into a textfield. More importantly, I wanted the user to be free to change that data later. And most importantly, I wanted the textfield to act like they do in real computers: Selecting the textfield places the cursor on the existing text.
Unfortunately, read() always assumes you're inputting a new line. At best, it seems like the only option is to erase the textfield and expect a new input. But that's not what we want – what if the user decides they don't want to change the value? It's cumbersome to expect them to re-type the previous data.
When I was researching this problem, the forum had it narrowed down to two options:
This is a way to retain the textfield's value. Even though the parameter is intended for command history, it's just a table of previous strings that can be cycled through. So when calling read() for the textfield, you can just pass the existing data, wrapped in a table: read(nil,{data}). We pass nil for the first parameter because we don't want to replace the characters that are printed to the screen.
Hang on. read() is still blank until the user presses "up". We don't want to expect this every time they change the text. But the computer is listening for an "up" keypress before it cycles through the history.
With os.queueEvent(), we can essentially tell the computer that the user pressed the "up" key. But there's another problem. You can't just queue the event directly after the read() function – read() is still running! Is there any way around this one?
I first looked at the third optional parameter autocomplete (function). All we really need is a function that can queue the "up" keypress. Unfortunately, autocomplete isn't documented and I couldn't figure out how it works. But that did give me another thought. We basically want read() to be invoked, and then "up" to be queued before read() finishes.
By using the Parallel API, we can do this. Specifically, we need to use parallel.waitForAll(), because the alternative (waitForAny()) will quit once "up" is queued. Using waitForAll(), the first parameter will be invoked first (it will have to be read() in this case), and then once that function yields (e.g. to read user input), the second parameter will be invoked (os.queueEvent()). Once it's finished, it'll go back to read(), and the user can now edit the existing data!
Note: The Parallel API accepts functions as parameters. All we have to do to make our code work with Parallel is wrap our code in anonymous functions.
The final code:
Here, value is the end value that the user enters. data is the existing value already in the textfield. The arguments to os.queueEvent() tell CC that we pressed the "up" key, but we're not holding it down.
In my application, I wrapped this code in a small function readfield(), which also sets the cursor to the location of the textfield. You can implement this however you want. Writing a program with a GUI in ComputerCraft was one of the most fun things I've ever coded! I highly recommend it. This was probably the most valuable thing I learned throughout the experience. It taught me a lot about the built-in functions, as well as the important Parallel API.
Early 2017, I created a program for which I wrote my own GUI. I wanted the user to be able to write some data – for example, a name – into a textfield. More importantly, I wanted the user to be free to change that data later. And most importantly, I wanted the textfield to act like they do in real computers: Selecting the textfield places the cursor on the existing text.
Unfortunately, read() always assumes you're inputting a new line. At best, it seems like the only option is to erase the textfield and expect a new input. But that's not what we want – what if the user decides they don't want to change the value? It's cumbersome to expect them to re-type the previous data.
When I was researching this problem, the forum had it narrowed down to two options:
- Use an existing GUI API that handles textfields gracefully.
- Rewrite the built-in read() function.
This is a way to retain the textfield's value. Even though the parameter is intended for command history, it's just a table of previous strings that can be cycled through. So when calling read() for the textfield, you can just pass the existing data, wrapped in a table: read(nil,{data}). We pass nil for the first parameter because we don't want to replace the characters that are printed to the screen.
Hang on. read() is still blank until the user presses "up". We don't want to expect this every time they change the text. But the computer is listening for an "up" keypress before it cycles through the history.
With os.queueEvent(), we can essentially tell the computer that the user pressed the "up" key. But there's another problem. You can't just queue the event directly after the read() function – read() is still running! Is there any way around this one?
I first looked at the third optional parameter autocomplete (function). All we really need is a function that can queue the "up" keypress. Unfortunately, autocomplete isn't documented and I couldn't figure out how it works. But that did give me another thought. We basically want read() to be invoked, and then "up" to be queued before read() finishes.
By using the Parallel API, we can do this. Specifically, we need to use parallel.waitForAll(), because the alternative (waitForAny()) will quit once "up" is queued. Using waitForAll(), the first parameter will be invoked first (it will have to be read() in this case), and then once that function yields (e.g. to read user input), the second parameter will be invoked (os.queueEvent()). Once it's finished, it'll go back to read(), and the user can now edit the existing data!
Note: The Parallel API accepts functions as parameters. All we have to do to make our code work with Parallel is wrap our code in anonymous functions.
The final code:
parallel.waitForAll(
function() value = read(nil,{data}) end,
function() os.queueEvent("key",200,false) end
)
Here, value is the end value that the user enters. data is the existing value already in the textfield. The arguments to os.queueEvent() tell CC that we pressed the "up" key, but we're not holding it down.
In my application, I wrapped this code in a small function readfield(), which also sets the cursor to the location of the textfield. You can implement this however you want. Writing a program with a GUI in ComputerCraft was one of the most fun things I've ever coded! I highly recommend it. This was probably the most valuable thing I learned throughout the experience. It taught me a lot about the built-in functions, as well as the important Parallel API.