This is a read-only snapshot of the ComputerCraft forums, taken in April 2020.
CodeWeaver's profile picture

Editable Textfield (Persistent Value)

Started by CodeWeaver, 22 July 2017 - 12:40 AM
CodeWeaver #1
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:
  1. Use an existing GUI API that handles textfields gracefully.
  2. Rewrite the built-in read() function.
I didn't want to go with either of these, so I dug deeper. Upon closer inspection of the wiki's page on read() (http://www.computercraft.info/wiki/Read), there's three optional parameters. The one that's particularly interesting is the second one – history {}. This parameter lets you press "up" while in the terminal to access command history.

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.
KingofGamesYami #2
Posted 22 July 2017 - 03:37 AM
I would like to point out that the usage of the parallel API is entirely unnecessary. This does the exact same thing with less overhead:

local function readField( data )
os.queueEvent( "key", keys.up )
return read( nil, {data} )
end

Edit: Since this is the tutorial section, I'll explain a little bit on why this works.

Events are stored in a queue (hence queueEvent) and won't leave until coroutine.yeild is called (usually through os.pullEvent). That means queuing an event before calling read will lead to the event remaining in queue for read to pull, triggering the effect described in the above post.
Edited on 22 July 2017 - 08:02 PM
MineRobber___T #3
Posted 24 July 2017 - 05:06 AM
umm… this is unnecessary?

for string data, just do:


local result = read(nil,nil,nil,data)

See the code. (https://github.com/dan200/ComputerCraft/blob/master/src/main/resources/assets/computercraft/lua/bios.lua#L286)
Wojbie #4
Posted 24 July 2017 - 08:11 AM
umm… this is unnecessary?

for string data, just do:


local result = read(nil,nil,nil,data)

See the code. (https://github.com/dan200/ComputerCraft/blob/master/src/main/resources/assets/computercraft/lua/bios.lua#L286)

That only works in latest beta. Not in released versions.