Skip to content

Commit

Permalink
Keyboard traversal support
Browse files Browse the repository at this point in the history
Add an explicit keyboard traversal support which assumes all
editable components are labeled or labelable and so allows
tab order to be specified explicitly as subordinate to parent
components, which have a group orering index.
  • Loading branch information
baconpaul committed Jan 6, 2025
1 parent d81a214 commit 86e1174
Show file tree
Hide file tree
Showing 6 changed files with 298 additions and 6 deletions.
6 changes: 5 additions & 1 deletion include/sst/jucegui/accessibility/FocusDebugger.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@

#include <juce_gui_basics/juce_gui_basics.h>

#include "KeyboardTraverser.h"

namespace sst::jucegui::accessibility
{
struct FocusDebugger : public juce::FocusChangeListener
Expand Down Expand Up @@ -57,8 +59,10 @@ struct FocusDebugger : public juce::FocusChangeListener
bd += fc->getBounds().getTopLeft();
fc = fc->getParentComponent();
}

std::cout << "FD : [" << std::hex << ofc << std::dec << "] " << ofc->getTitle() << " @ "
<< bd.toString() << std::endl;
<< bd.toString() << " traversalId=" << KeyboardTraverser::traversalId(ofc)
<< std::endl;
debugComponent->setBounds(bd);
}

Expand Down
264 changes: 264 additions & 0 deletions include/sst/jucegui/accessibility/KeyboardTraverser.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,264 @@
/*
* sst-jucegui - an open source library of juce widgets
* built by Surge Synth Team.
*
* Copyright 2023-2024, various authors, as described in the GitHub
* transaction log.
*
* sst-jucegui is released under the MIT license, as described
* by "LICENSE.md" in this repository. This means you may use this
* in commercial software if you are a JUCE Licensee. If you use JUCE
* in the open source / GPL3 context, your combined work must be
* released under GPL3.
*
* All source in sst-jucegui available at
* https://github.com/surge-synthesizer/sst-jucegui
*/

#ifndef INCLUDE_SST_JUCEGUI_ACCESSIBILITY_KEYBOARDTRAVERSER_H
#define INCLUDE_SST_JUCEGUI_ACCESSIBILITY_KEYBOARDTRAVERSER_H

#include <iostream>
#include <limits>

