Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Expanded Documentation and Examples for handling user input #343

Open
sk33z3r opened this issue Jan 30, 2022 · 14 comments
Open

Expanded Documentation and Examples for handling user input #343

sk33z3r opened this issue Jan 30, 2022 · 14 comments

Comments

@sk33z3r
Copy link

sk33z3r commented Jan 30, 2022

Is your feature request related to a problem? Please describe.
Full disclosure, I'm not a python power user, so I may well be missing something obvious with python itself.

I'm building a video game, and have the engine mostly done in curses. I wanted to add fancy animations, so I started trying to port things over to asciimatics. However, I'm finding it hard to follow the docs in some places. For a specific example, I'm finding it hard to sort out how to use wait_for_input() in conjunction with get_event() as outlined in the docs, but there are no examples to pull from. It's not clear to me how I should even begin to get a user's input and then process it using these two methods.

I can see the timeout of wait_for_input() is working in my script, but when I press any character the app just exits immediately. From the explanation in the docs, it made it seem as though I could just keep typing and the app would block execution until something is entered (presumably just hitting enter, but the docs aren't clear here either). I don't think this is a bug, but rather my misunderstanding of how it is supposed to function.

The one example I've tried to follow that sort of has what I'm looking for is forms.py, but I don't see that there is any get_event() or wait_for_input() calls. None of the examples I've dug through seem to have these calls in them.


Describe the solution you'd like
Some example of using wait_for_input() and get_event(), and expand on the docs here:
https://asciimatics.readthedocs.io/en/stable/io.html#input

Would it be possible, at some point, to get an example that allows the user to input a word or short phrase in one frame, then display that entered text as FigletText in its own, separate frame?


Additional context
In my particular application, I've got several frames on-screen where one of those frames is a single-line input. Like a text adventure game, I want to have the user input a simple command then process it to change the db and then update the display with new info.

I've been able to sort out a lot of other details from the docs, but the user input handling is one where I am a little lost. In curses, I was able to do what I needed to do with the textpad.Textbox object with relative ease. I thought that I might be able to use asciimatics's Text widget, but so far have been unsuccessful.

EDIT:

I'm currently running python 3.10 with asciimatics v1.13.0, and I am being careful to dig through examples in the 1.13.0 tag.

@peterbrittain
Copy link
Owner

Ok... Let's work through it hereband then I'll update the docs...

Asciimatics uses raw input with no delay from the terminal (assuming you're familiar with curses). This means when you call get_event(), it will either return you a key (or mouse event) if there is one queued, or nothing if there isn't.

The wait_for_input() function simply waits for any mouse or key input for the time specified, returning early if there is anything, or delaying for the specified time otherwise. I use it to get a more responsive UI for the widgets.

So, you just need to call get_event() whenever you want to process any queued input and wait_for_input() when you have nothing else to do. If you look in Screen.play(), you'll see that this is how it handles each frame... Check for any events, process them, update the screen and then wait until the next frame is due (or some input comes in if you allowed that option).

Asciimatics' high-level objects are more like an asynchronous API, unlike the curses TextBox API. Each component in the widgets/effects/sprites/etc fits into that same basic input/output loop, doing stuff right now as prompted by the current state, or returning nothing because it is not the right time/state. The closest you'll get to a synchronous call like TextBox is to create a Scene, add a Frame, play it, wait for play to end and then check the data in the Frame.

Any clearer?

@sk33z3r
Copy link
Author

sk33z3r commented Jan 31, 2022

Thanks for the response! I didn't expect one so quick.

The wait_for_input() function simply waits for any mouse or key input for the time specified, returning early if there is anything, or delaying for the specified time otherwise. I use it to get a more responsive UI for the widgets.

Initially for some reason, I thought it actually returned the input it received, essentially waiting for user to type something then hit enter. I'm not sure why I had it that way in my mind.

So, you just need to call get_event() whenever you want to process any queued input and wait_for_input() when you have nothing else to do.

I'm a bit confused here. As far as I understand, get_event() only returns a single event. Do I need to grab and store each character entered individually and then string them together? Then it seems I would also have to take the individual keys and print them on screen, so the user can see what's being typed. I think I'm really turned around, because this sounds like a lot of extra stuff if I'm understanding it right.

Asciimatics' high-level objects are more like an asynchronous API, unlike the curses TextBox API. Each component in the widgets/effects/sprites/etc fits into that same basic input/output loop, doing stuff right now as prompted by the current state, or returning nothing because it is not the right time/state.

I guess what's really throwing me off is the Text widget. I was expecting to be able to plop the widget into a Frame, let the user put in text, then store that text and do something with it when enter is pressed. But what I gather from your explanation, asciimatics just doesn't work that way.

The closest you'll get to a synchronous call like TextBox is to create a Scene, add a Frame, play it, wait for play to end and then check the data in the Frame.

I don't quite follow. I've at least gotten as far as creating a scene and playing it, but waiting for play to end and checking the data is where I get lost. I think there are some fundamentals about scenes and play that I have overlooked or misunderstood.

At the moment when I use play, my scene plays and I can see the borders around my frames (but not the content), and there is a Text widget that's taking input and I can see/edit what I'm typing. Is this where I would need to use get_event()? To, say, check for enter and then read the data from the field?

Side note, if I do set_scenes() and draw_next_frame() manually (outside of just play), my content prints to screen but the text box doesn't work.

My brain is in swirls from trying to sort things out today, so I'm having a hard time testing new things at the moment. I'm starting to think that asciimatics is too over my head and/or may not be right for the project. I realize from your response that handling input is not the only thing I'm running into roadblocks with, it just seemed like that was the issue. I'm going to try and read more of the docs tomorrow regarding scenes and screens.

Do you have any examples, or know of any projects I could look into, that use get_event() like you are describing?

@peterbrittain
Copy link
Owner

You're confusing the low level (input/output) API with the high level (object based) API. If you have already created a Frame with a Text widget, that widget will handle all the low level logic to string text together, output it in the right location and store off the full entered text as the value on the widget. You just need to access the value at the time you want it.

@peterbrittain
Copy link
Owner

To get a better idea of various ways to check Frame.data and Widget.value, take a look at the contact_list.py demo.

If you want to have a simple form that returns on pressing enter, you need to create your Scene with suitable Widgets laid out in a Frame, then add a global handler (https://asciimatics.readthedocs.io/en/latest/widgets.html#global-key-handling) to stop the animation on receiving enter. You can then just play this Scene and look at Frame.data when play() returns.

@sk33z3r
Copy link
Author

sk33z3r commented Jan 31, 2022

@peterbrittain Thanks again for being willing and taking some time out of your day to help me sort this out. I was getting discouraged last night, mainly because I'm self-taught and I still think there are some fundamentals in python I'm missing that makes understanding asciimatics a little more difficult. I really want to use it, though, so I'm gonna keep trying and learning.

You're confusing the low level (input/output) API with the high level (object based) API.

I suppose I was, but it came from how I understand get_event(). I'm still unsure how exactly get_event() could get a full string of text. My understanding of this method is that it only reads the last key (or Ctrl+key combo) and not a buffer of multiple keys. Perhaps I am wrong on that? Can I store a buffer of user input, then use get_event() to dump the entire buffer into a variable? EDIT: I guess I could do a loop of some type and store each key into a list, then join that list together for the full value after I detect enter.

Just curious as I'm still confused on that point (and in the spirit of the thread trying to clear things up), but I think that using the Text widget and then getting the stored value is ultimately the right way for me to go in this project.

If you have already created a Frame with a Text widget, that widget will handle all the low level logic to string text together, output it in the right location and store off the full entered text as the value on the widget. You just need to access the value at the time you want it.

From this explanation it sounds like the Text widget can work like I expected it to initially. I see the code in forms.py and contact_list.py that is grabbing the data from the form, and at least at first glance it seems to make sense. I just need to get my hands on it to fully understand (just reading does nothing for me, I need to learn by doing, makes this whole thing more complicated and slow for me, unfortunately). So, I'm going to spend some time trying to write my own based on the example today to try and understand this process more.

If you want to have a simple form that returns on pressing enter, you need to create your Scene with suitable Widgets laid out in a Frame, then add a global handler (https://asciimatics.readthedocs.io/en/latest/widgets.html#global-key-handling) to stop the animation on receiving enter. You can then just play this Scene and look at Frame.data when play() returns.

I think this part of the docs is something I kept overlooking, but it answers a curiosity I had last night regarding the unhandled_input attribute. It's not clear how exactly I can listen for enter, but I will play around with that today as well. Is there a list of the known key_codes anywhere in the docs?

I believe that part of what I'm doing wrong is 1) misunderstanding the Frames effect and 2) not treating Scenes like an ordered list of frames in an animation, but rather just a list of windows to display onscreen.

Regarding Frames, I have been operating under the impression that Frames are the equivalent to curses Windows or Sub Windows (description here makes me think this). Do you intend the Frame effect to be a window analogue, or is it intended to represent a single frame in an animation? I see the word "frames" used many times in the docs related to animation, but because of the explanation and the effect being named that same thing, I'm getting the two confused.

Unless it is intended to be like frames in an animation, and not a Window analogue. Perhaps this is the real reason I'm all turned around. I have 5 frames that get drawn onscreen at the same time right now (because I have 5 distinct sections of data to display to the user). Is this where I'm going wrong? Maybe it was intended that only 1 frame be onscreen at a given time? Or perhaps there's a way to run 5 simultaneous animations, each affecting their own Frame? I can condense all my display down to a single frame, but the look of the full borders is so much better than the dividers (due to connecting, solid lines), I can have padding between the sections, and can set color palettes separately between each frame.

Maybe sending you the curses version of my game would help clear up what exactly I'm trying to do, and might allow you to see where/how I'm approaching asciimatics incorrectly. I don't want to bog you down, though, you're already helping me way more than I expected and probably have a job/family to pay attention to.


I've actually got a couple other questions but somewhat unrelated to the topic of this thread, may be real feature requests. Would you rather I just post them here first, or go ahead and make separate threads?

@sk33z3r
Copy link
Author

sk33z3r commented Jan 31, 2022

I kind of have something a little more coherent working, but there's still a disconnect I'm having somewhere. I can't seem to get my function to trigger when I hit ENTER using the example in the docs. Even if I click and change focus outside of the Text widget.

EDIT: The code below checks for 10, 13, or 343 key_codes, which I believe are \n, \r, and KEY_ENTER, respectively. However, I also tried with the doc's example that tries to grab Ctrl+Q with no luck.

Side note: I can't seem to get my static titles to display if I use Frame.add_effect in the Frame class. They get added to screen, but behind the frames (if I set the coords outside the frame boundaries, then I see the text). The static content will, however, display if I add them in the Scene list, but disappear if I click on that frame in the terminal.

Here's the code I'm running. Maybe you can point out any glaring issues with how I've put it together.

click to expand ascii_test.py

from time import sleep
from asciimatics.widgets import Frame, Layout, Divider, VerticalDivider, Text
from asciimatics.scene import Scene
from asciimatics.screen import Screen
from asciimatics.effects import Print, Background
from asciimatics.renderers import StaticRenderer
from asciimatics.event import KeyboardEvent, MouseEvent
from asciimatics.exceptions import StopApplication

ui_x = 110
ui_y = 40

custom_palette = {
    "background": ( Screen.COLOUR_WHITE, Screen.A_NORMAL, Screen.COLOUR_BLACK ),
    "borders": ( Screen.COLOUR_WHITE, Screen.A_NORMAL, Screen.COLOUR_BLACK ),
    "button": ( Screen.COLOUR_BLACK, Screen.A_NORMAL, Screen.COLOUR_WHITE ),
    "control": ( Screen.COLOUR_WHITE, Screen.A_NORMAL, Screen.COLOUR_BLACK ),
    "disabled": ( Screen.COLOUR_WHITE, Screen.A_NORMAL, Screen.COLOUR_BLACK ),
    "edit_text": ( Screen.COLOUR_WHITE, Screen.A_NORMAL, Screen.COLOUR_BLACK ),
    "field": ( Screen.COLOUR_WHITE, Screen.A_NORMAL, Screen.COLOUR_BLACK ),
    "focus_button": ( Screen.COLOUR_WHITE, Screen.A_NORMAL, Screen.COLOUR_BLACK ),
    "focus_control": ( Screen.COLOUR_WHITE, Screen.A_NORMAL, Screen.COLOUR_BLACK ),
    "focus_edit_text": ( Screen.COLOUR_WHITE, Screen.A_NORMAL, Screen.COLOUR_BLACK ),
    "focus_field": ( Screen.COLOUR_WHITE, Screen.A_NORMAL, Screen.COLOUR_BLACK ),
    "invalid": ( Screen.COLOUR_WHITE, Screen.A_NORMAL, Screen.COLOUR_BLACK ),
    "label": ( Screen.COLOUR_RED, Screen.A_NORMAL, Screen.COLOUR_BLACK ),
    "scroll": ( Screen.COLOUR_WHITE, Screen.A_NORMAL, Screen.COLOUR_BLACK ),
    "selected_control": ( Screen.COLOUR_WHITE, Screen.A_NORMAL, Screen.COLOUR_BLACK ),
    "selected_field": ( Screen.COLOUR_WHITE, Screen.A_NORMAL, Screen.COLOUR_BLACK ),
    "selected_focus_control": ( Screen.COLOUR_WHITE, Screen.A_NORMAL, Screen.COLOUR_BLACK ),
    "selected_focus_field": ( Screen.COLOUR_WHITE, Screen.A_NORMAL, Screen.COLOUR_BLACK ),
    "title": ( Screen.COLOUR_WHITE, Screen.A_NORMAL, Screen.COLOUR_BLACK )
}

player_command = None

def get_player_command(event):

    global player_command

    if isinstance(event, KeyboardEvent):
        c = event.key_code
        if c in (10, 13, 343):
            player_command = command.value
            raise StopApplication("Player entered a command")

class eventsFrame(Frame):
    def __init__(self, screen):
        super(eventsFrame, self).__init__(  screen,
                                            dimensions["events"]["height"],
                                            dimensions["events"]["width"],
                                            has_border=True,
                                            name="events",
                                            x=dimensions["events"]["x"],
                                            y=dimensions["events"]["y"],
                                            reduce_cpu=True,
                                            can_scroll=False)

        event_layout = Layout([100])
        self.add_layout(event_layout)
        self.palette = custom_palette
        event_layout.add_widget(Divider(draw_line=False), 0)
        event_layout.add_widget(Divider(), 0)
        self.fix()

class groundInfoFrame(Frame):
    def __init__(self, screen):
        super(groundInfoFrame, self).__init__(  screen,
                                                dimensions["ground_info"]["height"],
                                                dimensions["ground_info"]["width"],
                                                has_border=True,
                                                name="ground_info",
                                                x=dimensions["ground_info"]["x"],
                                                y=dimensions["ground_info"]["y"],
                                                reduce_cpu=True,
                                                can_scroll=False)

        ground_info_layout = Layout([49, 1, 30])
        self.add_layout(ground_info_layout)
        self.palette = custom_palette
        ground_info_layout.add_widget(VerticalDivider(height=10), 1)
        self.fix()

class characterFrame(Frame):
    def __init__(self, screen):
        super(characterFrame, self).__init__(   screen,
                                                dimensions["character"]["height"],
                                                dimensions["character"]["width"],
                                                has_border=True,
                                                name="character",
                                                x=dimensions["character"]["x"],
                                                y=dimensions["character"]["y"],
                                                reduce_cpu=True,
                                                can_scroll=False)

        character_layout = Layout([5, 28, 7])
        self.add_layout(character_layout)
        self.palette = custom_palette
        self.fix()

class inputFrame(Frame):
    def __init__(self, screen):

        global command

        super(inputFrame, self).__init__(   screen,
                                            dimensions["input"]["height"],
                                            dimensions["input"]["width"],
                                            has_border=True,
                                            name="input",
                                            x=dimensions["input"]["x"],
                                            y=dimensions["input"]["y"],
                                            reduce_cpu=True,
                                            can_scroll=False)

        input_layout = Layout([2, 77])
        self.add_layout(input_layout)
        self.palette = custom_palette
        command = Text(label="\m/:", name="command", max_length=75)
        input_layout.add_widget(command, 1)
        command.focus()
        self.fix()

def main(screen):

    global dimensions

    begin_x = (screen.width - ui_x) // 2
    begin_y = (screen.height - ui_y) // 2

    dimensions = {
        "events": {
            "width": 79,
            "height": 27,
            "x": begin_x,
            "y": begin_y
        },
        "ground_info": {
            "width": 79,
            "height": 10,
            "x": begin_x,
            "y": begin_y + 27
        },
        "input": {
            "width": 79,
            "height": 3,
            "x": begin_x,
            "y": begin_y + 37
        },
        "character": {
            "width": 30,
            "height": 40,
            "x": begin_x + 80,
            "y": begin_y
        }
    }

    screen.play( [
        Scene( [
            Background(screen),
            eventsFrame(screen),
            groundInfoFrame(screen),
            characterFrame(screen),
            inputFrame(screen),
            Print(screen, StaticRenderer(images=[" TED MOONCHILD "]), dimensions["character"]["y"], x=(dimensions["character"]["x"] + 2), speed=0, transparent=False),
            Print(screen, StaticRenderer(images=["{0: ^28}".format("EQUIPPED")]), (dimensions["character"]["y"] + 14), x=(dimensions["character"]["x"] + 1), colour=Screen.COLOUR_BLACK, bg=Screen.COLOUR_CYAN, speed=0, transparent=False),
            Print(screen, StaticRenderer(images=["{0: ^28}".format("INVENTORY")]), (dimensions["character"]["y"] + 20), x=(dimensions["character"]["x"] + 1), colour=Screen.COLOUR_BLACK, bg=Screen.COLOUR_WHITE, speed=0, transparent=False),
            Print(screen, StaticRenderer(images=["{0: >5} {1: <14} {2: <7}".format("#", "ITEM", "EFFECT")]), (dimensions["character"]["y"] + 21), x=(dimensions["character"]["x"] + 1), colour=Screen.COLOUR_BLACK, bg=Screen.COLOUR_WHITE, speed=0, transparent=False),
            Print(screen, StaticRenderer(images=[" ITEMS ON GROUND "]), dimensions["ground_info"]["y"], x=(dimensions["ground_info"]["x"] + 2), speed=0, transparent=False),
            Print(screen, StaticRenderer(images=[" AVAILABLE EXITS "]), dimensions["ground_info"]["y"], x=(dimensions["ground_info"]["x"] + 60), speed=0, transparent=False)
        ], -1) ],
        unhandled_input=get_player_command,
        allow_int=True)

    if player_command != None:
        screen.print_at("You entered {0} into the field".format(player_command), (dimensions["events"]["x"] + 1), (dimensions["events"]["y"] + 1))
        screen.refresh()

if __name__ == "__main__":
    while True:
        Screen.wrapper(main)

@peterbrittain
Copy link
Owner

peterbrittain commented Jan 31, 2022

Going through your points/questions...

  1. Yes - get_event() returns one event (or None). You need to loop through many to get some string input.
  2. Yes - Text really does all that for you.
  3. Key code definitions are explained in https://asciimatics.readthedocs.io/en/stable/io.html#keyboardevent
  4. In retrospect Frame may not have been the best choice (thanks to the use of animation frame elsewhere), but it fits with the use of Canvas (which is my equivalent of a curses subwin)... The Frame (with widgets) + Canvas is basically equivalent to an application window on a desktop GUI.
  5. You're very nearly there with your sample code! You have actually hit an interesting interaction that I had forgotten... When you start using multiple Frames, they coordinate like a desktop and so swallow keys that they don't understand. There is no unhandled input as such and so you won't hit that code. To act on enter, you need to put that logic somewhere in your active Frame, or reduce this down to just one Frame.
  6. I wouldn't recommend adding efffects to your Frame... That has always been experimental and is problematic in various ways. I expect I'll remove it when I go to 2.0.

@peterbrittain
Copy link
Owner

peterbrittain commented Jan 31, 2022

You might also see undesirable things as you click the mouse around the Screen - e.g. losing focus on your input Frame, bringing your other Frames in front of your printed Effects.

Net is that I would recommend your UI design to treat each Frame as if it was a separate Window in a GUI desktop app. If that doesn't give you the sort of input and navigation you need, I suspect you should consider a custom Effect to handle your game output and overlay a single Frame over the top for input handling. This is similar to what I did for the ray casting demo, so that should give you some pointers on how to create your new Effect.

@peterbrittain
Copy link
Owner

The MiniMap class is a nice simple example of how to create your new Effect. You could go very modular and create your own custom display logic for each one (using a Canvas for each so that they are truly independent UI elements), or push it all into one Effect that uses a single Canvas (or just direct to the Screen if so desired).

@peterbrittain
Copy link
Owner

If you have other questions, why not carry on on in gitter (https://gitter.im/asciimatics/Lobby)?

@sk33z3r
Copy link
Author

sk33z3r commented Jan 31, 2022

Thanks! I have a good bit to mull over here. I may join the chat at some point after I digest.

I treat my UI sections as separate windows in curses, now, so that's definitely the approach I want to continue with.

I wouldn't recommend adding efffects to your Frame... That has always been experimental and is problematic in various ways. I expect I'll remove it when I go to 2.0.

What do you recommend for printing static items, like headers and such? My sample code is applying the Print effects to the Scene at the moment. Perhaps digging into the MiniMap and Ray Casting demos will answer this question.

@peterbrittain
Copy link
Owner

The problem with (or great benefit of) Frames is that they try to act like Windows (in the GUI sense), so each is aware of its position in the Z order and whether it has the focus or not. They will coordinate with each other in ways that you might not intend - e.g. moving input focus away from your text line.

If your current curses program has one global input loop and lots of subwindows (in the curses sense), you may well be better off using multiple Canvases (inside one or more Effects) and just one Frame. That way your input focus will always be in the one Frame and it will handle events as expected. It also gives you a single location to do any custom input handling for your game. I had to use my Scene to do that in the ray casting demo... which is another way to do it, but may be getting over complicated for your UI.

For static content inside a Frame, see https://asciimatics.readthedocs.io/en/stable/widgets.html#disabling-widgets. If you are using Effects, layering of Print Effects is fine, though may be overcomplicating it. It's possible you just want a new custom Effect to draw your new content directly. The MiniMap class in the ray caster demo should show you what I mean...

@peterbrittain
Copy link
Owner

Just going through the questions here to see if docs need updating...

Is there anything else that would have helped you get up to speed faster on how to use asciimatics?

@sk33z3r
Copy link
Author

sk33z3r commented Feb 8, 2022

Hey Peter, sorry for the silence. I got deep into a few things and got side-tracked.

I think your bullet points sum it up pretty well.

The only thing I can think of that may have really helped (besides me reading a few more times) is adding one additional sample that uses multiple frames onscreen at the same time, or at least explaining what you mentioned about gobbling keys. Especially the bit about "no unhandled input" in this scenario:

When you start using multiple Frames, they coordinate like a desktop and so swallow keys that they don't understand. There is no unhandled input as such and so you won't hit that code. To act on enter, you need to put that logic somewhere in your active Frame, or reduce this down to just one Frame.

You can go ahead and close this if you'd like. Thanks for your help so far, I'm sure once I'm able to clear my head and try asciimatics again I'll be in gitter asking questions. Cheers!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants