ocornut / imgui

Dear ImGui: Bloat-free Graphical User interface for C++ with minimal dependencies
MIT License
57.29k stars 9.87k forks source link

Keyboard / Controller Support? #323

Closed velvitonator closed 7 years ago

velvitonator commented 8 years ago

(ADMIN EDIT July 2016: see https://github.com/ocornut/imgui/issues/323#issuecomment-233785300 for the current todo list / ongoing tasks)

I've been looking to make our ImGUI accept controller input, for use on consoles. Something like hitting d-pad down to select the next item down, etc.

From what I could tell, however, the only way to do such a thing would be to spoof mouse control. Is this the case? If so, is there a way to ask for the rectangle of a given item outside of the update loop?

I also couldn't find any way to programmatically set the focus of an item other than input text fields; did I miss something?

ocornut commented 8 years ago

Before anything else, have you looked into using a system like Synergy + usynergy.c to have your game console use your PC mouse/keyboard ? I'd wholly recommend that.

If so, is there a way to ask for the rectangle of a given item outside of the update loop?

Not sure what you mean by "the update loop" nor why you would need that if you are spoofing mouse control? If you are going to spoof mouse control just make your controller adjust the MousePos and MouseDown (buttons) values.

You can call GetItemRectMin() / GetItemRectMax() to obtain the bounding rectangle of the last item (which might be clipped and outside of view, testable with IsItemVisible()) but I don't see how that would help here.

I also couldn't find any way to programmatically set the focus of an item other than input text fields; did I miss something?

No it's not available yet, there's no generalized concept of focus in fact.

Now if you really want controller support. There's different way we can approach this:

I have rename your topic to keyboard/controller because the problems are the same for keyboard controls and many people are likely to come from the angle of desiring keyboard control.

velvitonator commented 8 years ago

Before anything else, have you looked into using a system like Synergy + usynergy.c to have your game console use your PC mouse/keyboard ? I'd wholly recommend that.

Yeah, we've got this up and running (except for keyboard support, since I've yet to hook up the manual character-input stuff). We want to add controller as a good native option, so users don't have to switch input devices.

Not sure what you mean by "the update loop" nor why you would need that if you are spoofing mouse control? If you are going to spoof mouse control just make your controller adjust the MousePos and MouseDown (buttons) values.

Apologies; I meant that we could do something like move the mouse down "an item" instead, but that starts to get a bit complicated for client code, especially since we'd need this to be something for everyone to use, to make their own debug GUIs. With that approach, we'd need the screen-rect of the options etc. This is all certainly possible for us to do, it's just a matter of the effort we want to expend on it. Hence, I'm hoping to clarify the available options before embarking on something time-consuming. =)

Emulating mouse with game controller (or keyboard) would be easy to implement and functional immediately. Obviously it'd be rather awkward and underwhelming interaction-wise, but for casual use it may be enough.

Yeah, this is looking to be the only option in the timeframe we're looking at. Since we also have Synergy, I'm not convinced adding this is worth it; I know I'd easily switch to mouse + keys as soon as I had to, rather than bear analog-stick mouse cursor!

If you only want this for simple sort of UI like list of items you can emulate the concept of "whats focused" on user side and handle interaction yourself (e.g. Button X pressed while Item N is selected).

Certainly! But getting it to reflect in the UI is the tricky bit =/ Also, we already have a couple of in-game debug UIs using ImGUI, and they have quite a bit of variety. =P

The correct thing would be to fully handle keyboard/controller navigation and per-widget focus. It's not a simple feature, probably a bunch of work to get it right (I'd say 1 week optimistically). It is a very desirable feature but I don't know when / if I'll ever be able to do it.

Hmm, I'll be honest: I'm so accustomed to working with closed-source things that simply adding the feature to ImGUI didn't occur to me. I think it's a little out of scope for the timeframe I have, but it's certainly worth considering in the longer term, should we find that controller support is indispensable for whatever reason.

I have rename your topic to keyboard/controller because the problems are the same for keyboard controls and many people are likely to come from the angle of desiring keyboard control.

Good idea!

ocornut commented 8 years ago

Apologies; I meant that we could do something like move the mouse down "an item" instead, but that starts to get a bit complicated for client code, especially since we'd need this to be something for everyone to use,

It's trivial to store the rectangle of the hovered item (we can add that to the library), so you could move the cursor outside of it but you wouldn't know by how much as you don't know where the next item. An hybrid approach to move outward the item by the amount of its width/height and then move smoothly if there's no item under the cursor. It would be easy to implement and may be a little better than a cursor but probably still mediocre. If you are on PS4, I haven't tried it but I suspect the touch pad is precise enough to emulate a mouse.

Hmm, I'll be honest: I'm so accustomed to working with closed-source things that simply adding the feature to ImGUI didn't occur to me. I think it's a little out of scope for the timeframe I have, but it's certainly worth considering in the longer term, should we find that controller support is indispensable for whatever reason.

FYI if your project/company has the fund I'm more than happy to provide custom work on imgui to develop that sort of non-trivial feature (been looking for possible ways to fund its development as I can't really afford it any more myself, at least not at the pace I was working on it a few months ago). Something to consider, may be more optimal than you doing it and it'll benefit the sustainability of the library.

It's one of the those feature that's touching quite a lot of elements in the code - quite a complex feature to do right. If you to want to tackle it yourself I can also guide you.

velvitonator commented 8 years ago

Sorry for the silence; I forgot the re-mark the mail as unread to get back to it.

Your idea to use the PS4 touchpad was pretty effective--it's somewhat finicky, but much better than a stick, and it wasn't too difficult to hook up the sticks and pad for scrolling, either.

So, we're good for now; thanks for the suggestion!

ocornut commented 8 years ago

I don't have an ETA yet but I've been working on this lately. When it starts to be semi usable I will push it to a public branch, right now it is way too prototypey.

navigation_v0b

ocornut commented 8 years ago

Some progress report, been improving support for the popup stack, combos, menus, sliders, visualizing selection, avoiding confusion between keyboard and mouse, etc. There's still a billion things to do, it is a little overwhelming.

navigation_v1

Pagghiu commented 8 years ago

This is VERY interesting :grin:

MrMarkie commented 7 years ago

This looks great. We'd like to use it for interaction with debug UI in PSVR. It's exactly the kind of thing we need for this. When are you looking to make it available?

ocornut commented 7 years ago

When it is done @MrMarkie :) I've been working on my non-existent spare time until now, but because this feature is now sponsored (a first) I'm aiming before end of July. Will probably start testing and iterating on API and edge cases mid-july.

MrMarkie commented 7 years ago

Hi. Is there any news on this feature? I'd be happy to help test it if you have a preview version

ocornut commented 7 years ago

I was thinking about pushing a branch but the IO api is utterly temporary/broken yet and it's still missing important features. I'll work on it this week-end. From Monday if you're happy with experimenting with it (at the cost of minor breakage in the following weeks) it would be indeed quite helpful if you want to try it and submit feedback. You can also e-mail me if you want to move this off-line for back and forth.

MrMarkie commented 7 years ago

Absolutely. Let me know when / where I can get it. Look forwards to trying it out. Markie On 15 Jul 2016 17:35, omar notifications@github.com wrote:I was thinking about pushing a branch but the IO api is utterly temporary/broken yet and it's still missing important features. I'll work on it this week-end. From Monday if you're happy with experimenting with it (at the cost of minor breakage in the following weeks) it would be indeed quite helpful if you want to try it and submit feedback. You can also e-mail me if you want to move this off-line for back and forth.

—You are receiving this because you were mentioned.Reply to this email directly, view it on GitHub, or mute the thread.

ocornut commented 7 years ago

Quick update GIF navigation_v2

I'm not finished but I'll be pushing first test version to a branch tonight, along with instructions for those interested.

MrMarkie commented 7 years ago

This looks excellent!! On 19 Jul 2016 22:28, omar notifications@github.com wrote:Quick update GIF

I'm not finished but I'll be pushing first test version to a branch tonight, along with instructions for those interested.

—You are receiving this because you were mentioned.Reply to this email directly, view it on GitHub, or mute the thread.

ocornut commented 7 years ago

Hello,

I have started to push gamepad/keyboard controls this into a work-in-progress branch: https://github.com/ocornut/imgui/tree/navigation

navigation_v2

Brain dump, some of it may be hard to digest :)

It's been a big chunk of work and it isn't finished (probably 50+ changes left to apply, bug fixes and tweaks), BUT i think it is ready for early evaluation. If you are willing to help and checkout this branch it would be super helpful.

If you don't have time to look it and help, come back in 2-4 weeks and it'll be in a better state.

I will keep working on this mostly in the upcoming week-ends. Even if incomplete it could be merged in master in a few weeks if everything works out nicely, and then I can keep improving/fixing afterwards. Any test with testing would obviously accelerate merging into master.

The development of this feature has been sponsored by Insomniac Games (thank you!).

Note that I didn't yet add Moving or Collapsing windows yet. The code for those is now trivial to add but the key question is how to decide on an input layout and how to expose it to the end-user so it is decently configurable. Things like ALT-TAB window focus alteration requires by design holding one button while pressing another (because of the MRU logic) so I don't yet know how to suitably expose all those inputs. Currently the L/R triggers are mapped on ImGuiKey_NavTweakSlower ImGuiKey_NavTweakFaster but that's also used to change focus. So while for a gamepad ImGuiKey_NavTweakSlower==ImGuiKey_NavPrevWindow and ImGuiKey_NavTweakFaster==ImGuiKey_NavNextWindow, on a keyboard we are likely to want to use Alt/Shift for Slower/Faster and CTRL+TAB/CTRL+SHIFT+TAB, etc. So there's work to rearrange those.

I expect we can also use analog stick and that will also weight in the design to pass all those inputs.