namespace sst::jucegui::accessibility
{
// #define KLG(...) std::cout << __FILE__ << ":" << __LINE__ << " " << __func__ << " " <<
// __VA_ARGS__ << std::endl;
#define KLG(...)
struct KeyboardTraverser : juce::KeyboardFocusTraverser
{
struct IssueIDIfMissingMarker
{
virtual ~IssueIDIfMissingMarker() = default;
};

~KeyboardTraverser() override = default;
juce::Component *getDefaultComponent(juce::Component *parentComponent) override
{
KLG(nm(parentComponent));
auto tn = findNeighborByIndex(parentComponent, START);
if (tn)
return tn;

return juce::KeyboardFocusTraverser::getDefaultComponent(parentComponent);
}

static const juce::Identifier &idIndex()
{
static juce::Identifier idIndex{"sstjucegui-idIndex"};
return idIndex;
}

static const juce::Identifier &fullIdIndex()
{
static juce::Identifier idIndex{"sstjucegui-fullIdIndex"};
return idIndex;
}

static std::string nm(juce::Component *c)
{
if (!c)
return "nullptr";
std::string res = "'" + c->getTitle().toStdString();
res += "' (tid=";
res += std::to_string(traversalId(c)) + ")";
return res;
}

enum Mode
{
START,
PRIOR,
NEXT,
END
};
const char *dirName[4] = {"start", "prior", "next", "end"};
juce::Component *findNeighborByIndex(juce::Component *from, Mode dir) const
{
auto c = from->getParentComponent();
while (c)
{
if (c->isKeyboardFocusContainer())
{
break;
}
c = c->getParentComponent();
}

if (!c)
return nullptr;

std::function<void(juce::Component *)> rec;

int lim = (dir == NEXT || dir == START ? std::numeric_limits<int>::max()
: std::numeric_limits<int>::min());
int midx = traversalId(from);
if (midx == 0)
{
return nullptr;
}

KLG("Finding neighbor " << dirName[(int)dir] << " from " << nm(from))

// also find extrma for wrap

// careful
juce::Component *res{nullptr};
rec = [&, this](juce::Component *c) {
if (!c->isVisible() || !c->isEnabled())
return;

if (c->isAccessible())
{
auto fidx = traversalId(c);
if (fidx != 0)
{
if (dir == PRIOR)
{
if (fidx < midx && fidx > lim)
{
lim = fidx;
res = c;
}
}
else if (dir == NEXT)
{
if (fidx > midx && fidx < lim)
{
lim = fidx;
res = c;
}
}
else if (dir == START)
{
if (fidx < lim)
{
lim = fidx;
res = c;
}
}
else if (dir == END)
{
if (fidx > lim)
{
lim = fidx;
res = c;
}
}
}
}
for (auto k : c->getChildren())
{
rec(k);
}
};
if (c)
rec(c);

KLG("Returning neighbor " << nm(res));
return res;
}

static void issueTraversalId(juce::Component *c)
{
int newID{3};
auto p = c->getParentComponent();
if (!p)
return;
for (auto k : p->getChildren())
{
if (k == c)
continue;
auto hasidx = k->getProperties().getVarPointer(idIndex());
if (hasidx)
newID = std::max(newID, (int)*hasidx + 1);
}
c->getProperties().set(idIndex(), newID);
KLG("Issued " << nm(c) << " " << newID);
}
static int traversalId(juce::Component *c)
{
if (!c->isAccessible())
return 0;

auto hasidx = c->getProperties().getVarPointer(idIndex());
if (!hasidx)
{
if (dynamic_cast<IssueIDIfMissingMarker *>(c))
{
issueTraversalId(c);
}
else
{
return 0;
}
}

auto fullidx = c->getProperties().getVarPointer(fullIdIndex());
if (fullidx)
return *fullidx;

auto res = 0;
auto curr = c;
while (curr)
{
auto idx = curr->getProperties().getVarPointer(idIndex());
if (idx)
res += (int)*idx;
curr = curr->getParentComponent();
}
if (res != 0)
{
c->getProperties().set(fullIdIndex(), res);
}
return res;
}

juce::Component *getNextComponent(juce::Component *current) override
{
auto tn = findNeighborByIndex(current, NEXT);
if (tn)
{
return tn;
}
KLG("** NULL NEXT ** " << nm(current));
auto ln = findNeighborByIndex(current, START);
if (ln)
{
KLG("Returning start " << nm(ln));
return ln;
}

return juce::KeyboardFocusTraverser::getNextComponent(current);
}
juce::Component *getPreviousComponent(juce::Component *current) override
{
auto tn = findNeighborByIndex(current, PRIOR);
if (tn)
{
return tn;
}
KLG("** NULL PREV ** " << nm(current));
auto ln = findNeighborByIndex(current, END);
if (ln)
{
KLG("Returning end " << nm(ln));
return ln;
}
// shrug
return juce::KeyboardFocusTraverser::getPreviousComponent(current);
}
std::vector<juce::Component *> getAllComponents(juce::Component *parentComponent) override
{
return juce::KeyboardFocusTraverser::getAllComponents(parentComponent);
}

static void assignTraversalIndex(juce::Component *c, int idx)
{
c->getProperties().set(idIndex(), idx);
}
};
#undef KLG
} // namespace sst::jucegui::accessibility
#endif // KEYBOARDTRAVERSER_H
5 changes: 3 additions & 2 deletions include/sst/jucegui/components/ContinuousParamEditor.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
#include "BaseStyles.h"
#include "sst/jucegui/accessibility/AccessibilityConfiguration.h"
#include "sst/jucegui/accessibility/AccessibilityKeyboardEdits.h"
#include "sst/jucegui/accessibility/KeyboardTraverser.h"

