This document discusses designing user interfaces for embedded synthesizer products. It covers hardware GUI design including creating a state of flow, targeting display hardware, and distinguishing hard versus soft controls. It also discusses implementing UIs with JUCE including embracing the ProJucer tool, instantiating components early, using virtual panels, and writing automated tests. Design topics like navigation, navigation widgets, and examples are presented. The document emphasizes rapid dispatch, keeping the whole UI up-to-date, and updating data without redraws for performance on embedded systems. It also shows examples of semantic UI tests, functional tests, and integrating tests into continuous integration workflows.
4. WHAT IS THIS ABOUT?
> Product with a central LCD UI
> Physical front-panel with knobs and switches
> Embedded Linux at its core
> JUCE as cross-platform framework
(develop on macOS, run on Linux)
> We learned a few things we'd like to share
14. HARD VERSUS SOFT CONTROLS
> Hard as in hardware (fixed-purpose)
High frequency
High immediacy
> Soft as in software-defined (changeable)
Embedded GUI provides soft control and feedback
31. FULL CODE-LEVEL TWEAKABILITY
OscillatorGraph::OscillatorGraph() {
//[Constructor_pre] You can add your own custom stuff here..
mix_ = 0.f;
pulseWidth_ = 0.f;
mode_ = "F";
//[/Constructor_pre]
//[UserPreSize]
//[/UserPreSize]
setSize (380, 280);
//[Constructor] You can add your own custom stuff here..
//[/Constructor]
}
43. NAVIGATIONAL WIDGETS
> Limitations can be a strength
> Keep UI organisation as shallow as possible
> Dedicated buttons are quicker than lists
> Minimise long lists - group by related functions
51. PLENTY OF MEMORY, LIMITED CPU
MainComponent::MainComponent() {
//[UserPreSize]
// create all UI panels
lfo1_ = new LFOPanel(0); lfo1_->setComponentID(LFO1);
lfo2_ = new LFOPanel(1); lfo2_->setComponentID(LFO2);
oscillator1_ = new OscillatorPanel(0); oscillator1_->setComponentID(OSCILLATOR1);
oscillator2_ = new OscillatorPanel(1); oscillator2_->setComponentID(OSCILLATOR2);
// ...
addChildComponent(lfo1_);
addChildComponent(lfo2_);
addChildComponent(oscillator1_);
addChildComponent(oscillator2_);
// ...
for (int c = 0; c < getNumChildComponents(); ++c) {
getChildComponent(c)->setVisible(false);
}
//[/UserPreSize]
52. PLENTY OF MEMORY, LIMITED CPU
void MainComponent::showPanel(Component* panel) {
if (panel) {
Component* parent = panel->getParentComponent();
if (parent) {
for (int i = 0; i < parent->getNumChildComponents(); ++i) {
Component* child = parent->getChildComponent(i);
if (child && child != panel) {
child->setVisible(false);
}
}
}
panel->setVisible(true);
}
}
53. PLENTY OF MEMORY, LIMITED CPU
LFOPanel::LFOPanel(int ordinal) : ordinal_(ordinal) {
addAndMakeVisible(rate_ = new Label(String(), TRANS("0.01ms")));
//[UserPreSize]
rate_->setComponentID(RATE);
//[/UserPreSize]
setSize (760, 460);
//[Constructor] You can add your own custom stuff here..
LApp->registerComponentUpdater(lfo1Rate, new LFORateUpdater(this));
//[/Constructor]
}
54. PLENTY OF MEMORY, LIMITED CPU
struct LFORateUpdater : ComponentUpdater {
LFORateUpdater(LFOPanel* panel) : panel_(panel)
{
};
void update(var value, var previous) override
{
panel_->updateRateLabel(value);
}
LFOPanel* const panel_;
};
72. USEFUL TEST RUNNER TRICKS
> Ship your executable with all tests included
void LASFrontEndApplication::initialise(const String& commandLine)
{
// Fully setup and initialise the application
// ...
// Run tests if necessary
if (!testRunner_.scheduleTests())
// If no tests are going to run, populate with example data if that was requested
// ...
}
}
LASTestRunner testRunner_;
73. USEFUL TEST RUNNER TRICKS
> Ship your executable with all tests included
bool LASTestRunner::Pimpl::scheduleTests() {
bool runTests = false;
#if JUCE_DEBUG
if (!JUCEApplication::getInstance()->getCommandLineParameters().contains("--disable-tests")) {
runTests = true;
}
#endif
if (JUCEApplication::getInstance()->getCommandLineParameters().contains("--enable-tests")) {
runTests = true;
}
if (runTests) {
(new ScheduleTestsCallback(this))->post();
}
}