In ImGui_ImplGlfw_NewFrame()

    io.NavMovesMouse = true;

    // Setup events/key mapping within the existing keyboard array.
    int avail_key = GLFW_KEY_LAST;
    io.KeyMap[ImGuiKey_NavActivate]     = avail_key++;
    io.KeyMap[ImGuiKey_NavCancel]       = avail_key++;
    io.KeyMap[ImGuiKey_NavWindowing]    = avail_key++;
    io.KeyMap[ImGuiKey_NavInput]        = avail_key++;
    io.KeyMap[ImGuiKey_NavLeft]         = avail_key++;
    io.KeyMap[ImGuiKey_NavRight]        = avail_key++;
    io.KeyMap[ImGuiKey_NavUp]           = avail_key++;
    io.KeyMap[ImGuiKey_NavDown]         = avail_key++;
    io.KeyMap[ImGuiKey_NavTweakFaster]  = avail_key++;
    io.KeyMap[ImGuiKey_NavTweakSlower]  = avail_key++;

    // Update Keyboard Nav
    const bool TEST_KEYBOARD = false;
    io.KeysDown[io.KeyMap[ImGuiKey_NavActivate]]    = TEST_KEYBOARD && io.KeysDown[GLFW_KEY_SPACE];
    io.KeysDown[io.KeyMap[ImGuiKey_NavCancel]]      = TEST_KEYBOARD && io.KeysDown[GLFW_KEY_ESCAPE];
    io.KeysDown[io.KeyMap[ImGuiKey_NavWindowing]]   = TEST_KEYBOARD && false;
    io.KeysDown[io.KeyMap[ImGuiKey_NavInput]]       = TEST_KEYBOARD && io.KeysDown[GLFW_KEY_ENTER];
    io.KeysDown[io.KeyMap[ImGuiKey_NavLeft]]        = TEST_KEYBOARD && io.KeysDown[GLFW_KEY_LEFT];
    io.KeysDown[io.KeyMap[ImGuiKey_NavRight]]       = TEST_KEYBOARD && io.KeysDown[GLFW_KEY_RIGHT];
    io.KeysDown[io.KeyMap[ImGuiKey_NavUp]]          = TEST_KEYBOARD && io.KeysDown[GLFW_KEY_UP];
    io.KeysDown[io.KeyMap[ImGuiKey_NavDown]]        = TEST_KEYBOARD && io.KeysDown[GLFW_KEY_DOWN];
    io.KeysDown[io.KeyMap[ImGuiKey_NavTweakFaster]] = TEST_KEYBOARD && io.KeyShift;
    io.KeysDown[io.KeyMap[ImGuiKey_NavTweakSlower]] = TEST_KEYBOARD && io.KeyAlt;

    // Update Joystick Nav
    if (glfwJoystickPresent(GLFW_JOYSTICK_1))
    {
        int buttons_count = 0;
        const unsigned char* buttons = glfwGetJoystickButtons(GLFW_JOYSTICK_1, &buttons_count);
        if (buttons_count > 0  && buttons[0]  == GLFW_PRESS) io.KeysDown[io.KeyMap[ImGuiKey_NavActivate]]= true;
        if (buttons_count > 1  && buttons[1]  == GLFW_PRESS) io.KeysDown[io.KeyMap[ImGuiKey_NavCancel]]  = true;
        if (buttons_count > 2  && buttons[2]  == GLFW_PRESS) io.KeysDown[io.KeyMap[ImGuiKey_NavWindowing]]  = true;
        if (buttons_count > 3  && buttons[3]  == GLFW_PRESS) io.KeysDown[io.KeyMap[ImGuiKey_NavInput]] = true;
        if (buttons_count > 10 && buttons[10] == GLFW_PRESS) io.KeysDown[io.KeyMap[ImGuiKey_NavUp]]      = true;
        if (buttons_count > 11 && buttons[11] == GLFW_PRESS) io.KeysDown[io.KeyMap[ImGuiKey_NavRight]]   = true;
        if (buttons_count > 12 && buttons[12] == GLFW_PRESS) io.KeysDown[io.KeyMap[ImGuiKey_NavDown]]    = true;
        if (buttons_count > 13 && buttons[13] == GLFW_PRESS) io.KeysDown[io.KeyMap[ImGuiKey_NavLeft]]    = true;
        if (buttons_count > 4  && buttons[4]  == GLFW_PRESS) io.KeysDown[io.KeyMap[ImGuiKey_NavTweakSlower]] = true;
        if (buttons_count > 5  && buttons[5]  == GLFW_PRESS) io.KeysDown[io.KeyMap[ImGuiKey_NavTweakFaster]] = true;
    }

Also in ImGui_ImplGlfw_NewFrame(), honor io.WantMoveMouse request:

    // Setup inputs
    // (we already got mouse wheel, keyboard keys & characters from glfw callbacks polled in glfwPollEvents())
    if (glfwGetWindowAttrib(g_Window, GLFW_FOCUSED))
    {
        if (io.WantMoveMouse)
        {
            // Requested to move mouse (when using directional navigation with 'NavMovesMouse=true'). Intentionally applied on following frame to compensate for render lag.
            glfwSetCursorPos(g_Window, (double)io.MousePos.x, (double)io.MousePos.y);
        }
        else
        {
            double mouse_x, mouse_y;
            glfwGetCursorPos(g_Window, &mouse_x, &mouse_y);
            io.MousePos = ImVec2((float)mouse_x, (float)mouse_y);   // Mouse position in screen coordinates (set to -1,-1 if no mouse / on another screen, etc.)
        }
    }
    else
    {
        io.MousePos = ImVec2(-1,-1);
    }

If you can spend an hour setting it up and reporting confusion or issues it'll be helpful!

Thanks!