namespace sst::jucegui::components
{
Expand All @@ -38,8 +39,8 @@ struct ContinuousParamEditor
public EditableComponentBase<ContinuousParamEditor>,
public style::SettingsConsumer,
public sst::jucegui::accessibility::AccessibilityConfiguration,
public sst::jucegui::accessibility::AccessibilityKeyboardEditSupport<ContinuousParamEditor>

public sst::jucegui::accessibility::AccessibilityKeyboardEditSupport<ContinuousParamEditor>,
public sst::jucegui::accessibility::KeyboardTraverser::IssueIDIfMissingMarker
{
struct Styles : base_styles::ValueBearing,
base_styles::ModulationValueBearing,
Expand Down
5 changes: 4 additions & 1 deletion include/sst/jucegui/components/DiscreteParamEditor.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
#include <sst/jucegui/components/DiscreteParamMenuBuilder.h>
#include "sst/jucegui/accessibility/AccessibilityConfiguration.h"
#include "sst/jucegui/accessibility/AccessibilityKeyboardEdits.h"
#include "sst/jucegui/accessibility/KeyboardTraverser.h"

namespace sst::jucegui::components
{
Expand All @@ -31,7 +32,9 @@ struct DiscreteParamEditor
public EditableComponentBase<DiscreteParamEditor>,
public data::Discrete::DataListener,
public sst::jucegui::accessibility::AccessibilityConfiguration,
public sst::jucegui::accessibility::AccessibilityKeyboardEditSupport<DiscreteParamEditor>
public sst::jucegui::accessibility::AccessibilityKeyboardEditSupport<DiscreteParamEditor>,
public sst::jucegui::accessibility::KeyboardTraverser::IssueIDIfMissingMarker

{
DiscreteParamEditor()
{
Expand Down
18 changes: 17 additions & 1 deletion include/sst/jucegui/components/WindowPanel.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
#include <juce_gui_basics/juce_gui_basics.h>
#include <string>
#include <sst/jucegui/style/StyleAndSettingsConsumer.h>
#include "sst/jucegui/accessibility/KeyboardTraverser.h"
#include <sst/jucegui/style/StyleSheet.h>
#include "BaseStyles.h"

Expand All @@ -42,7 +43,13 @@ struct WindowPanel : public juce::Component,
}
};

WindowPanel() : style::StyleConsumer(Styles::styleClass) { setAccessible(true); }
WindowPanel(bool withExplicitTraversal = false)
: explicitTraversal{withExplicitTraversal}, style::StyleConsumer(Styles::styleClass)
{
setAccessible(true);
setFocusContainerType(juce::Component::FocusContainerType::keyboardFocusContainer);
setTitle("Application Window");
}
~WindowPanel() = default;

void paint(juce::Graphics &g) override
Expand All @@ -53,6 +60,15 @@ struct WindowPanel : public juce::Component,
g.fillRect(getLocalBounds());
}

bool explicitTraversal{false};
std::unique_ptr<juce::ComponentTraverser> createKeyboardFocusTraverser() override
{
if (explicitTraversal)
return std::make_unique<sst::jucegui::accessibility::KeyboardTraverser>();
else
return std::make_unique<juce::KeyboardFocusTraverser>();
}

JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(WindowPanel)
};
} // namespace sst::jucegui::components
Expand Down
6 changes: 5 additions & 1 deletion src/sst/jucegui/components/JogUpDownButton.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@

namespace sst::jucegui::components
{
JogUpDownButton::JogUpDownButton() : style::StyleConsumer(Styles::styleClass) {}
JogUpDownButton::JogUpDownButton() : style::StyleConsumer(Styles::styleClass)
{
setAccessible(true);
setWantsKeyboardFocus(true);
}
JogUpDownButton::~JogUpDownButton()
{
if (data)
Expand Down

0 comments on commit 86e1174

Please sign in to comment.