re-structure menu, darken UI

This commit is contained in:
Armin 2026-09-08 21:57:34 +02:00
commit 4b44841049
16 changed files with 1217 additions and 304 deletions

View file

@ -1,6 +1,7 @@
#include "LcdPanel.h"
#include "BuildInfo.h"
#include <cstring>
#include <cmath>
// ---------------------------------------------------------------------------
// 5x7 dot-matrix font. Each glyph is 5 columns x 7 rows, bit 6 (0x40) of each
@ -180,9 +181,9 @@ const juce::uint8* LcdPanel::glyphFor (int key)
LcdPanel::LcdPanel (RMX19AudioProcessor& p)
: proc (p)
{
dotImage = juce::Image (juce::Image::ARGB, gridW, gridH, false);
displayImage = juce::Image (juce::Image::ARGB, gridW * 2, gridH * 2, false);
backdrop = juce::Image (juce::Image::ARGB, gridW * 2, gridH * 2, false);
displayImage = juce::Image (juce::Image::ARGB, gridW * subsample, gridH * subsample, false);
presentImage = juce::Image (juce::Image::ARGB, gridW * presentScale, gridH * presentScale, false);
backdrop = juce::Image (juce::Image::ARGB, gridW * subsample, gridH * subsample, false);
buildBackdrop();
splashUntil = juce::Time::getMillisecondCounter() + 1500;
lastMidi = proc.getLastMidiTime();
@ -216,12 +217,8 @@ void LcdPanel::timerCallback()
if (screen == Screen::Home)
{
const int v = proc.getActiveVoices();
if (v != lastVoices)
{
lastVoices = v;
needsRedraw = true;
}
lastVoices = proc.getActiveVoices();
needsRedraw = true; // drive the MIDI blip + VU meter
}
if (needsRedraw)
@ -239,13 +236,9 @@ void LcdPanel::paint (juce::Graphics& g)
g.drawRect (b, 2);
// inner LCD glass (black)
const int cell = 2;
const int w = gridW * cell;
const int h = gridH * cell;
const int ox = (b.getWidth() - w) / 2;
const int oy = (b.getHeight() - h) / 2 + 2;
const auto rect = lcdImageRect();
g.setColour (juce::Colour (0xff020703));
g.fillRect (ox, oy, w, h);
g.fillRect (rect);
if (contentDirty)
{
@ -253,7 +246,56 @@ void LcdPanel::paint (juce::Graphics& g)
contentDirty = false;
}
g.drawImageAt (displayImage, ox, oy);
g.drawImage (presentImage,
rect.getX(), rect.getY(), rect.getWidth(), rect.getHeight(),
0, 0, presentImage.getWidth(), presentImage.getHeight());
}
// the on-screen rectangle (component coords) the dot-matrix image occupies:
// exactly the presentation pixels, centered inside the LCD module (with a small
// extra downward shift to match the bezel). No scaling is applied so the dot
// matrix keeps its intended physical size.
juce::Rectangle<int> LcdPanel::lcdImageRect() const
{
const int w = presentImage.getWidth();
const int h = presentImage.getHeight();
const auto b = getLocalBounds();
return { (b.getWidth() - w) / 2, (b.getHeight() - h) / 2 + 2, w, h };
}
void LcdPanel::mouseDown (const juce::MouseEvent& e)
{
if (screen != Screen::Home)
return;
// map the click into logical dot-matrix coordinates
const auto rect = lcdImageRect();
const float lx = (e.x - rect.getX()) * (float) gridW / (float) rect.getWidth();
const float ly = (e.y - rect.getY()) * (float) gridH / (float) rect.getHeight();
// patch number/name row on the home screen (5x7 font at y=3), shown as "<num>: <name>"
const auto& cur = proc.getBank().get (proc.getPatchIndex());
const int nameEnd = 2 + (4 + (int) juce::String (cur.name).length()) * (NW + 1) + 2;
if (ly < 0.0f || ly >= 11.0f) return;
if (lx < 0.0f || lx >= (float) nameEnd) return;
// native mouse menu: one submenu per category, ticking the loaded patch
juce::PopupMenu menu;
const auto& cats = proc.getBank().categories();
for (size_t c = 0; c < cats.names.size(); ++c)
{
juce::PopupMenu sub;
for (int pi : cats.patchIndices[(size_t) c])
{
const bool isCurrent = (pi == proc.getPatchIndex());
sub.addItem (proc.getBank().get (pi).name, true, isCurrent,
[this, pi] { proc.selectPatch (pi); setDirty(); });
}
menu.addSubMenu (cats.names[(size_t) c], sub);
}
menu.showMenuAsync (juce::PopupMenu::Options().withTargetScreenArea (
juce::Rectangle<int> (e.getScreenX(), e.getScreenY(), 1, 1)));
}
void LcdPanel::buildBackdrop()
@ -268,8 +310,9 @@ void LcdPanel::buildBackdrop()
// even, faint backlight (brightest toward the middle of the glass)
{
const float cx = W * 0.5f, cy = H * 0.5f;
juce::ColourGradient backlight (juce::Colour::fromRGBA (0x28, 0x5a, 0x3c, 22), cx, cy,
juce::Colour::fromRGBA (0x18, 0x2e, 0x20, 0), W * 0.38f, cy, false);
const float radius = std::min (W, H) * 2.5f;
juce::ColourGradient backlight (juce::Colour::fromRGBA (0x40, 0x80, 0x50, 40), cx, cy,
juce::Colour::fromRGBA (0x20, 0x40, 0x28, 0), cx + radius, cy, true);
dg.setGradientFill (backlight);
dg.fillRect (backdrop.getBounds());
}
@ -301,31 +344,25 @@ void LcdPanel::buildBackdrop()
dg.setGradientFill (eR); dg.fillRect (W - 10, 0, 10, H);
}
// faint "ghost" dot for every (unlit) segment, exactly like a passive matrix LCD
// faint "ghost" dot for every (unlit) segment, exactly like a passive matrix LCD.
// Alpha is boosted since the 2:1 downsample to the presentation image halves it.
for (int y = 0; y < gridH; ++y)
for (int x = 0; x < gridW; ++x)
{
const float cx = x * 2.0f + 1.0f;
const float cy = y * 2.0f + 1.0f;
dg.setColour (juce::Colour::fromRGBA (0x00, 0x88, 0x55, 6));
dg.fillEllipse (cx - 0.55f, cy - 0.55f, 1.1f, 1.1f);
const float gx = (x * (float) subsample) + subsample * 0.5f;
const float gy = (y * (float) subsample) + subsample * 0.5f;
const float r = subsample * 0.32f;
dg.setColour (juce::Colour::fromRGBA (0x00, 0x88, 0x55, 14));
dg.fillEllipse (gx - r, gy - r, r * 2.0f, r * 2.0f);
}
}
void LcdPanel::rebuild()
{
// Image::clear() hard-zeros pixels; Graphics::fillAll(transparent black) only blends
// and leaves stale content behind.
dotImage.clear (dotImage.getBounds());
{
juce::Graphics dg (dotImage);
drawContent (dg);
}
const int W = displayImage.getWidth();
const int H = displayImage.getHeight();
// start from the pre-rendered base layer (backlight, sheen, vignette, ghost dots)
// start from the pre-rendered 3D glass base (backlight, sheen, vignette, ghost dots)
{
juce::Image::BitmapData src (backdrop, juce::Image::BitmapData::readOnly);
juce::Image::BitmapData dst (displayImage, juce::Image::BitmapData::writeOnly);
@ -333,63 +370,41 @@ void LcdPanel::rebuild()
std::memcpy (dst.getLinePointer (y), src.getLinePointer (y), (size_t) W * 4u);
}
// blend the lit segments in as crisp round dots with just a hint of bloom.
// The core is near-solid so glyphs stay sharp; the glow is faint and tight
// so it can't wash the dark gaps between rows/columns.
constexpr float coreR2 = 0.36f; // bright core radius ~0.60 cell
constexpr float glowR2 = 1.10f; // faint bloom radius ~1.05 cells
// paint the content dots directly onto the glass base; px() stamps a rounded,
// anti-aliased dot for every lit segment
{
juce::Image::BitmapData cells (dotImage, juce::Image::BitmapData::readOnly);
juce::Image::BitmapData out (displayImage, juce::Image::BitmapData::readWrite);
juce::Graphics dg (displayImage);
drawContent (dg);
}
for (int cy = 0; cy < gridH; ++cy)
{
for (int cx = 0; cx < gridW; ++cx)
// exact integer box downsample (2:1) to the presentation resolution. Because
// each dot is still drawn large enough to touch its neighbours only at the
// final 2px/dot image, text reads as crisp, distinct dots.
const int PW = presentImage.getWidth(), PH = presentImage.getHeight();
const int kx = W / PW, ky = H / PH, n = kx * ky;
{
juce::Image::BitmapData src (displayImage, juce::Image::BitmapData::readOnly);
juce::Image::BitmapData dst (presentImage, juce::Image::BitmapData::writeOnly);
for (int py = 0; py < PH; ++py)
for (int px = 0; px < PW; ++px)
{
const float lvl = cells.getPixelColour (cx, cy).getAlpha() / 255.0f;
if (lvl <= 0.02f) continue;
const float ccx = cx * 2.0f + 1.0f;
const float ccy = cy * 2.0f + 1.0f;
const int x0 = cx * 2 - 1, x1 = cx * 2 + 3;
const int y0 = cy * 2 - 1, y1 = cy * 2 + 3;
for (int py = y0; py <= y1; ++py)
unsigned r = 0, g = 0, b = 0, a = 0;
for (int yy = 0; yy < ky; ++yy)
{
if (py < 0 || py >= H) continue;
for (int px = x0; px <= x1; ++px)
const juce::PixelARGB* srcRow =
reinterpret_cast<const juce::PixelARGB*> (src.getPixelPointer (px * kx, py * ky + yy));
for (int xx = 0; xx < kx; ++xx)
{
if (px < 0 || px >= W) continue;
const float dx = (px - ccx) * 0.5f;
const float dy = (py - ccy) * 0.5f;
const float d2 = dx * dx + dy * dy;
if (d2 > glowR2) continue;
float a;
juce::uint8 r, g, b;
if (d2 <= coreR2)
{
a = (0.85f + 0.15f * (1.0f - d2 / coreR2)) * lvl;
r = 0xdf; g = 0xff; b = 0xe6;
}
else
{
a = (1.0f - (d2 - coreR2) / (glowR2 - coreR2)) * 0.16f * lvl;
r = 0x90; g = 0xff; b = 0xa8;
}
if (a <= 0.002f) continue;
const juce::Colour base = out.getPixelColour (px, py);
const float ia = 1.0f - a;
out.setPixelColour (px, py, juce::Colour (
(juce::uint8) juce::jmin (255.0f, (float) base.getRed() * ia + r * a),
(juce::uint8) juce::jmin (255.0f, (float) base.getGreen() * ia + g * a),
(juce::uint8) juce::jmin (255.0f, (float) base.getBlue() * ia + b * a)));
const juce::PixelARGB p = srcRow[xx];
r += p.getRed(); g += p.getGreen();
b += p.getBlue(); a += p.getAlpha();
}
}
juce::PixelARGB* dstRow =
reinterpret_cast<juce::PixelARGB*> (dst.getPixelPointer (px, py));
*dstRow = juce::PixelARGB ((juce::uint8) (a / n), (juce::uint8) (r / n),
(juce::uint8) (g / n), (juce::uint8) (b / n));
}
}
}
}
@ -397,23 +412,31 @@ void LcdPanel::drawContent (juce::Graphics& g)
{
switch (screen)
{
case Screen::Splash: drawSplash (g); break;
case Screen::Home: drawHome (g); break;
case Screen::Menu: drawMenu (g); break;
case Screen::Patch: drawPatchList (g); break;
case Screen::Param: drawParamScreen (g); break;
case Screen::System: drawSystem (g); break;
case Screen::Info: drawInfo (g); break;
case Screen::Splash: drawSplash (g); break;
case Screen::Home: drawHome (g); break;
case Screen::Menu: drawMenu (g); break;
case Screen::Patches: drawPatches (g); break;
case Screen::Patch: drawPatchList (g); break;
case Screen::Param: drawParamScreen (g); break;
case Screen::System: drawSystem (g); break;
case Screen::Info: drawInfo (g); break;
case Screen::Category: drawCategory (g); break; // legacy
}
}
// Stamp one lit segment as a solid full-cell dot at subsample resolution.
// Drawing the whole 4x4 subsample cell solid (instead of an anti-aliased circle)
// makes the 2:1 box-downsample produce a perfectly sharp 2px/dot block at full
// brightness. An AA circle here lands centred exactly on the corner of the four
// output pixels and gets smeared to ~half brightness — that read as soft text.
void LcdPanel::px (juce::Graphics& g, int x, int y, float intensity)
{
if (x < 0 || y < 0 || x >= gridW || y >= gridH) return;
if (intensity <= 0.0f) return;
const int a = (int) (255.0f * juce::jlimit (0.0f, 1.0f, intensity));
g.setColour (juce::Colour::fromRGBA (255, 255, 255, a));
g.fillRect (x, y, 1, 1);
const float s = (float) subsample;
g.setColour (juce::Colour::fromRGBA (
0xa0, 0xff, 0xc0, (juce::uint8) (255.0f * juce::jlimit (0.0f, 1.0f, intensity))));
g.fillRect (x * s, y * s, s, s);
}
void LcdPanel::pxText (juce::Graphics& g, int x, int y, const juce::String& text, float intensity)
@ -500,18 +523,46 @@ void LcdPanel::drawHome (juce::Graphics& g)
const auto& p = proc.getBank().get (patch);
const juce::String num = juce::String (patch + 1).paddedLeft ('0', 2);
pxText (g, 2, 3, num, 1.0f);
pxText (g, 14, 3, p.name, 1.0f);
pxText (g, 2, 3, num + ": " + juce::String (p.name), 1.0f);
pxTextM (g, 4, 17, p.category, 0.9f);
drawFilled (g, 4, 28, 88, 1, 0.5f);
drawHBar (g, 4, 29, 88, proc.getActiveVoices() / 16.0, 0.9f);
// arp indicator on home
if (auto* arpPar = proc.apvts.getRawParameterValue ("arpon"))
{
if (arpPar->load() >= 0.5f)
{
const int rateIdx = (int) proc.apvts.getRawParameterValue ("arprate")->load();
const int patIdx = (int) proc.apvts.getRawParameterValue ("arppattern")->load();
const juce::String arpTxt = "ARP " + juce::String (prm::arpRateName (rateIdx))
+ " " + juce::String (prm::arpPatternName (patIdx));
pxTextM (g, 96, 17, arpTxt, 0.85f);
}
}
// bottom status strip: MIDI input blip + output VU meter
pxTextM (g, 4, 58, "MIDI IN", 0.6f);
drawFilled (g, 35, 60, 2, 3, juce::jmax (0.15f, powerGlow));
pxTextM (g, 50, 58, "OUT", 0.6f);
const int vw = 84;
const int vx = 68;
const int filled = juce::roundToInt (vw * juce::jlimit (0.0f, 1.0f, proc.getOutputLevel()));
if (filled > 0)
{
const int warm = juce::jmin (filled, vw / 2);
drawFilled (g, vx, 60, warm, 2, 0.65f);
drawFilled (g, vx + warm, 60, filled - warm, 2, 1.0f);
drawFilled (g, vx + filled - 1, 62, 1, 1, 0.4f); // leading-edge tick
}
}
void LcdPanel::drawMenu (juce::Graphics& g)
{
static const char* items[] = {
"PATCH", "VOICE", "FILTER", "ENVELOPE", "LFO", "FX", "MASTER", "ABOUT"
"PATCHES", "VOICE", "FILTER", "ENVELOPE", "LFO", "FX", "ARP", "MASTER", "ABOUT"
};
const int rows = (int) juce::numElementsInArray (items);
for (int i = 0; i < rows; ++i)
@ -523,22 +574,67 @@ void LcdPanel::drawMenu (juce::Graphics& g)
}
}
void LcdPanel::drawPatchList (juce::Graphics& g)
void LcdPanel::drawCategory (juce::Graphics& g)
{
const int bankSize = proc.getBank().size();
const auto& cats = proc.getBank().categories();
const int count = (int) cats.names.size() + 1; // +1 = SETTINGS row
const int visible = 8;
const int top = juce::jmax (0, juce::jmin (patchCursor - 3, bankSize - visible));
const int top = juce::jmax (0, juce::jmin (catCursor - 3, count - visible));
for (int i = 0; i < visible; ++i)
{
const int idx = top + i;
if (idx >= bankSize) break;
const auto& p = proc.getBank().get (idx);
if (idx >= count) break;
const int y = 2 + i * 7;
const bool sel = (idx == catCursor);
if (sel) drawSelBar (g, y - 1);
if (idx == (int) cats.names.size())
{
pxTextM (g, 6, y, "SETTINGS", sel ? 1.0f : 0.72f);
drawFilled (g, 6, y - 1, gridW - 12, 1, 0.30f);
continue;
}
pxTextM (g, 6, y, cats.names[(size_t) idx], sel ? 1.0f : 0.72f);
const int pcount = (int) cats.patchIndices[(size_t) idx].size();
pxTextM (g, gridW - 16, y, juce::String (pcount), sel ? 0.85f : 0.55f);
}
}
void LcdPanel::drawPatches (juce::Graphics& g)
{
const auto& cats = proc.getBank().categories();
const int count = (int) cats.names.size();
const int visible = 8;
const int top = juce::jmax (0, juce::jmin (catCursor - 3, count - visible));
for (int i = 0; i < visible; ++i)
{
const int idx = top + i;
if (idx >= count) break;
const int y = 2 + i * 7;
const bool sel = (idx == catCursor);
if (sel) drawSelBar (g, y - 1);
pxTextM (g, 6, y, cats.names[(size_t) idx], sel ? 1.0f : 0.72f);
const int pcount = (int) cats.patchIndices[(size_t) idx].size();
pxTextM (g, gridW - 16, y, juce::String (pcount), sel ? 0.85f : 0.55f);
}
}
void LcdPanel::drawPatchList (juce::Graphics& g)
{
const int count = patchCountForCurrentView();
const int visible = 8;
const int top = juce::jmax (0, juce::jmin (patchCursor - 3, count - visible));
for (int i = 0; i < visible; ++i)
{
const int idx = top + i;
if (idx >= count) break;
const int pi = patchIndexAtCursor (idx);
const auto& p = proc.getBank().get (pi);
const int y = 2 + i * 7;
const bool sel = (idx == patchCursor);
if (sel) drawSelBar (g, y - 1);
pxTextM (g, 6, y, juce::String (idx + 1).paddedLeft ('0', 2) + " " + juce::String (p.name),
sel ? 1.0f : 0.72f);
if (idx == proc.getPatchIndex())
if (pi == proc.getPatchIndex())
pxTextM (g, gridW - 7, y, "*", sel ? 1.0f : 0.9f);
}
}
@ -553,6 +649,7 @@ void LcdPanel::drawParamScreen (juce::Graphics& g)
static const prm::Id envMt[] = { prm::Id::ampA, prm::Id::ampD, prm::Id::ampS, prm::Id::ampR, prm::Id::fenvA, prm::Id::fenvD, prm::Id::fenvS, prm::Id::fenvR };
static const prm::Id lfo[] = { prm::Id::lfoRate, prm::Id::lfoShape, prm::Id::lfoDepth, prm::Id::lfoTarget };
static const prm::Id fx[] = { prm::Id::fxType, prm::Id::fxAmt, prm::Id::fxTime };
static const prm::Id arp[] = { prm::Id::arpOn, prm::Id::arpRate, prm::Id::arpPattern, prm::Id::arpOct, prm::Id::arpGate };
static const prm::Id master[] = { prm::Id::vol, prm::Id::pan, prm::Id::poly, prm::Id::glide, prm::Id::tune, prm::Id::hq };
const prm::Id* rows = nullptr;
@ -564,7 +661,8 @@ void LcdPanel::drawParamScreen (juce::Graphics& g)
case 2: rows = envMt; count = (int) juce::numElementsInArray (envMt); break;
case 3: rows = lfo; count = (int) juce::numElementsInArray (lfo); break;
case 4: rows = fx; count = (int) juce::numElementsInArray (fx); break;
case 5: rows = master; count = (int) juce::numElementsInArray (master); break;
case 5: rows = arp; count = (int) juce::numElementsInArray (arp); break;
case 6: rows = master; count = (int) juce::numElementsInArray (master); break;
default: return;
}
@ -633,8 +731,10 @@ int LcdPanel::rowCountForScreen() const
{
switch (screen)
{
case Screen::Menu: return 8;
case Screen::System: return 4;
case Screen::Menu: return 9;
case Screen::Patches: return proc.getBank().categories().names.size();
case Screen::System: return 4;
case Screen::Category: return proc.getBank().categories().names.size() + 1; // +1 = SETTINGS row
default: return 0;
}
}
@ -647,8 +747,9 @@ int LcdPanel::paramCountForScreen (int idx) const
case 1: return 5;
case 2: return 8;
case 3: return 4;
case 4: return 4;
case 5: return 5;
case 4: return 4; // 3 FX params + SYNC row
case 5: return 5; // ARP: on, rate, pattern, oct, gate
case 6: return 6;
default: return 0;
}
}
@ -662,33 +763,56 @@ const char* LcdPanel::paramScreenTitle (int idx) const
case 2: return "ENVELOPE";
case 3: return "LFO";
case 4: return "FX";
case 5: return "MASTER";
case 5: return "ARP";
case 6: return "MASTER";
default: return "";
}
}
void LcdPanel::openParamScreen (int idx)
{
paramScreenIdx = juce::jlimit (0, 5, idx);
paramScreenIdx = juce::jlimit (0, 6, idx);
cursor = 0;
enterScreen (Screen::Param);
}
// cyclic cursor step: wrap around the list instead of clamping at the ends
static int cyclicStep (int cur, int count, int dir)
{
if (count <= 0) return 0;
int n = (cur + dir) % count;
if (n < 0) n += count;
return n;
}
void LcdPanel::moveCursor (int d)
{
if (screen == Screen::Patch)
if (screen == Screen::Patches)
{
patchCursor = juce::jlimit (0, proc.getBank().size() - 1, patchCursor + d);
const int count = proc.getBank().categories().names.size();
catCursor = cyclicStep (catCursor, count, d);
setDirty();
}
else if (screen == Screen::Category)
{
const int count = proc.getBank().categories().names.size() + 1; // +1 = SETTINGS row
catCursor = cyclicStep (catCursor, count, d);
setDirty();
}
else if (screen == Screen::Patch)
{
const int count = patchCountForCurrentView();
patchCursor = cyclicStep (patchCursor, count, d);
setDirty();
}
else if (screen == Screen::Param)
{
cursor = juce::jlimit (0, paramCountForScreen (paramScreenIdx) - 1, cursor + d);
cursor = cyclicStep (cursor, paramCountForScreen (paramScreenIdx), d);
setDirty();
}
else if (rowCountForScreen() > 0)
{
cursor = juce::jlimit (0, rowCountForScreen() - 1, cursor + d);
cursor = cyclicStep (cursor, rowCountForScreen(), d);
setDirty();
}
}
@ -723,6 +847,7 @@ static prm::Id paramForCursor (int screenIdx, int row)
static const prm::Id envMt[] = { prm::Id::ampA, prm::Id::ampD, prm::Id::ampS, prm::Id::ampR, prm::Id::fenvA, prm::Id::fenvD, prm::Id::fenvS, prm::Id::fenvR };
static const prm::Id lfo[] = { prm::Id::lfoRate, prm::Id::lfoShape, prm::Id::lfoDepth, prm::Id::lfoTarget };
static const prm::Id fx[] = { prm::Id::fxType, prm::Id::fxAmt, prm::Id::fxTime };
static const prm::Id arp[] = { prm::Id::arpOn, prm::Id::arpRate, prm::Id::arpPattern, prm::Id::arpOct, prm::Id::arpGate };
static const prm::Id master[] = { prm::Id::vol, prm::Id::pan, prm::Id::poly, prm::Id::glide, prm::Id::tune, prm::Id::hq };
switch (screenIdx)
@ -732,7 +857,8 @@ static prm::Id paramForCursor (int screenIdx, int row)
case 2: return envMt[row];
case 3: return lfo[row];
case 4: return fx[row];
case 5: return master[row];
case 5: return arp[row];
case 6: return master[row];
default: return prm::Id::vol;
}
}
@ -766,36 +892,83 @@ void LcdPanel::applyPatchOnBrowserMove()
{
if (screen == Screen::Patch)
{
proc.selectPatch (patchCursor);
const int pi = patchIndexAtCursor (patchCursor);
if (pi >= 0 && pi < proc.getBank().size())
proc.selectPatch (pi);
setDirty();
}
}
void LcdPanel::navLeft() { adjustCurrent (-1, false); }
void LcdPanel::navRight() { adjustCurrent (1, false); }
void LcdPanel::openPatchBrowser()
{
const auto& cats = proc.getBank().categories();
int start = 0;
const int cur = proc.getPatchIndex();
for (size_t c = 0; c < cats.patchIndices.size(); ++c)
{
bool found = false;
for (int pi : cats.patchIndices[(size_t) c])
if (pi == cur) { found = true; break; }
if (found) { start = (int) c; break; }
}
catCursor = start;
enterScreen (Screen::Patches);
}
void LcdPanel::navEnter()
{
switch (screen)
{
case Screen::Splash: enterScreen (Screen::Home); break;
case Screen::Home: enterScreen (Screen::Menu); break;
case Screen::Home:
enterScreen (Screen::Menu);
break;
case Screen::Menu:
switch (cursor)
{
case 0: enterScreen (Screen::Patch); break;
case 0: openPatchBrowser(); break;
case 1: openParamScreen (0); break;
case 2: openParamScreen (1); break;
case 3: openParamScreen (2); break;
case 4: openParamScreen (3); break;
case 5: openParamScreen (4); break;
case 6: openParamScreen (5); break;
case 7: enterScreen (Screen::Info); break;
case 7: openParamScreen (6); break;
case 8: enterScreen (Screen::Info); break;
default: break;
}
break;
case Screen::Patches:
{
const auto& cats = proc.getBank().categories();
if (catCursor >= 0 && catCursor < (int) cats.names.size())
{
buildFilteredPatches (catCursor);
patchCursor = 0;
enterScreen (Screen::Patch);
}
break;
}
case Screen::Category:
{
const auto& cats = proc.getBank().categories();
if (catCursor >= 0 && catCursor < (int) cats.names.size())
{
buildFilteredPatches (catCursor);
patchCursor = 0;
enterScreen (Screen::Patch);
}
else if (catCursor == (int) cats.names.size())
{
enterScreen (Screen::Menu); // SETTINGS row -> main menu
}
break;
}
case Screen::Patch:
proc.selectPatch (patchCursor);
enterScreen (Screen::Menu);
proc.selectPatch (patchIndexAtCursor (patchCursor));
enterScreen (Screen::Home); // back to the main screen showing the new patch
break;
case Screen::System:
switch (cursor)
@ -820,7 +993,13 @@ void LcdPanel::navEnter()
void LcdPanel::navExit()
{
if (screen == Screen::Menu)
if (screen == Screen::Patch)
enterScreen (Screen::Patches);
else if (screen == Screen::Patches)
enterScreen (Screen::Menu);
else if (screen == Screen::Category)
enterScreen (Screen::Menu); // legacy: go to main menu
else if (screen == Screen::Menu)
enterScreen (Screen::Home);
else if (screen != Screen::Home)
enterScreen (Screen::Menu);
@ -838,3 +1017,30 @@ bool LcdPanel::keyTrap (const juce::KeyPress& k)
{ navExit(); return true; }
return false;
}
int LcdPanel::patchCountForCurrentView() const
{
if (! filteredPatches.empty())
return (int) filteredPatches.size();
return proc.getBank().size();
}
int LcdPanel::patchIndexAtCursor (int viewIdx) const
{
if (viewIdx < 0) return 0;
if (! filteredPatches.empty())
{
if (viewIdx < (int) filteredPatches.size())
return filteredPatches[(size_t) viewIdx];
return filteredPatches.empty() ? 0 : filteredPatches.back();
}
return viewIdx;
}
void LcdPanel::buildFilteredPatches (int catIdx)
{
filteredPatches.clear();
const auto& cats = proc.getBank().categories();
if (catIdx >= 0 && catIdx < cats.names.size())
filteredPatches = cats.patchIndices[(size_t) catIdx];
}