Extra musing.. Unfortunately some of the code to support that feature has become rather fragile, not in the sense that it is crippled with bugs (hopefully it isn't), but many of those changes requires too much expertise and knowledge of the codebase, which isn't a good thing at all. The testing suite and documentation will need to take more priority in the future and I above everything want to steer this project away from ever being a bus-factor-1 project. I will return to this topic later and looking forward to gather any help I can to make the testing suite happen! (#435).

ocornut commented 7 years ago

Some of my todo list.

The big commit already includes fixes/changes for probably a hundred different things, so the left-over may be quite easy :) As long as we focus on gamepad this is fairly scoped and we can see the end of the tunnel (full keyboard friendlyness is another thing).

unpacklo commented 7 years ago

I haven't integrated any of this into my own code, just looking at your branch directly, but it seems like setup will be very simple.

The only hiccup I had was I had to install DS4Windows to get the controller inputs to be mapped correctly for your setup. I guess Sony hasn't provided any official drivers for the DS4... shame! However, I noticed that the touch pad works and I can click/move windows with it, but it looks like its a DS4Windows feature since I can move the mouse on my entire system and click things with it.

Navigation with the controller produces the expected results for most things, but the one area I've noticed which can yield very poor navigation is switching windows. I suspect it's because you may be cycling through windows somehow in memory? But most of the time I'm trying to cycle through them in some spatial fashion on screen. This may be exacerbated by the fact that I was moving the windows around with the mouse in between.

Will be a little while before I have more detailed feedback, but this is very promising!

michaelbartnett commented 7 years ago

Easy to get going, I liked the sub-selection in the style color editor. Something that felt like it was missing was a way to jump to the parent in a tree widget. That's tricky though, since I'd normally expect NavLeft to do that, which doesn't necessarily make sense if the children of that tree node have horizontal elements. I'm not doing very complex things with imgui at the moment, so I'll keep my comment brief.

Here's a quick hack for people wishing to test it out with an Xbox 360 controller on Mac:

    if (glfwJoystickPresent(GLFW_JOYSTICK_1))
    {
        struct GamepadMap
        {
            enum {
                DPAD_UP, DPAD_DOWN, DPAD_LEFT, DPAD_RIGHT,
                FACE_UP, FACE_DOWN, FACE_LEFT, FACE_RIGHT,
                SHOULDER_LEFT, SHOULDER_RIGHT,
                LAST
            };

            unsigned char map[LAST];

            bool checkpress(const unsigned char* buttons, int buttons_count, int btn)  { return buttons_count > btn && buttons[map[btn]] == GLFW_PRESS; }
        };

        static GamepadMap ds4win_map = {{10, 12, 13, 11, 3, 0, 2, 1, 4, 5}};
        static GamepadMap x360macos_map = {{0, 1, 2, 3, 14, 11, 13, 12, 8, 9}};
        // GamepadMap &padmap = ds4win_map;
        GamepadMap &padmap = x360macos_map;

        int buttons_count = 0;
        const unsigned char* buttons = glfwGetJoystickButtons(GLFW_JOYSTICK_1, &buttons_count);

        if (padmap.checkpress(buttons, buttons_count, GamepadMap::FACE_DOWN))      io.KeysDown[io.KeyMap[ImGuiKey_NavActivate]]= true;
        if (padmap.checkpress(buttons, buttons_count, GamepadMap::FACE_RIGHT))     io.KeysDown[io.KeyMap[ImGuiKey_NavCancel]]  = true;
        if (padmap.checkpress(buttons, buttons_count, GamepadMap::FACE_LEFT))      io.KeysDown[io.KeyMap[ImGuiKey_NavWindowing]]  = true;
        if (padmap.checkpress(buttons, buttons_count, GamepadMap::FACE_UP))        io.KeysDown[io.KeyMap[ImGuiKey_NavInput]] = true;
        if (padmap.checkpress(buttons, buttons_count, GamepadMap::DPAD_UP))        io.KeysDown[io.KeyMap[ImGuiKey_NavUp]]      = true;
        if (padmap.checkpress(buttons, buttons_count, GamepadMap::DPAD_RIGHT))     io.KeysDown[io.KeyMap[ImGuiKey_NavRight]]   = true;
        if (padmap.checkpress(buttons, buttons_count, GamepadMap::DPAD_DOWN))      io.KeysDown[io.KeyMap[ImGuiKey_NavDown]]    = true;
        if (padmap.checkpress(buttons, buttons_count, GamepadMap::DPAD_LEFT))      io.KeysDown[io.KeyMap[ImGuiKey_NavLeft]]    = true;
        if (padmap.checkpress(buttons, buttons_count, GamepadMap::SHOULDER_LEFT))  io.KeysDown[io.KeyMap[ImGuiKey_NavTweakSlower]] = true;
        if (padmap.checkpress(buttons, buttons_count, GamepadMap::SHOULDER_RIGHT)) io.KeysDown[io.KeyMap[ImGuiKey_NavTweakFaster]] = true;
    }

[EDIT] NavLeft not NavRight >_<

ocornut commented 7 years ago

@Roflraging Yeah, DS4window provide mouse emulation. I expect native PS4 developers to implement a similar setup to emulate a fallback mouse, even awkwardly.

Navigation with the controller produces the expected results for most things, but the one area I've noticed which can yield very poor navigation is switching windows. I suspect it's because you may be cycling through windows somehow in memory? But most of the time I'm trying to cycle through them in some spatial fashion on screen. This may be exacerbated by the fact that I was moving the windows around with the mouse in between.

Switching windows is indeed a previous/next thing based on the current z-order (~most recently used window). It currently works this way because it compares to Windows' ALT-TAB and because spatial navigation wouldn't work very well with lots of overlapping windows. It would work better if we could show a preview of the windows arranged in a linear fashion, the same way Mac/Windows does it, but that seems like lots of extra work and the contents of imgui windows aren't really fit for easy identification after resizing down. If you have any idea on how to improve this let me know. I don't think it is a big issue however.

Also thanks @michaelbartnett. Eventually we should provide similar code in the committed example that has some sort of nice remapping table, even if the code itself ends up being commented out.

I've got a bunch of e-mail feedback from @pdoane which he may copy here later.

pdoane commented 7 years ago

I did an integration into one of my test projects - mostly just a property grid and a couple of modal dialog boxes. My interest is in keyboard navigation which as you say is going to be more complicated, but even the controller support is already a good foundation.

Integration was easy. I followed your GLFW sample and was up and running in a few minutes. No major issues and a lot of the navigation already felt natural.

Menus:

Modal Dialogs:

Text input:

Multi-column navigation:

Drag controls:

List box:

Tree Nodes:

Navigation override:

unpacklo commented 7 years ago

It currently works this way because it compares to Windows' ALT-TAB and because spatial navigation wouldn't work very well with lots of overlapping windows.

So I think part of the reason why I have the spatial expectation at all is because of the controller bindings. Having it cycle through with left + right bumpers give me some sort of expectation that I should be cycling left/right on the screen. I'll have to try with different binds to see if it makes it better for me, but I suspect that the preview of the cycling order as in alt+tab makes the most difference.

I do tend to agree that this is not a huge issue, I personally don't have more than 2 or 3 windows at any time, if that.

ocornut commented 7 years ago

@pdoane:

Escape closed the dialog box. Good! Enter didn't didn't accept the dialog. I don't remember a flag for it but I'm assuming we'll need to semantically identify the Ok button. I want control over the initial focus. There's a text input field that would be the natural thing to be active on opening of the dialog.

There's a new function SetItemDefaultFocus() for that. So you could use that on the OK button as well if you are happy with just regularly "activating" it (mapped to "Space" in my example) vs a window "global" Enter to identify OK button. Perhaps doable as an extra function let's say SetItemDefaultValidate() which would record the ID and that can be activated from anywhere in the window with Enter?

TextInput: After I've moved the highlight over a text input field, I'm expecting that I could start typing. Or at least to activate it and start typing.

Right now you can type in by pressing NavInput (Enter) which works on sliders, drags.. we could make NavActivate function on TextInput as well, can't tell if that would feel more or less consistent?

Drag controls: Having to hold the activate key while pressing left/right feels awkward.

I will experiment with keeping the control activated until pressing NavCancel but I think the current scheme allows for faster interactions.

Tree nodes: I am expecting Left/Right to open/close the node. Not sure how that would interact with a multi-column environment though.

Yes need to look at columns before we can try that on trees. EDIT Problem is also that one can have items next to a closed tree node (I often do that myself).

and then press the down key to transfer into navigating within the list box. Maybe that is the appropriate behavior for single-line text input?

Will try.

ocornut commented 7 years ago

@pdoane

Multi-colum navigation Up/Down isn't staying in the same column. This is just a two column setup, pressing up ends up on the > right column and down on the left column.

Please generally provide screenshots or repro because that's a general navigation issue and may not be tied to columns. It works for example in the simple columns part of the demo, but acts weirdly as you described in the "Property Example" sample. In that case it is related to the Selectable() on the left incorrectly sticking out too far and the navigation scoring calculation not applying clipping over the item rectangle.

untitled

ocornut commented 7 years ago

@pdoane I have pushed some fixes, it would be nice if you could confirm if you columns issues are resolved or improved.

ocornut commented 7 years ago

API BREAKING CHANGES

Pushed various changes above. Most notable, the NavMenu button (recommended "Square" on a DualShock4 controller) allows toggling between the scrollable area of a window and the menubar. Holding the NavMenu button still allows resizing and selecting window focus.

I will add moving and collapsing window once I figure out the input scheme for that and the menu/layer system was a necessary step forward. May adopt Windows style NavMenu+left to access a window menu to select among those options, which also would be keyboard compatible (rather than taking advantage of multiple sticks).

unpacklo commented 7 years ago

Finally got around to checking out these controller changes in a non-trivial imgui setup.

Some thoughts:

I would write more, but it is very late for me, I'll try to get more thoughts tomorrow. Biggest thing I've realized is that having a GUI already set up with the mouse + keyboard expectation doesn't really work with the controller. It's workable, but not ideal! Many of the issues I currently have is much more of a problem with the GUI design fundamentally not matching the input method rather than legitimate issues with your implementation.

ocornut commented 7 years ago

@Roflraging

Will there be support for analogous middle mouse and right mouse action for the controller?

You can always map mouse buttons to free buttons of your controller (if there are any?), which may or not cover you enough if you have io.NavMovesMouse activated and your binding is honoring io.WantMoveMouse.

Fast scrolling/next item? Some of the scrolling areas we have are very large, and to cycle through the items at the default rate can be tedious. Obviously, a controller based GUI would probably rely on large menus less, but GUIs already set up, this might be very convenient.

"Faster next item" is a little cumbersome to get right, but I agree we need manual scrolling in (there's already support for scrolling but only when there's no item to interact with). The question is how to return back from "scrolling" to "selecting a widget" and how is said widget selected (perhaps based on relative widget position of where we were before scrolling?)

Currently in check-list above as - [ ] B. Explicit manual scrolling (e..g mapped on a analog stick): how to reapply suitable focus? currently only possible when no item is navigable.

Support to pop up to top level child.

There's different issues and possible solution here. If you can post screenshots (or e-mail privately, I have a NDA with your company) I could look at the specific case. If they are childs with no visible scrolling I think we could adopt a scheme where we can cross through parent-child borders. It is also possible we need extra window flags or options to parametrize those behaviors as well.

ocornut commented 7 years ago

Still uncertain about how to express/configure inputs, it appears too difficult to treat gamepad and keyboard as a same source and gets in the way of lots of possible improvements

Instead of attempting to expose semantic-only I may bite the bullet and expose separate explicit sets of inputs for gamepad and keyboard, and we can optimize input scheme specifically for those (with first focus on gamepad).

For now I have added 4 new digital inputs currently mapped to left-analog stick on my dual shock:

ImGuiKey_NavScrollLeft, // e.g. Analog left
ImGuiKey_NavScrollRight,// e.g. Analog right
ImGuiKey_NavScrollUp,   // e.g. Analog up
ImGuiKey_NavScrollDown, // e.g. Analog down

Used to:

With those changes, apart from the big questions mark of how to expose inputs, it is starting to be quite usable with a gamepad. There's probably hundreds of upcoming incremental changes obviously, but you can do stuff without a keyboard/mouse around.

Reminder: todo list is here https://github.com/ocornut/imgui/issues/323#issuecomment-233785300

The ugly binding for GLFW+DualShock4 with DS4Window is: We shall make that less ugly, probably using a dedicated input visualization/config window. Currently under "Keyboard,Mouse,etc." in the demo window there's a panel that shows all pressed inputs.

// FIXME-NAVIGATION
// Setup events/key mapping within the existing keyboard array.
int avail_key = GLFW_KEY_LAST;
for (int n = ImGuiKey_NavActivate; n < ImGuiKey_NavLast_; n++)
{
    io.KeyMap[n] = avail_key++;
    io.KeysDown[io.KeyMap[n]] = false;
}

// Update Keyboard Inputs
if (1)
{
    io.KeysDown[io.KeyMap[ImGuiKey_NavActivate]]    = io.KeysDown[GLFW_KEY_SPACE];
    io.KeysDown[io.KeyMap[ImGuiKey_NavCancel]]      = io.KeysDown[GLFW_KEY_ESCAPE];
    io.KeysDown[io.KeyMap[ImGuiKey_NavInput]]       = io.KeysDown[GLFW_KEY_ENTER];
    io.KeysDown[io.KeyMap[ImGuiKey_NavLeft]]        = io.KeysDown[GLFW_KEY_LEFT];
    io.KeysDown[io.KeyMap[ImGuiKey_NavRight]]       = io.KeysDown[GLFW_KEY_RIGHT];
    io.KeysDown[io.KeyMap[ImGuiKey_NavUp]]          = io.KeysDown[GLFW_KEY_UP];
    io.KeysDown[io.KeyMap[ImGuiKey_NavDown]]        = io.KeysDown[GLFW_KEY_DOWN];
    io.KeysDown[io.KeyMap[ImGuiKey_NavTweakFaster]] = io.KeyShift;
    io.KeysDown[io.KeyMap[ImGuiKey_NavTweakSlower]] = io.KeyAlt;
}

// Update Joystick Inputs
int axes_count = 0;
const float* axes = glfwGetJoystickAxes(GLFW_JOYSTICK_1, &axes_count);
if (glfwJoystickPresent(GLFW_JOYSTICK_1))
{
    int buttons_count = 0;
    const unsigned char* buttons = glfwGetJoystickButtons(GLFW_JOYSTICK_1, &buttons_count);
    if (buttons_count > 0  && buttons[0]  == GLFW_PRESS) io.KeysDown[io.KeyMap[ImGuiKey_NavActivate]]= true;
    if (buttons_count > 1  && buttons[1]  == GLFW_PRESS) io.KeysDown[io.KeyMap[ImGuiKey_NavCancel]]  = true;
    if (buttons_count > 2  && buttons[2]  == GLFW_PRESS) io.KeysDown[io.KeyMap[ImGuiKey_NavMenu]]    = true;
    if (buttons_count > 3  && buttons[3]  == GLFW_PRESS) io.KeysDown[io.KeyMap[ImGuiKey_NavInput]]   = true;
    if (buttons_count > 10 && buttons[10] == GLFW_PRESS) io.KeysDown[io.KeyMap[ImGuiKey_NavUp]]      = true;
    if (buttons_count > 11 && buttons[11] == GLFW_PRESS) io.KeysDown[io.KeyMap[ImGuiKey_NavRight]]   = true;
    if (buttons_count > 12 && buttons[12] == GLFW_PRESS) io.KeysDown[io.KeyMap[ImGuiKey_NavDown]]    = true;
    if (buttons_count > 13 && buttons[13] == GLFW_PRESS) io.KeysDown[io.KeyMap[ImGuiKey_NavLeft]]    = true;
    if (buttons_count > 4  && buttons[4]  == GLFW_PRESS) io.KeysDown[io.KeyMap[ImGuiKey_NavTweakSlower]] = true;
    if (buttons_count > 5  && buttons[5]  == GLFW_PRESS) io.KeysDown[io.KeyMap[ImGuiKey_NavTweakFaster]] = true;

    if (axes_count > 0 && axes[0] < -0.5f) io.KeysDown[io.KeyMap[ImGuiKey_NavScrollLeft]]  = true;
    if (axes_count > 0 && axes[0] > +0.5f) io.KeysDown[io.KeyMap[ImGuiKey_NavScrollRight]] = true;
    if (axes_count > 1 && axes[1] < -0.5f) io.KeysDown[io.KeyMap[ImGuiKey_NavScrollDown]]  = true;
    if (axes_count > 1 && axes[1] > +0.5f) io.KeysDown[io.KeyMap[ImGuiKey_NavScrollUp]]    = true;
}
ocornut commented 7 years ago

FYI I have been working on how to pass inputs, including analog inputs, in a way that would work for both gamepad and future keyboard. That'll probably be the last major change before the thing can be marked "usable beta".

ocornut commented 7 years ago

Ok I made a first-pass breaking commit. EDIT typos, fixes

Here's the enum

enum ImGuiNavInput_
{
    ImGuiNavInput_PadActivate,      // press button, tweak value                    // e.g. Circle button
    ImGuiNavInput_PadCancel,        // close menu/popup/child, lose selection       // e.g. Cross button
    ImGuiNavInput_PadInput,         // text input                                   // e.g. Triangle button
    ImGuiNavInput_PadMenu,          // access menu, focus, move, resize             // e.g. Square button
    ImGuiNavInput_PadUp,            // move up, resize window (with PadMenu held)   // e.g. D-pad up/down/left/right
    ImGuiNavInput_PadDown,          // move down
    ImGuiNavInput_PadLeft,          // move left
    ImGuiNavInput_PadRight,         // move right
    ImGuiNavInput_PadScrollUp,      // scroll up, move window (with PadMenu held)   // e.g. right stick up/down/left/right
    ImGuiNavInput_PadScrollDown,    // "
    ImGuiNavInput_PadScrollLeft,    //
    ImGuiNavInput_PadScrollRight,   //
    ImGuiNavInput_PadFocusPrev,     // next window (with PadMenu held)              // e.g. L-trigger
    ImGuiNavInput_PadFocusNext,     // prev window (with PadMenu held)              // e.g. R-trigger
    ImGuiNavInput_PadTweakSlow,     // slower tweaks                                // e.g. L-trigger
    ImGuiNavInput_PadTweakFast,     // faster tweaks                                // e.g. R-trigger

    ImGuiNavInput_COUNT,
};

Here's my current GLFW binding code for DualShock4 with DSWindow:

// Setup directional navigation events/key mapping
bool nav_uses_keyboard = true;
bool nav_uses_gamepad = true;
memset(io.NavInputs, 0, sizeof(io.NavInputs));

// Enable to allow ImGui moving mouse cursor when using keyboard/gamepad navigation
io.NavMovesMouse = false;

// Update Keyboard Inputs
if (nav_uses_keyboard)
{
    #define MAP_KEY(NAV_NO, KEY_NO) { if (io.KeysDown[KEY_NO]) io.NavInputs[NAV_NO] = 1.0f; }
    MAP_KEY(ImGuiNavInput_PadActivate,      GLFW_KEY_SPACE);
    MAP_KEY(ImGuiNavInput_PadCancel,        GLFW_KEY_ESCAPE);
    MAP_KEY(ImGuiNavInput_PadMenu,          GLFW_KEY_LEFT_ALT);
    MAP_KEY(ImGuiNavInput_PadInput,         GLFW_KEY_ENTER);
    MAP_KEY(ImGuiNavInput_PadUp,            GLFW_KEY_UP);
    MAP_KEY(ImGuiNavInput_PadDown,          GLFW_KEY_DOWN);
    MAP_KEY(ImGuiNavInput_PadLeft,          GLFW_KEY_LEFT);
    MAP_KEY(ImGuiNavInput_PadRight,         GLFW_KEY_RIGHT);
    MAP_KEY(ImGuiNavInput_PadTweakSlow,     GLFW_KEY_LEFT_ALT);
    MAP_KEY(ImGuiNavInput_PadTweakSlow,     GLFW_KEY_RIGHT_ALT);
    MAP_KEY(ImGuiNavInput_PadTweakFast,     GLFW_KEY_LEFT_SHIFT);
    MAP_KEY(ImGuiNavInput_PadTweakFast,     GLFW_KEY_RIGHT_SHIFT);
    #undef MAP_KEY
}

// Update Gamepad Inputs
if (nav_uses_gamepad)
{
    #define MAP_BUTTON(NAV_NO, BUTTON_NO)       { if (buttons_count > BUTTON_NO && buttons[BUTTON_NO] == GLFW_PRESS) io.NavInputs[NAV_NO] = 1.0f; }
    #define MAP_ANALOG(NAV_NO, AXIS_NO, V0, V1) { float v = (axes_count > AXIS_NO) ? axes[AXIS_NO] : V0; v = (v - V0) / (V1 - V0); if (v > 1.0f) v = 1.0f; if (io.NavInputs[NAV_NO] < v) io.NavInputs[NAV_NO] = v; }
    int axes_count = 0, buttons_count = 0;
    const float* axes = glfwGetJoystickAxes(GLFW_JOYSTICK_1, &axes_count);
    const unsigned char* buttons = glfwGetJoystickButtons(GLFW_JOYSTICK_1, &buttons_count);
    MAP_BUTTON(ImGuiNavInput_PadActivate,   0);
    MAP_BUTTON(ImGuiNavInput_PadCancel,     1);
    MAP_BUTTON(ImGuiNavInput_PadMenu,       2);
    MAP_BUTTON(ImGuiNavInput_PadInput,      3);
    MAP_BUTTON(ImGuiNavInput_PadUp,         10);
    MAP_BUTTON(ImGuiNavInput_PadDown,       12);
    MAP_BUTTON(ImGuiNavInput_PadLeft,       13);
    MAP_BUTTON(ImGuiNavInput_PadRight,      11);
    MAP_BUTTON(ImGuiNavInput_PadFocusPrev,  4);
    MAP_BUTTON(ImGuiNavInput_PadFocusNext,  5);
    MAP_BUTTON(ImGuiNavInput_PadTweakSlow,  4);
    MAP_BUTTON(ImGuiNavInput_PadTweakFast,  5);
    MAP_ANALOG(ImGuiNavInput_PadScrollUp,   1,  +0.3f,  +0.9f);
    MAP_ANALOG(ImGuiNavInput_PadScrollDown, 1,  -0.3f,  -0.9f);
    MAP_ANALOG(ImGuiNavInput_PadScrollLeft, 0,  -0.3f,  -0.9f);
    MAP_ANALOG(ImGuiNavInput_PadScrollRight,0,  +0.3f,  +0.9f);
    #undef MAP_BUTTON
    #undef MAP_ANALOG
}

This is me toying with a macro based version, but I won't keep that. The naive data driven version didn't encourage custom control flow and I'm pretty sure that beginner users will feel restricted to any provided data structure, whereas I want to encourage people to tweak their input. So I'll may just keep something like the above but with a helper function, the question is wether we can provide a similar structure for all the bindings or not. Even if that code isn't part of imgui itself it's nice to figure out something more elegant.

ocornut commented 7 years ago

whereas I want to encourage people to tweak their input.

In particular, I am looking at mechanism/idioms to make it easy to selectively transfer input from one part of the app (imgui/tools) to another (game) which may vary per peripheral, per platform, etc. Right now the nav_uses_keyboard nav_uses_joystick flags in that demo code are part of the backend but it would be nice to provide a standard interface in imgui to allow app to pass info to the bindings.

ocornut commented 7 years ago

I am stuck on this one, so will be thinking aloud:

[ ] Popup: add options to disable auto-closing popups when using a MenuItem/Selectable

@MrMarkie asked for it under those terms:

"Also, when I select a check option from a sub menu, the sub menu closes. I wonder if it should stay open until you press cancel? It's just that setting a bunch of check boxes one after another could be quite laborious"

This is indeed desirable when using MenuItem that are meant to be toggled/checked. But not desirable for other MenuItem that are meant to be activated, such as a typical "Open.." or "Quit" item.

The problem is that the two MenuItem() APIs don't allow to differentiate one from case another.

bool MenuItem(const char* label, const char* shortcut = NULL, bool selected = false, bool enabled = true);
bool MenuItem(const char* label, const char* shortcut, bool* p_selected, bool enabled = true);

We could fairly assume that the bool* version is always a checkable when the pointer isn't NULL and not a checkable when NULL. But in many situations the bool version can be used (most commonly for code that doesn't use bool for storage or need to access the data via accessors). And then passing 'false' is ambiguous, we can't tell if the item is a checkable or not. Particularly common as if you just want to create an "Open" item you are likely to just either pass false or NULL inconsistently.

