338 lines
14 KiB
C++
338 lines
14 KiB
C++
#include "UIManager.h"
|
|
#include "config.h"
|
|
#include "SharedState.h"
|
|
|
|
// --- HARDWARE CONFIGURATION ---
|
|
#define SCREEN_WIDTH 128
|
|
#define SCREEN_HEIGHT 64
|
|
#define OLED_RESET -1
|
|
#define SCREEN_ADDRESS 0x3C
|
|
|
|
UIManager ui;
|
|
|
|
UIManager::UIManager()
|
|
: display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET)
|
|
{
|
|
}
|
|
|
|
void UIManager::begin() {
|
|
// Setup Display
|
|
Wire.begin();
|
|
if(!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
|
|
Serial.println(F("SSD1306 allocation failed"));
|
|
for(;;);
|
|
}
|
|
display.clearDisplay();
|
|
display.display();
|
|
|
|
// Setup NeoPixel Matrix
|
|
ledMatrix.begin();
|
|
}
|
|
|
|
void UIManager::showMessage(const char* msg) {
|
|
display.clearDisplay();
|
|
display.setCursor(10, 25);
|
|
display.setTextColor(SSD1306_WHITE);
|
|
display.setTextSize(2);
|
|
display.print(msg);
|
|
display.display();
|
|
delay(500);
|
|
display.setTextSize(1);
|
|
}
|
|
|
|
void UIManager::draw(UIState currentState, int menuSelection,
|
|
int midiChannel, int tempo, MelodyStrategy* currentStrategy,
|
|
int queuedTheme, int currentThemeIndex,
|
|
int numScaleNotes, const int* scaleNotes, int melodySeed, int currentTrackNumSteps,
|
|
bool mutationEnabled, bool songModeEnabled,
|
|
const Step sequence[][NUM_STEPS], int playbackStep, bool isPlaying,
|
|
int randomizeTrack, const bool* trackMute, const int* trackIntensities) {
|
|
|
|
display.clearDisplay();
|
|
display.setTextSize(1);
|
|
display.setTextColor(SSD1306_WHITE);
|
|
display.setCursor(0, 0);
|
|
|
|
switch(currentState) {
|
|
case UI_MENU_MAIN:
|
|
drawMenu(menuSelection, currentState, midiChannel, tempo, currentStrategy->getName(), queuedTheme, currentThemeIndex, numScaleNotes, scaleNotes, melodySeed, currentTrackNumSteps, mutationEnabled, songModeEnabled, isPlaying, randomizeTrack, trackMute, trackIntensities);
|
|
break;
|
|
case UI_SETUP_CHANNEL_EDIT:
|
|
drawEditScreen("SET MIDI CHANNEL", "CH: ", midiChannel);
|
|
break;
|
|
case UI_EDIT_TEMPO:
|
|
drawEditScreen("SET TEMPO", "BPM: ", tempo);
|
|
break;
|
|
case UI_EDIT_STEPS:
|
|
drawEditScreen("SET STEPS", "LEN: ", currentTrackNumSteps);
|
|
break;
|
|
case UI_EDIT_FLAVOUR:
|
|
drawEditScreen("SET FLAVOUR", "", currentStrategy->getName());
|
|
break;
|
|
case UI_EDIT_ROOT:
|
|
{
|
|
const char* noteNames[] = {"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"};
|
|
drawEditScreen("SET ROOT NOTE", "", noteNames[currentRoot % 12]);
|
|
}
|
|
break;
|
|
case UI_EDIT_SCALE_TYPE:
|
|
display.println(F("ENABLED SCALES"));
|
|
display.drawLine(0, 8, 128, 8, SSD1306_WHITE);
|
|
display.setCursor(0, 20);
|
|
if (menuSelection < 10) {
|
|
const char* typeNames[] = {"Chrom", "Major", "Minor", "H.Min", "P.Maj", "P.Min", "ChdMaj", "ChdMin", "ChdDim", "Chd7"};
|
|
display.setTextSize(2);
|
|
display.print(typeNames[menuSelection]);
|
|
display.setTextSize(1);
|
|
display.setCursor(0, 45);
|
|
bool isEnabled = (enabledScaleTypes & (1 << menuSelection));
|
|
display.print(isEnabled ? F("[ENABLED]") : F("[DISABLED]"));
|
|
} else {
|
|
display.setTextSize(2);
|
|
display.print(F("Back"));
|
|
}
|
|
display.setTextSize(1);
|
|
display.setCursor(0, 55);
|
|
display.println(F(" (Press to toggle)"));
|
|
break;
|
|
case UI_EDIT_INTENSITY:
|
|
drawNumberEditor("SET INTENSITY", trackIntensities[randomizeTrack], 1, 10);
|
|
break;
|
|
case UI_RANDOMIZE_TRACK_EDIT:
|
|
drawEditScreen("SET TRACK", "TRK: ", randomizeTrack + 1);
|
|
break;
|
|
case UI_SCALE_EDIT:
|
|
case UI_SCALE_NOTE_EDIT:
|
|
case UI_SCALE_TRANSPOSE:
|
|
display.println(F("EDIT SCALE"));
|
|
display.drawLine(0, 8, 128, 8, SSD1306_WHITE);
|
|
|
|
int totalItems = numScaleNotes + 5; // Back + Randomize + Notes + Add + Remove + Transpose
|
|
int startIdx = 0;
|
|
if (menuSelection >= 4) startIdx = menuSelection - 3;
|
|
|
|
int y = 12;
|
|
for (int i = startIdx; i < totalItems; i++) {
|
|
if (y > 54) break;
|
|
|
|
if (i == menuSelection) {
|
|
display.fillRect(0, y, 75, 9, SSD1306_WHITE);
|
|
display.setTextColor(SSD1306_BLACK, SSD1306_WHITE);
|
|
} else {
|
|
display.setTextColor(SSD1306_WHITE);
|
|
}
|
|
display.setCursor(2, y + 1);
|
|
|
|
if (i == 0) {
|
|
display.print(F("Back"));
|
|
} else if (i == 1) {
|
|
display.print(F("Randomize"));
|
|
} else if (i <= numScaleNotes + 1) {
|
|
int noteIdx = i - 2;
|
|
const char* noteNames[] = {"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"};
|
|
display.print(noteNames[scaleNotes[noteIdx]]);
|
|
if (currentState == UI_SCALE_NOTE_EDIT && i == menuSelection) {
|
|
display.print(F(" <"));
|
|
}
|
|
} else if (i == numScaleNotes + 2) {
|
|
display.print(F("Transpose"));
|
|
if (currentState == UI_SCALE_TRANSPOSE) {
|
|
display.print(F(" < >"));
|
|
}
|
|
} else if (i == numScaleNotes + 3) {
|
|
display.print(F("Add Note"));
|
|
} else if (i == numScaleNotes + 4) {
|
|
display.print(F("Remove Note"));
|
|
}
|
|
y += 9;
|
|
}
|
|
|
|
// Piano Roll Preview
|
|
int px = 82;
|
|
int py = 20;
|
|
int wk_w = 5;
|
|
int wk_h = 20;
|
|
int bk_w = 4;
|
|
int bk_h = 12;
|
|
|
|
// White keys: C, D, E, F, G, A, B
|
|
int whiteNotes[] = {0, 2, 4, 5, 7, 9, 11};
|
|
for (int k = 0; k < 7; k++) {
|
|
bool active = false;
|
|
for (int j = 0; j < numScaleNotes; j++) {
|
|
if (scaleNotes[j] == whiteNotes[k]) { active = true; break; }
|
|
}
|
|
if (active) display.fillRect(px + k*6, py, wk_w, wk_h, SSD1306_WHITE);
|
|
else display.drawRect(px + k*6, py, wk_w, wk_h, SSD1306_WHITE);
|
|
}
|
|
|
|
// Black keys: C#, D#, F#, G#, A#
|
|
int blackNotes[] = {1, 3, 6, 8, 10};
|
|
int blackOffsets[] = {3, 9, 21, 27, 33};
|
|
for (int k = 0; k < 5; k++) {
|
|
bool active = false;
|
|
for (int j = 0; j < numScaleNotes; j++) {
|
|
if (scaleNotes[j] == blackNotes[k]) { active = true; break; }
|
|
}
|
|
int bx = px + blackOffsets[k];
|
|
display.fillRect(bx - 1, py - 1, bk_w + 2, bk_h + 2, SSD1306_BLACK);
|
|
if (active) display.fillRect(bx, py, bk_w, bk_h, SSD1306_WHITE);
|
|
else display.drawRect(bx, py, bk_w, bk_h, SSD1306_WHITE);
|
|
}
|
|
break;
|
|
}
|
|
display.display();
|
|
}
|
|
|
|
void UIManager::drawNumberEditor(const char* title, int value, int minVal, int maxVal) {
|
|
display.println(title);
|
|
display.drawLine(0, 8, 128, 8, SSD1306_WHITE);
|
|
|
|
// Display value
|
|
display.setCursor(20, 20);
|
|
display.setTextSize(2);
|
|
display.print(value);
|
|
|
|
// Graphical bar
|
|
int barWidth = 100;
|
|
int barX = (SCREEN_WIDTH - barWidth) / 2;
|
|
int barY = 40;
|
|
int barHeight = 10;
|
|
float percentage = (float)(value - minVal) / (maxVal - minVal);
|
|
int fillWidth = (int)(percentage * barWidth);
|
|
|
|
display.drawRect(barX, barY, barWidth, barHeight, SSD1306_WHITE);
|
|
display.fillRect(barX, barY, fillWidth, barHeight, SSD1306_WHITE);
|
|
|
|
display.setTextSize(1);
|
|
display.setCursor(0, 54);
|
|
display.println(F(" (Press to confirm)"));
|
|
}
|
|
|
|
void UIManager::drawEditScreen(const char* title, const char* label, const char* valueStr) {
|
|
display.println(title);
|
|
display.drawLine(0, 8, 128, 8, SSD1306_WHITE);
|
|
display.setCursor(20, 25);
|
|
display.setTextSize(2);
|
|
if (label && *label) display.print(label);
|
|
display.print(valueStr);
|
|
display.setTextSize(1);
|
|
display.setCursor(0, 50);
|
|
display.println(F(" (Press to confirm)"));
|
|
}
|
|
|
|
void UIManager::drawEditScreen(const char* title, const char* label, int value) {
|
|
char buf[16];
|
|
itoa(value, buf, 10);
|
|
drawEditScreen(title, label, buf);
|
|
}
|
|
|
|
void UIManager::drawMenu(int selection, UIState currentState, int midiChannel, int tempo, const char* flavourName,
|
|
int queuedTheme, int currentThemeIndex, int numScaleNotes,
|
|
const int* scaleNotes, int melodySeed, int currentTrackNumSteps, bool mutationEnabled,
|
|
bool songModeEnabled, bool isPlaying, int randomizeTrack, const bool* trackMute, const int* trackIntensities) {
|
|
|
|
// Calculate visual cursor position and scroll offset
|
|
int visualCursor = 0;
|
|
for(int i=0; i<selection; i++) {
|
|
if(isItemVisible(i)) visualCursor++;
|
|
}
|
|
|
|
const int MAX_LINES = 7; // No title, so we have more space
|
|
int startVisualIndex = 0;
|
|
if (visualCursor >= MAX_LINES) {
|
|
startVisualIndex = visualCursor - (MAX_LINES - 1);
|
|
}
|
|
|
|
int currentVisualIndex = 0;
|
|
int y = 0;
|
|
|
|
for (int i = 0; i < menuItemsCount; i++) {
|
|
if (!isItemVisible(i)) continue;
|
|
|
|
if (currentVisualIndex >= startVisualIndex) {
|
|
if (y > 55) break;
|
|
|
|
if (i == selection) {
|
|
display.fillRect(0, y, 128, 9, SSD1306_WHITE);
|
|
display.setTextColor(SSD1306_BLACK, SSD1306_WHITE);
|
|
} else {
|
|
display.setTextColor(SSD1306_WHITE);
|
|
}
|
|
|
|
int x = 2 + (menuItems[i].indentLevel * 6);
|
|
display.setCursor(x, y + 1);
|
|
|
|
if (menuItems[i].isGroup) {
|
|
display.print(menuItems[i].expanded ? F("v ") : F("> "));
|
|
}
|
|
|
|
display.print(menuItems[i].label);
|
|
|
|
MenuItemID id = menuItems[i].id;
|
|
|
|
if (id == MENU_ID_CHANNEL) {
|
|
display.print(F(": ")); display.print(midiChannel);
|
|
}
|
|
|
|
// Dynamic values
|
|
if (id == MENU_ID_PLAYBACK) { display.print(F(": ")); display.print(isPlaying ? F("ON") : F("OFF")); }
|
|
else if (id == MENU_ID_MELODY) {
|
|
display.print(F(": ")); display.print(melodySeed);
|
|
} else if (id == MENU_ID_SCALE) {
|
|
display.print(F(": "));
|
|
if (numScaleNotes > 0) {
|
|
const char* noteNames[] = {"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"};
|
|
for (int j = 0; j < min(numScaleNotes, 6); j++) {
|
|
display.print(noteNames[scaleNotes[j]]);
|
|
if (j < min(numScaleNotes, 6) - 1) display.print(F(" "));
|
|
}
|
|
}
|
|
} else if (id == MENU_ID_TEMPO) { display.print(F(": ")); display.print(tempo); }
|
|
else if (id == MENU_ID_ROOT) {
|
|
const char* noteNames[] = {"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"};
|
|
display.print(F(": ")); display.print(noteNames[currentRoot % 12]);
|
|
}
|
|
else if (id == MENU_ID_SCALE_TYPE) {
|
|
const char* typeNames[] = {"Chrom", "Major", "Minor", "H.Min", "P.Maj", "P.Min", "ChdMaj", "ChdMin", "ChdDim", "Chd7"};
|
|
display.print(F(": ")); if (currentScaleType >= 0 && currentScaleType < 10) display.print(typeNames[currentScaleType]);
|
|
}
|
|
else if (id == MENU_ID_STEPS) { display.print(F(": ")); display.print(currentTrackNumSteps); }
|
|
else if (id == MENU_ID_SONG_MODE) { display.print(F(": ")); display.print(songModeEnabled ? F("ON") : F("OFF")); }
|
|
else if (id == MENU_ID_TRACK_SELECT) { display.print(F(": ")); display.print(randomizeTrack + 1); }
|
|
else if (id == MENU_ID_MUTE) { display.print(F(": ")); display.print(trackMute[randomizeTrack] ? F("YES") : F("NO")); }
|
|
else if (id == MENU_ID_FLAVOUR) { display.print(F(": ")); display.print(flavourName); }
|
|
else if (id == MENU_ID_INTENSITY) {
|
|
display.print(F(": "));
|
|
display.print(trackIntensities[randomizeTrack]);
|
|
int val = trackIntensities[randomizeTrack];
|
|
int barX = display.getCursorX() + 3;
|
|
int barY = y + 2;
|
|
int maxW = 20;
|
|
int h = 5;
|
|
uint16_t color = (i == selection) ? SSD1306_BLACK : SSD1306_WHITE;
|
|
display.drawRect(barX, barY, maxW + 2, h, color);
|
|
display.fillRect(barX + 1, barY + 1, (val * maxW) / 10, h - 2, color);
|
|
}
|
|
else if (id == MENU_ID_MUTATION) { display.print(F(": ")); display.print(mutationEnabled ? F("ON") : F("OFF")); }
|
|
else if (id == MENU_ID_PROTECTED_MODE) { display.print(F(": ")); display.print(protectedMode ? F("ON") : F("OFF")); }
|
|
|
|
if (id >= MENU_ID_THEME_1 && id <= MENU_ID_THEME_7) {
|
|
int themeIdx = id - MENU_ID_THEME_1 + 1;
|
|
if (queuedTheme == themeIdx) display.print(F(" [NEXT]"));
|
|
if (currentThemeIndex == themeIdx) display.print(F(" *"));
|
|
}
|
|
|
|
y += 9;
|
|
}
|
|
|
|
currentVisualIndex++;
|
|
}
|
|
}
|
|
|
|
void UIManager::updateLeds(const Step sequence[][NUM_STEPS], int playbackStep, bool isPlaying,
|
|
UIState currentState, bool songModeEnabled,
|
|
int songRepeatsRemaining, bool sequenceChangeScheduled, PlayMode playMode,
|
|
int selectedTrack, const int* numSteps, int numScaleNotes, const int* scaleNotes, const bool* trackMute) {
|
|
ledMatrix.update(sequence, playbackStep, isPlaying, currentState, songModeEnabled, songRepeatsRemaining, sequenceChangeScheduled, playMode, selectedTrack, numSteps, trackMute);
|
|
} |