Nav: Tabbing now cycles through all items when ImGuiConfigFlags_NavEnableKeyboard is set. (#3092, #5759, #787)

This commit is contained in:
ocornut 2023-03-09 18:53:20 +01:00
parent e83fb468c6
commit 2bb9e35a48
3 changed files with 103 additions and 73 deletions

View File

@ -53,6 +53,10 @@ Breaking Changes:
Other changes:
- Nav: Tabbing now cycles through all items when ImGuiConfigFlags_NavEnableKeyboard is set.
(#3092, #5759, #787)
- Nav: Tabbing/Shift-Tabbing can more reliably be used to step out of an item that is not
tab-stoppable. (#3092, #5759, #787)
- Nav: Made Enter key submit the same type of Activation event as Space key,
allowing to press buttons with Enter. (#5606)
(Enter emulates a "prefer text input" activation vs.

170
imgui.cpp
View File

@ -39,14 +39,13 @@ Index of this file:
DOCUMENTATION
- MISSION STATEMENT
- END-USER GUIDE
- CONTROLS GUIDE
- PROGRAMMER GUIDE
- READ FIRST
- HOW TO UPDATE TO A NEWER VERSION OF DEAR IMGUI
- GETTING STARTED WITH INTEGRATING DEAR IMGUI IN YOUR CODE/ENGINE
- HOW A SIMPLE APPLICATION MAY LOOK LIKE
- HOW A SIMPLE RENDERING FUNCTION MAY LOOK LIKE
- USING KEYBOARD/GAMEPAD NAVIGATION CONTROLS
- API BREAKING CHANGES (read me when you update!)
- FREQUENTLY ASKED QUESTIONS (FAQ)
- Read all answers online: https://www.dearimgui.org/faq, or in docs/FAQ.md (with a Markdown viewer)
@ -114,27 +113,73 @@ CODE
- Limited layout features, intricate layouts are typically crafted in code.
END-USER GUIDE
CONTROLS GUIDE
==============
- Double-click on title bar to collapse window.
- Click upper right corner to close a window, available when 'bool* p_open' is passed to ImGui::Begin().
- Click and drag on lower right corner to resize window (double-click to auto fit window to its contents).
- Click and drag on any empty space to move window.
- TAB/SHIFT+TAB to cycle through keyboard editable fields.
- CTRL+Click on a slider or drag box to input value as text.
- Use mouse wheel to scroll.
- Text editor:
- Hold SHIFT or use mouse to select text.
- CTRL+Left/Right to word jump.
- CTRL+Shift+Left/Right to select words.
- CTRL+A or Double-Click to select all.
- CTRL+X,CTRL+C,CTRL+V to use OS clipboard/
- CTRL+Z,CTRL+Y to undo/redo.
- ESCAPE to revert text to its original value.
- Controls are automatically adjusted for OSX to match standard OSX text editing operations.
- General Keyboard controls: enable with ImGuiConfigFlags_NavEnableKeyboard.
- General Gamepad controls: enable with ImGuiConfigFlags_NavEnableGamepad. Download controller mapping PNG/PSD at http://dearimgui.org/controls_sheets
- MOUSE CONTROLS
- Mouse wheel: Scroll vertically.
- SHIFT+Mouse wheel: Scroll horizontally.
- Click [X]: Close a window, available when 'bool* p_open' is passed to ImGui::Begin().
- Click ^, Double-Click title: Collapse window.
- Drag on corner/border: Resize window (double-click to auto fit window to its contents).
- Drag on any empty space: Move window (unless io.ConfigWindowsMoveFromTitleBarOnly = true).
- Left-click outside popup: Close popup stack (right-click over underlying popup: Partially close popup stack).
- TEXT EDITOR
- Hold SHIFT or Drag Mouse: Select text.
- CTRL+Left/Right: Word jump.
- CTRL+Shift+Left/Right: Select words.
- CTRL+A or Double-Click: Select All.
- CTRL+X, CTRL+C, CTRL+V: Use OS clipboard.
- CTRL+Z, CTRL+Y: Undo, Redo.
- ESCAPE: Revert text to its original value.
- On OSX, controls are automatically adjusted to match standard OSX text editing shortcuts and behaviors.
- KEYBOARD CONTROLS
- Basic:
- Tab, SHIFT+Tab Cycle through text editable fields.
- CTRL+Tab, CTRL+Shift+Tab Cycle through windows.
- CTRL+Click Input text into a Slider or Drag widget.
- Extended features with `io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard`:
- Tab, SHIFT+Tab: Cycle through every items.
- Arrow keys Move through items using directional navigation. Tweak value.
- Arrow keys + Alt, Shift Tweak slower, tweak faster (when using arrow keys).
- Enter Activate item (prefer text input when possible).
- Space Activate item (prefer tweaking with arrows when possible).
- Escape Deactivate item, leave child window, close popup.
- Page Up, Page Down Previous page, next page.
- Home, End Scroll to top, scroll to bottom.
- Alt Toggle between scrolling layer and menu layer.
- CTRL+Tab then Ctrl+Arrows Move window. Hold SHIFT to resize instead of moving.
- Output when ImGuiConfigFlags_NavEnableKeyboard set,
- io.WantCaptureKeyboard flag is set when keyboard is claimed.
- io.NavActive: true when a window is focused and it doesn't have the ImGuiWindowFlags_NoNavInputs flag set.
- io.NavVisible: true when the navigation cursor is visible (usually goes to back false when mouse is used).
- GAMEPAD CONTROLS
- Enable with 'io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad'.
- Particularly useful to use Dear ImGui on a console system (e.g. PlayStation, Switch, Xbox) without a mouse!
- Download controller mapping PNG/PSD at http://dearimgui.org/controls_sheets
- Backend support: backend needs to:
- Set 'io.BackendFlags |= ImGuiBackendFlags_HasGamepad' + call io.AddKeyEvent/AddKeyAnalogEvent() with ImGuiKey_Gamepad_XXX keys.
- For analog values (0.0f to 1.0f), backend is responsible to handling a dead-zone and rescaling inputs accordingly.
Backend code will probably need to transform your raw inputs (such as e.g. remapping your 0.2..0.9 raw input range to 0.0..1.0 imgui range, etc.).
- BEFORE 1.87, BACKENDS USED TO WRITE TO io.NavInputs[]. This is now obsolete. Please call io functions instead!
- If you need to share inputs between your game and the Dear ImGui interface, the easiest approach is to go all-or-nothing,
with a buttons combo to toggle the target. Please reach out if you think the game vs navigation input sharing could be improved.
- REMOTE INPUTS SHARING & MOUSE EMULATION
- PS4/PS5 users: Consider emulating a mouse cursor with DualShock touch pad or a spare analog stick as a mouse-emulation fallback.
- Consoles/Tablet/Phone users: Consider using a Synergy 1.x server (on your PC) + run examples/libs/synergy/uSynergy.c (on your console/tablet/phone app)
in order to share your PC mouse/keyboard.
- See https://github.com/ocornut/imgui/wiki/Useful-Extensions#remoting for other remoting solutions.
- On a TV/console system where readability may be lower or mouse inputs may be awkward, you may want to set the ImGuiConfigFlags_NavEnableSetMousePos flag.
Enabling ImGuiConfigFlags_NavEnableSetMousePos + ImGuiBackendFlags_HasSetMousePos instructs Dear ImGui to move your mouse cursor along with navigation movements.
When enabled, the NewFrame() function may alter 'io.MousePos' and set 'io.WantSetMousePos' to notify you that it wants the mouse cursor to be moved.
When that happens your backend NEEDS to move the OS or underlying mouse cursor on the next frame. Some of the backends in examples/ do that.
(If you set the NavEnableSetMousePos flag but don't honor 'io.WantSetMousePos' properly, Dear ImGui will misbehave as it will see your mouse moving back & forth!)
(In a setup when you may not have easy control over the mouse cursor, e.g. uSynergy.c doesn't expose moving remote mouse cursor, you may want
to set a boolean to ignore your other external mouse positions until the external source is moved again.)
PROGRAMMER GUIDE
@ -344,41 +389,6 @@ CODE
}
USING KEYBOARD/GAMEPAD NAVIGATION CONTROLS
------------------------------------------
- The keyboard/gamepad navigation is fairly functional and keeps being improved.
- Gamepad support is particularly useful to use Dear ImGui on a console system (e.g. PlayStation, Switch, Xbox) without a mouse!
- The initial focus was to support game controllers, but keyboard is becoming increasingly and decently usable.
- Keyboard:
- Application: Set io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard to enable.
- Arrow keys to move. Space or Enter to activate (on a slider/drag: Space will tweak with arrow, Enter will input text).
- When keyboard navigation is active (io.NavActive + ImGuiConfigFlags_NavEnableKeyboard),
the io.WantCaptureKeyboard flag will be set. For more advanced uses, you may want to read from:
- io.NavActive: true when a window is focused and it doesn't have the ImGuiWindowFlags_NoNavInputs flag set.
- io.NavVisible: true when the navigation cursor is visible (and usually goes false when mouse is used).
- or query focus information with e.g. IsWindowFocused(ImGuiFocusedFlags_AnyWindow), IsItemFocused() etc. functions.
Please reach out if you think the game vs navigation input sharing could be improved.
- Gamepad:
- Application: Set io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad to enable.
- Backend: Set io.BackendFlags |= ImGuiBackendFlags_HasGamepad + call io.AddKeyEvent/AddKeyAnalogEvent() with ImGuiKey_Gamepad_XXX keys.
For analog values (0.0f to 1.0f), backend is responsible to handling a dead-zone and rescaling inputs accordingly.
Backend code will probably need to transform your raw inputs (such as e.g. remapping your 0.2..0.9 raw input range to 0.0..1.0 imgui range, etc.).
- BEFORE 1.87, BACKENDS USED TO WRITE TO io.NavInputs[]. This is now obsolete. Please call io functions instead!
- You can download PNG/PSD files depicting the gamepad controls for common controllers at: http://dearimgui.org/controls_sheets
- If you need to share inputs between your game and the Dear ImGui interface, the easiest approach is to go all-or-nothing,
with a buttons combo to toggle the target. Please reach out if you think the game vs navigation input sharing could be improved.
- Mouse:
- PS4/PS5 users: Consider emulating a mouse cursor with DualShock4 touch pad or a spare analog stick as a mouse-emulation fallback.
- Consoles/Tablet/Phone users: Consider using a Synergy 1.x server (on your PC) + uSynergy.c (on your console/tablet/phone app) to share your PC mouse/keyboard.
- On a TV/console system where readability may be lower or mouse inputs may be awkward, you may want to set the ImGuiConfigFlags_NavEnableSetMousePos flag.
Enabling ImGuiConfigFlags_NavEnableSetMousePos + ImGuiBackendFlags_HasSetMousePos instructs dear imgui to move your mouse cursor along with navigation movements.
When enabled, the NewFrame() function may alter 'io.MousePos' and set 'io.WantSetMousePos' to notify you that it wants the mouse cursor to be moved.
When that happens your backend NEEDS to move the OS or underlying mouse cursor on the next frame. Some of the backends in examples/ do that.
(If you set the NavEnableSetMousePos flag but don't honor 'io.WantSetMousePos' properly, imgui will misbehave as it will see your mouse moving back and forth!)
(In a setup when you may not have easy control over the mouse cursor, e.g. uSynergy.c doesn't expose moving remote mouse cursor, you may want
to set a boolean to ignore your other external mouse positions until the external source is moved again.)
API BREAKING CHANGES
====================
@ -1013,7 +1023,7 @@ static void NavEndFrame();
static bool NavScoreItem(ImGuiNavItemData* result);
static void NavApplyItemToResult(ImGuiNavItemData* result);
static void NavProcessItem();
static void NavProcessItemForTabbingRequest(ImGuiID id);
static void NavProcessItemForTabbingRequest(ImGuiID id, ImGuiItemFlags item_flags, ImGuiNavMoveFlags move_flags);
static ImVec2 NavCalcPreferredRefPos();
static void NavSaveLastChildNavWindowIntoParent(ImGuiWindow* nav_window);
static ImGuiWindow* NavRestoreLastChildNavWindow(ImGuiWindow* window);
@ -10746,16 +10756,14 @@ static void ImGui::NavProcessItem()
// Process Move Request (scoring for navigation)
// FIXME-NAV: Consider policy for double scoring (scoring from NavScoringRect + scoring from a rect wrapped according to current wrapping policy)
if (g.NavMoveScoringItems)
if (g.NavMoveScoringItems && (item_flags & ImGuiItemFlags_Disabled) == 0)
{
const bool is_tab_stop = (item_flags & ImGuiItemFlags_Inputable) && (item_flags & (ImGuiItemFlags_NoTabStop | ImGuiItemFlags_Disabled)) == 0;
const bool is_tabbing = (g.NavMoveFlags & ImGuiNavMoveFlags_Tabbing) != 0;
if (is_tabbing)
{
if (is_tab_stop || (g.NavMoveFlags & ImGuiNavMoveFlags_FocusApi))
NavProcessItemForTabbingRequest(id);
NavProcessItemForTabbingRequest(id, item_flags, g.NavMoveFlags);
}
else if ((g.NavId != id || (g.NavMoveFlags & ImGuiNavMoveFlags_AllowCurrentNavId)) && !(item_flags & ImGuiItemFlags_Disabled))
else if (g.NavId != id || (g.NavMoveFlags & ImGuiNavMoveFlags_AllowCurrentNavId))
{
ImGuiNavItemData* result = (window == g.NavWindow) ? &g.NavMoveResultLocal : &g.NavMoveResultOther;
if (NavScoreItem(result))
@ -10789,18 +10797,31 @@ static void ImGui::NavProcessItem()
// - Case 3: tab forward wrap: set result to first eligible item (preemptively), on ref id set counter, on next frame if counter hasn't elapsed store result. // FIXME-TABBING: Could be done as a next-frame forwarded request
// - Case 4: tab backward: store all results, on ref id pick prev, stop storing
// - Case 5: tab backward wrap: store all results, on ref id if no result keep storing until last // FIXME-TABBING: Could be done as next-frame forwarded requested
void ImGui::NavProcessItemForTabbingRequest(ImGuiID id)
void ImGui::NavProcessItemForTabbingRequest(ImGuiID id, ImGuiItemFlags item_flags, ImGuiNavMoveFlags move_flags)
{
ImGuiContext& g = *GImGui;
if ((move_flags & ImGuiNavMoveFlags_FocusApi) == 0)
if (g.NavLayer != g.CurrentWindow->DC.NavLayerCurrent)
return;
// - Can always land on an item when using API call.
// - Tabbing with _NavEnableKeyboard (space/enter/arrows): goes through every item.
// - Tabbing without _NavEnableKeyboard: goes through inputable items only.
bool can_stop;
if (move_flags & ImGuiNavMoveFlags_FocusApi)
can_stop = true;
else
can_stop = (item_flags & ImGuiItemFlags_NoTabStop) == 0 && ((g.IO.ConfigFlags & ImGuiConfigFlags_NavEnableKeyboard) || (item_flags & ImGuiItemFlags_Inputable));
// Always store in NavMoveResultLocal (unlike directional request which uses NavMoveResultOther on sibling/flattened windows)
ImGuiNavItemData* result = &g.NavMoveResultLocal;
if (g.NavTabbingDir == +1)
{
// Tab Forward or SetKeyboardFocusHere() with >= 0
if (g.NavTabbingResultFirst.ID == 0)
if (can_stop && g.NavTabbingResultFirst.ID == 0)
NavApplyItemToResult(&g.NavTabbingResultFirst);
if (--g.NavTabbingCounter == 0)
if (can_stop && g.NavTabbingCounter > 0 && --g.NavTabbingCounter == 0)
NavMoveRequestResolveWithLastItem(result);
else if (g.NavId == id)
g.NavTabbingCounter = 1;
@ -10816,16 +10837,18 @@ void ImGui::NavProcessItemForTabbingRequest(ImGuiID id)
NavUpdateAnyRequestFlag();
}
}
else
else if (can_stop)
{
// Keep applying until reaching NavId
NavApplyItemToResult(result);
}
}
else if (g.NavTabbingDir == 0)
{
// Tab Init
if (g.NavTabbingResultFirst.ID == 0)
NavMoveRequestResolveWithLastItem(&g.NavTabbingResultFirst);
if (can_stop && g.NavId == id)
NavMoveRequestResolveWithLastItem(result);
if (can_stop && g.NavTabbingResultFirst.ID == 0) // Tab init
NavApplyItemToResult(&g.NavTabbingResultFirst);
}
}
@ -11348,8 +11371,11 @@ void ImGui::NavUpdateCreateTabbingRequest()
// (this is ALWAYS ENABLED, regardless of ImGuiConfigFlags_NavEnableKeyboard flag!)
// Initially this was designed to use counters and modulo arithmetic, but that could not work with unsubmitted items (list clipper). Instead we use a strategy close to other move requests.
// See NavProcessItemForTabbingRequest() for a description of the various forward/backward tabbing cases with and without wrapping.
//// FIXME: We use (g.ActiveId == 0) but (g.NavDisableHighlight == false) might be righter once we can tab through anything
g.NavTabbingDir = g.IO.KeyShift ? -1 : (g.ActiveId == 0) ? 0 : +1;
const bool nav_keyboard_active = (g.IO.ConfigFlags & ImGuiConfigFlags_NavEnableKeyboard) != 0;
if (nav_keyboard_active)
g.NavTabbingDir = g.IO.KeyShift ? -1 : (g.NavDisableHighlight == true && g.ActiveId == 0) ? 0 : +1;
else
g.NavTabbingDir = g.IO.KeyShift ? -1 : (g.ActiveId == 0) ? 0 : +1;
ImGuiScrollFlags scroll_flags = window->Appearing ? ImGuiScrollFlags_KeepVisibleEdgeX | ImGuiScrollFlags_AlwaysCenterY : ImGuiScrollFlags_KeepVisibleEdgeX | ImGuiScrollFlags_KeepVisibleEdgeY;
ImGuiDir clip_dir = (g.NavTabbingDir < 0) ? ImGuiDir_Up : ImGuiDir_Down;
NavMoveRequestSubmit(ImGuiDir_None, clip_dir, ImGuiNavMoveFlags_Tabbing, scroll_flags); // FIXME-NAV: Once we refactor tabbing, add LegacyApi flag to not activate non-inputable.
@ -11369,7 +11395,7 @@ void ImGui::NavMoveRequestApplyResult()
ImGuiNavItemData* result = (g.NavMoveResultLocal.ID != 0) ? &g.NavMoveResultLocal : (g.NavMoveResultOther.ID != 0) ? &g.NavMoveResultOther : NULL;
// Tabbing forward wrap
if (g.NavMoveFlags & ImGuiNavMoveFlags_Tabbing)
if ((g.NavMoveFlags & ImGuiNavMoveFlags_Tabbing) && result == NULL)
if ((g.NavTabbingCounter == 1 || g.NavTabbingDir == 0) && g.NavTabbingResultFirst.ID)
result = &g.NavTabbingResultFirst;

View File

@ -1495,7 +1495,7 @@ enum ImGuiNavInput
enum ImGuiConfigFlags_
{
ImGuiConfigFlags_None = 0,
ImGuiConfigFlags_NavEnableKeyboard = 1 << 0, // Master keyboard navigation enable flag.
ImGuiConfigFlags_NavEnableKeyboard = 1 << 0, // Master keyboard navigation enable flag. Enable full Tabbing + directional arrows + space/enter to activate.
ImGuiConfigFlags_NavEnableGamepad = 1 << 1, // Master gamepad navigation enable flag. Backend also needs to set ImGuiBackendFlags_HasGamepad.
ImGuiConfigFlags_NavEnableSetMousePos = 1 << 2, // Instruct navigation to move the mouse cursor. May be useful on TV/console systems where moving a virtual mouse is awkward. Will update io.MousePos and set io.WantSetMousePos=true. If enabled you MUST honor io.WantSetMousePos requests in your backend, otherwise ImGui will react as if the mouse is jumping around back and forth.
ImGuiConfigFlags_NavNoCaptureKeyboard = 1 << 3, // Instruct navigation to not set the io.WantCaptureKeyboard flag when io.NavActive is set.