A) If we were to disable auto-closing popup solely based on passing a non-NULL pointer to the bool* version that would make both overloads behave inconsistently which isn't so great. It would solve 90% use cases for most people but make the whole thing feel inconsistent :(

B) We could introduce new flags to the function, e.g. MenuItemEx() which could fit the additional information we want (either "can be checked" or "dont close popup"/"close popup"). That seems a little overkill just for this purpose but we can keep this idea brewing. Current API was designed as one of the rare function to take two bools, which is generally not great API design but in this case made a lot of sense as they are very commonly used and we want to make code terse, but that means we can't just add a new flag to an existing flag set. And we are not going to add a third bool while I'm alive. So MenuItemEx(const char* label, const char* label, ImGuiMenuItemFlags flags) could be a candidate, for again, probably not willing to add that just for this. Even with that added, it would make thing a little more painful to use on the users' side.

C) Third solution, perhaps more approachable, would be to add a new window state flag to enable-disable the behavior of auto-closing popups. The problem is to settle on what's the correct way to name this API. The behavior that's currently used but not exposed publicly is completely arbitrary ("Selectable/MenuItem closes current popup when their parent is a popup, unless ImGuiSelectableFlags_DontClosePopups flag is set"). If we expose it as an option in the public API it comes with extras expectations: why just Selectable/MenuItem? Can I configure it separately for different types of widget?

D) Fourth solution, if you need this behavior just use Checkbox() and not MenuItem().

ocornut commented 7 years ago

@MrMarkie following post above, I have now added a non-publicly exposed flag, if you include <imgui_internal.h> you can do in your menu: ImGui::PushItemFlag(ImGuiItemFlags_SelectableDontClosePopup, true); / ImGui::PopItemFlag(). To disable closing the menu. You can still call CloseCurrentPopup() to close a menu explicitly in code. Putting it in imgui_internal.h for now means I have more flexibility with changing the name/semantic/rules of this later. In particular we might want to split the behavior between mouse and gamepad/keyboard.

ocornut commented 7 years ago

MOVING TO #787