added red label for duplicate cubes in mapping + list of unused cubes
[SugarCubes.git] / _Overlay.pde
index e21c9cdff0f5f4bd222ace65c4d2b770e7680edd..2623e8276fb668135acc5a4f039fc0a8cb92e35f 100644 (file)
+import java.lang.reflect.*;
+
 /**
+ *     DOUBLE BLACK DIAMOND        DOUBLE BLACK DIAMOND
+ *
+ *         //\\   //\\                 //\\   //\\  
+ *        ///\\\ ///\\\               ///\\\ ///\\\
+ *        \\\/// \\\///               \\\/// \\\///
+ *         \\//   \\//                 \\//   \\//
+ *
+ *        EXPERTS ONLY!!              EXPERTS ONLY!!
+ *
  * Overlay UI that indicates pattern control, etc. This will be moved
  * into the Processing library once it is stabilized and need not be
  * regularly modified.
  */
-class OverlayUI {
-  
-  private final PFont titleFont = createFont("Myriad Pro", 10);
-  private final PFont itemFont = createFont("Lucida Grande", 11);
-  private final PFont knobFont = titleFont;
-  private final int w = 140;
-  private final int leftPos;
-  private final int leftTextPos;
-  private final int lineHeight = 20;
-  private final int sectionSpacing = 12;
-  private final int controlSpacing = 18;
-  private final int tempoHeight = 20;
-  private final int knobSize = 28;
-  private final float knobIndent = .4;  
-  private final int knobSpacing = 6;
-  private final int knobLabelHeight = 14;
-  private final color lightBlue = #666699;
-  private final color lightGreen = #669966;
+abstract class OverlayUI {
+  protected final PFont titleFont = createFont("Myriad Pro", 10);
+  protected final color titleColor = #AAAAAA;
+  protected final PFont itemFont = createFont("Lucida Grande", 11);
+  protected final PFont knobFont = titleFont;
+  protected final int w = 140;
+  protected final int leftPos;
+  protected final int leftTextPos;
+  protected final int lineHeight = 20;
+  protected final int sectionSpacing = 12;
+  protected final int controlSpacing = 18;
+  protected final int tempoHeight = 20;
+  protected final int knobSize = 28;
+  protected final float knobIndent = .4;  
+  protected final int knobSpacing = 6;
+  protected final int knobLabelHeight = 14;
+  protected final int scrollWidth = 14;
+  protected final color lightBlue = #666699;
+  protected final color lightGreen = #669966;
+  protected final int toggleButtonSize = 10;
+  
+  private PImage logo;
+
+  protected final int STATE_DEFAULT = 0;
+  protected final int STATE_ACTIVE = 1;
+  protected final int STATE_PENDING = 2;
+  
+  protected int[] pandaLeft = new int[pandaBoards.length];
+  protected final int pandaWidth = 64;
+  protected final int pandaHeight = 13;
+  protected final int pandaTop = height-16;
+  
+  protected int eligibleLeft;
+  
+  protected OverlayUI() {
+    leftPos = width - w;
+    leftTextPos = leftPos + 4;
+    logo = loadImage("logo-sm.png");
+  }
+  
+  protected void drawLogoAndBackground() {
+    image(logo, 4, 4);
+    stroke(color(0, 0, 100));
+    // fill(color(0, 0, 50, 50)); // alpha is bad for perf
+    fill(color(0, 0, 30));
+    rect(leftPos-1, -1, w+2, height+2);
+  }
+  
+  protected void drawToggleTip(String s) {
+    fill(#999999);
+    textFont(itemFont);
+    textAlign(LEFT);
+    text(s, leftTextPos, height-6);
+  }
+  
+  protected void drawHelpTip() {
+    textFont(itemFont);
+    textAlign(RIGHT);
+    text("Tap 'u' to restore UI", width-4, height-6);
+  }
+
+  public void drawDanText() {
+    textFont(itemFont);
+    textAlign(LEFT);
+    fill(#FFFFFF);
+    text(DanTextLine1, 4, height-50);
+    text(DanTextLine2, 4, height-30);
+  }
+
+  public void drawFPS() {
+    textFont(titleFont);
+    textAlign(LEFT);
+    fill(#666666);
+    int lPos = 4;
+    String fps = "FPS: " + (((int)(frameRate * 10)) / 10.);
+    text(fps, lPos, height-6);
+    lPos += 48;
+    
+    String target = "Target (-/+):";
+    text(target, lPos, height-6);
+    fill(#000000);
+    lPos += textWidth(target) + 4;
+    rect(lPos, height-16, 24, 13);
+    fill(#666666);
+    text("" + targetFramerate, lPos + (24 - textWidth("" + targetFramerate))/2, height-6);
+    lPos += 32;
+    String pandaOutput = "PandaOutput (p):";
+    text(pandaOutput, lPos, height-6);
+    lPos += textWidth(pandaOutput)+4;
+    int pi = 0;
+    for (PandaDriver p : pandaBoards) {
+      pandaLeft[pi++] = lPos;
+      fill(p.enabled ? #666666 : #000000);
+      rect(lPos, pandaTop, pandaWidth, pandaHeight);
+      fill(p.enabled ? #000000 : #666666);
+      text(p.ip, lPos + (pandaWidth - textWidth(p.ip)) / 2, height-6);
+      lPos += pandaWidth + 8;
+    }
+
+  }
+
+  protected int drawObjectList(int yPos, String title, Object[] items, Method stateMethod) {
+    int sz = (items != null) ? items.length : 0;
+    return drawObjectList(yPos, title, items, stateMethod, sz, 0);
+  }
+
+  protected int drawObjectList(int yPos, String title, Object[] items, Method stateMethod, int scrollLength, int scrollPos) {
+    return drawObjectList(yPos, title, items, classNameArray(items, null), stateMethod, scrollLength, scrollPos);
+  }
+
+  protected int drawObjectList(int yPos, String title, Object[] items, String[] names, Method stateMethod) {
+    int sz = (items != null) ? items.length : 0;
+    return drawObjectList(yPos, title, items, names, stateMethod, sz, 0);
+  }
+  
+  protected void drawToggleButton(float x, float y, boolean eligible, color textColor) {
+    noFill();
+    stroke(textColor);
+    rect(x, y, toggleButtonSize, toggleButtonSize);
+    if (eligible) {
+      noStroke();
+      fill(textColor);
+      rect(x + 2, y + 2, toggleButtonSize - 4, toggleButtonSize - 4);
+    }
+  }    
+  
+  protected int drawObjectList(int yPos, String title, Object[] items, String[] names, Method stateMethod, int scrollLength, int scrollPos) {
+    noStroke();
+    fill(titleColor);
+    textFont(titleFont);
+    textAlign(LEFT);
+    text(title, leftTextPos, yPos += lineHeight);    
+    if (items != null) {
+      boolean hasScroll = (scrollPos > 0) || (scrollLength < items.length);
+      textFont(itemFont);
+      color textColor;      
+      boolean even = true;
+      int yTop = yPos+6;
+      for (int i = scrollPos; i < items.length && i < (scrollPos + scrollLength); ++i) {
+        Object o = items[i];
+        int state = STATE_DEFAULT;
+        try {
+           state = ((Integer) stateMethod.invoke(this, o)).intValue();
+        } catch (Exception x) {
+          throw new RuntimeException(x);
+        }
+        switch (state) {
+          case STATE_ACTIVE:
+            fill(lightGreen);
+            textColor = #eeeeee;
+            break;
+          case STATE_PENDING:
+            fill(lightBlue);
+            textColor = color(0, 0, 75 + 15*sin(millis()/200.));;
+            break;
+          default:
+            textColor = 0;
+            fill(even ? #666666 : #777777);
+            break;
+        }
+        noStroke();
+        rect(leftPos, yPos+6, w, lineHeight);
+        fill(textColor);
+        text(names[i], leftTextPos, yPos += lineHeight);
+        if (lx.isAutoTransitionEnabled() && items[i] instanceof LXPattern) {
+          boolean eligible = ((LXPattern)items[i]).isEligible();
+          eligibleLeft = leftPos + w - (hasScroll ? scrollWidth : 0) - 15;
+          drawToggleButton(eligibleLeft, yPos-8, eligible, textColor);
+        }
+        even = !even;       
+      }
+      if (hasScroll) {
+        int yHere = yPos+6;
+        noStroke();
+        fill(color(0, 0, 0, 50));
+        rect(leftPos + w - scrollWidth, yTop, scrollWidth, yHere - yTop);
+        fill(#666666);
+        rect(leftPos + w - scrollWidth + 2, yTop + (yHere-yTop) * (scrollPos / (float)items.length), scrollWidth - 4, (yHere - yTop) * (scrollLength / (float)items.length));
+        
+      }
+      
+    }
+    return yPos;
+  }
+  
+  protected String[] classNameArray(Object[] objects, String suffix) {
+    if (objects == null) {
+      return null;
+    }
+    String[] names = new String[objects.length];
+    for (int i = 0; i < objects.length; ++i) {
+      names[i] = className(objects[i], suffix);
+    }
+    return names;
+  }
   
+  protected String className(Object p, String suffix) {
+    String s = p.getClass().getName();
+    int li;
+    if ((li = s.lastIndexOf(".")) > 0) {
+      s = s.substring(li + 1);
+    }
+    if (s.indexOf("SugarCubes$") == 0) {
+      s = s.substring("SugarCubes$".length());
+    }
+    if ((suffix != null) && ((li = s.indexOf(suffix)) != -1)) {
+      s = s.substring(0, li);
+    }
+    return s;
+  }  
+  
+  protected int objectClickIndex(int firstItemY) {
+    return (mouseY - firstItemY) / lineHeight;
+  }
+  
+  abstract public void draw();  
+  abstract public void mousePressed();
+  abstract public void mouseDragged();
+  abstract public void mouseReleased();
+  abstract public void mouseWheel(int delta);
+}
+
+/**
+ * UI for control of patterns, transitions, effects.
+ */
+class ControlUI extends OverlayUI {  
   private final String[] patternNames;
   private final String[] transitionNames;
   private final String[] effectNames;
-
-  private PImage logo;
   
   private int firstPatternY;
   private int firstPatternKnobY;
@@ -34,41 +250,24 @@ class OverlayUI {
   private int firstTransitionKnobY;
   private int firstEffectY;
   private int firstEffectKnobY;
+  
+  private int autoRotateX;
+  private int autoRotateY;
+  
+  private final int PATTERN_LIST_LENGTH = 8;
+  private int patternScrollPos = 0;
 
   private int tempoY;
   
   private Method patternStateMethod;
   private Method transitionStateMethod;
   private Method effectStateMethod;
-  
-  private final int NUM_TRANSITION_KNOBS = 4;
-  private final int NUM_EFFECT_KNOBS = 4;
-  
-  private int activeTransitionIndex = 0;
-  private int activeEffectIndex = 0;
-  
-  private final VirtualTransitionKnob[] transitionKnobs;
-  private final VirtualEffectKnob[] effectKnobs;
-    
-  OverlayUI() {
-    leftPos = width - w;
-    leftTextPos = leftPos + 4;
-    logo = loadImage("logo-sm.png");
-    
+
+  ControlUI() {    
     patternNames = classNameArray(patterns, "Pattern");
     transitionNames = classNameArray(transitions, "Transition");
     effectNames = classNameArray(effects, "Effect");
 
-    transitionKnobs = new VirtualTransitionKnob[NUM_TRANSITION_KNOBS];
-    for (int i = 0; i < transitionKnobs.length; ++i) {
-      transitionKnobs[i] = new VirtualTransitionKnob(i);
-    }
-
-    effectKnobs = new VirtualEffectKnob[NUM_EFFECT_KNOBS];
-    for (int i = 0; i < effectKnobs.length; ++i) {
-      effectKnobs[i] = new VirtualEffectKnob(i);
-    }
-
     try {
       patternStateMethod = getClass().getMethod("getState", LXPattern.class);
       effectStateMethod = getClass().getMethod("getState", LXEffect.class);
@@ -77,30 +276,23 @@ class OverlayUI {
       throw new RuntimeException(x);
     }    
   }
-  
-  void drawHelpTip() {
-    textFont(itemFont);
-    textAlign(RIGHT);
-    text("Tap 'u' to restore UI", width-4, height-6);
-  }
-  
-  void draw() {    
-    image(logo, 4, 4);
-    
-    stroke(color(0, 0, 100));
-    // fill(color(0, 0, 50, 50)); // alpha is bad for perf
-    fill(color(0, 0, 30));
-    rect(leftPos-1, -1, w+2, height+2);
-    
-    int yPos = 0;    
+      
+  public void draw() {    
+    drawLogoAndBackground();
+    int yPos = 0;
+    autoRotateX = leftPos + w - 29;
+    autoRotateY = yPos + 12;
+    drawToggleButton(autoRotateX, autoRotateY, lx.isAutoTransitionEnabled(), #999999);
+    fill(lx.isAutoTransitionEnabled() ? #222222: #999999);
+    text("A", autoRotateX + 2, autoRotateY + 9);
     firstPatternY = yPos + lineHeight + 6;
-    yPos = drawObjectList(yPos, "PATTERN", patterns, patternNames, patternStateMethod);
+    yPos = drawObjectList(yPos, "PATTERN", patterns, patternNames, patternStateMethod, PATTERN_LIST_LENGTH, patternScrollPos);
     yPos += controlSpacing;
     firstPatternKnobY = yPos;
     int xPos = leftTextPos;
     for (int i = 0; i < glucose.NUM_PATTERN_KNOBS/2; ++i) {
-      drawKnob(xPos, yPos, knobSize, glucose.patternKnobs[i]);
-      drawKnob(xPos, yPos + knobSize + knobSpacing + knobLabelHeight, knobSize, glucose.patternKnobs[glucose.NUM_PATTERN_KNOBS/2 + i]);
+      drawKnob(xPos, yPos, knobSize, glucose.patternKnobs.get(i));
+      drawKnob(xPos, yPos + knobSize + knobSpacing + knobLabelHeight, knobSize, glucose.patternKnobs.get(glucose.NUM_PATTERN_KNOBS/2 + i));
       xPos += knobSize + knobSpacing;
     }
     yPos += 2*(knobSize + knobLabelHeight) + knobSpacing;
@@ -111,8 +303,8 @@ class OverlayUI {
     yPos += controlSpacing;
     firstTransitionKnobY = yPos;
     xPos = leftTextPos;
-    for (int i = 0; i < transitionKnobs.length; ++i) {
-      drawKnob(xPos, yPos, knobSize, transitionKnobs[i]);
+    for (VirtualTransitionKnob knob : glucose.transitionKnobs) {
+      drawKnob(xPos, yPos, knobSize, knob);
       xPos += knobSize + knobSpacing;
     }
     yPos += knobSize + knobLabelHeight;
@@ -123,8 +315,8 @@ class OverlayUI {
     yPos += controlSpacing;
     firstEffectKnobY = yPos;    
     xPos = leftTextPos;
-    for (int i = 0; i < effectKnobs.length; ++i) {
-      drawKnob(xPos, yPos, knobSize, effectKnobs[i]);
+    for (VirtualEffectKnob knob : glucose.effectKnobs) {    
+      drawKnob(xPos, yPos, knobSize, knob);
       xPos += knobSize + knobSpacing;
     }
     yPos += knobSize + knobLabelHeight;
@@ -141,10 +333,7 @@ class OverlayUI {
     text("" + ((int)(lx.tempo.bpmf() * 100) / 100.), leftPos + w/2., yPos + tempoHeight - 6);
     yPos += tempoHeight;
     
-    fill(#999999);
-    textFont(itemFont);
-    textAlign(LEFT);
-    text("Tap 'u' to hide UI (~+3FPS)", leftTextPos, height-6);
+    drawToggleTip("Tap 'u' to hide");
   }
   
   public LXParameter getOrNull(List<LXParameter> items, int index) {
@@ -154,17 +343,6 @@ class OverlayUI {
     return null;
   }
   
-  public void drawFPS() {
-    textFont(titleFont);
-    textAlign(LEFT);
-    fill(#666666);
-    text("FPS: " + (((int)(frameRate * 10)) / 10.), 4, height-6);     
-  }
-
-  private final int STATE_DEFAULT = 0;
-  private final int STATE_ACTIVE = 1;
-  private final int STATE_PENDING = 2;
-
   public int getState(LXPattern p) {
     if (p == lx.getPattern()) {
       return STATE_ACTIVE;
@@ -177,7 +355,7 @@ class OverlayUI {
   public int getState(LXEffect e) {
     if (e.isEnabled()) {
       return STATE_PENDING;
-    } else if (effects[activeEffectIndex] == e) {
+    } else if (e == glucose.getSelectedEffect()) {
       return STATE_ACTIVE;
     }
     return STATE_DEFAULT;
@@ -186,61 +364,13 @@ class OverlayUI {
   public int getState(LXTransition t) {
     if (t == lx.getTransition()) {
       return STATE_PENDING;
-    } else if (t == transitions[activeTransitionIndex]) {
+    } else if (t == glucose.getSelectedTransition()) {
       return STATE_ACTIVE;
     }
     return STATE_DEFAULT;
   }
-
-  protected int drawObjectList(int yPos, String title, Object[] items, Method stateMethod) {
-    return drawObjectList(yPos, title, items, classNameArray(items, null), stateMethod);
-  }
-  
-  private int drawObjectList(int yPos, String title, Object[] items, String[] names, Method stateMethod) {
-    noStroke();
-    fill(#aaaaaa);
-    textFont(titleFont);
-    textAlign(LEFT);
-    text(title, leftTextPos, yPos += lineHeight);    
-    if (items != null) {
-      textFont(itemFont);
-      color textColor;      
-      boolean even = true;
-      for (int i = 0; i < items.length; ++i) {
-        Object o = items[i];
-        int state = STATE_DEFAULT;
-        try {
-           state = ((Integer) stateMethod.invoke(this, o)).intValue();
-        } catch (Exception x) {
-          throw new RuntimeException(x);
-        }
-        switch (state) {
-          case STATE_ACTIVE:
-            fill(lightGreen);
-            textColor = #eeeeee;
-            break;
-          case STATE_PENDING:
-            fill(lightBlue);
-            textColor = color(0, 0, 75 + 15*sin(millis()/200.));;
-            break;
-          default:
-            textColor = 0;
-            fill(even ? #666666 : #777777);
-            break;
-        }
-        rect(leftPos, yPos+6, width, lineHeight);
-        fill(textColor);
-        text(names[i], leftTextPos, yPos += lineHeight);
-        even = !even;       
-      }
-    }
-    return yPos;
-  }
   
   private void drawKnob(int xPos, int yPos, int knobSize, LXParameter knob) {
-    if (!knobsOn) {
-      return;
-    }
     final float knobValue = knob.getValuef();
     String knobLabel = knob.getLabel();
     if (knobLabel == null) {
@@ -250,6 +380,7 @@ class OverlayUI {
     }
     
     ellipseMode(CENTER);
+    noStroke();
     fill(#222222);
     // For some reason this arc call really crushes drawing performance. Presumably
     // because openGL is drawing it and when we overlap the second set of arcs it
@@ -261,18 +392,17 @@ class OverlayUI {
     fill(lightGreen);
     arc(xPos + knobSize/2, yPos + knobSize/2, knobSize, knobSize, HALF_PI + knobIndent, endArc);
     
-    // Center circle of knob
-    fill(#333333);
-    ellipse(xPos + knobSize/2, yPos + knobSize/2, knobSize/2, knobSize/2);
-
     // Mask notch out of knob
     fill(color(0, 0, 30));
     beginShape();
-    vertex(xPos + knobSize/2 - 3, yPos + knobSize - 8);
-    vertex(xPos + knobSize/2 - 5, yPos + knobSize);
-    vertex(xPos + knobSize/2 + 5, yPos + knobSize);
-    vertex(xPos + knobSize/2 + 3, yPos + knobSize - 8);
+    vertex(xPos + knobSize/2, yPos + knobSize/2.);
+    vertex(xPos + knobSize/2 - 6, yPos + knobSize);
+    vertex(xPos + knobSize/2 + 6, yPos + knobSize);
     endShape();
+
+    // Center circle of knob
+    fill(#333333);
+    ellipse(xPos + knobSize/2, yPos + knobSize/2, knobSize/2, knobSize/2);    
     
     fill(0);
     rect(xPos, yPos + knobSize + 2, knobSize, knobLabelHeight - 2);
@@ -280,70 +410,12 @@ class OverlayUI {
     textAlign(CENTER);
     textFont(knobFont);
     text(knobLabel, xPos + knobSize/2, yPos + knobSize + knobLabelHeight - 2);
-
-  }
-  
-  private String[] classNameArray(Object[] objects, String suffix) {
-    if (objects == null) {
-      return null;
-    }
-    String[] names = new String[objects.length];
-    for (int i = 0; i < objects.length; ++i) {
-      names[i] = className(objects[i], suffix);
-    }
-    return names;
-  }
-  
-  private String className(Object p, String suffix) {
-    String s = p.getClass().getName();
-    int li;
-    if ((li = s.lastIndexOf(".")) > 0) {
-      s = s.substring(li + 1);
-    }
-    if (s.indexOf("SugarCubes$") == 0) {
-      s = s.substring("SugarCubes$".length());
-    }
-    if ((suffix != null) && ((li = s.indexOf(suffix)) != -1)) {
-      s = s.substring(0, li);
-    }
-    return s;
-  }
-
-  class VirtualTransitionKnob extends LXVirtualParameter {
-    private final int index;
-    
-    VirtualTransitionKnob(int index) {
-      this.index = index;
-    }
-    
-    public LXParameter getRealParameter() {
-      List<LXParameter> parameters = transitions[activeTransitionIndex].getParameters();
-      if (index < parameters.size()) {
-        return parameters.get(index);
-      }
-      return null;
-    }
-  }
-
-  class VirtualEffectKnob extends LXVirtualParameter {
-    private final int index;
-    
-    VirtualEffectKnob(int index) {
-      this.index = index;
-    }
-    
-    public LXParameter getRealParameter() {
-      List<LXParameter> parameters = effects[activeEffectIndex].getParameters();
-      if (index < parameters.size()) {
-        return parameters.get(index);
-      }
-      return null;
-    }
   }
   
   private int patternKnobIndex = -1;
   private int transitionKnobIndex = -1;
   private int effectKnobIndex = -1;
+  private boolean patternScrolling = false;
   
   private int lastY;
   private int releaseEffect = -1;
@@ -353,6 +425,36 @@ class OverlayUI {
     lastY = mouseY;
     patternKnobIndex = transitionKnobIndex = effectKnobIndex = -1;
     releaseEffect = -1;
+    patternScrolling = false;
+    
+    for (int p = 0; p < pandaLeft.length; ++p) {
+      int xp = pandaLeft[p];
+      if ((mouseX >= xp) &&
+          (mouseX < xp + pandaWidth) &&
+          (mouseY >= pandaTop) &&
+          (mouseY < pandaTop + pandaHeight)) {
+          pandaBoards[p].toggle();
+      }
+    }
+    
+    if (mouseX < leftPos) {
+      return;
+    }
+    
+    if ((mouseX >= autoRotateX) &&
+        (mouseX < autoRotateX + toggleButtonSize) &&
+        (mouseY >= autoRotateY) &&
+        (mouseY < autoRotateY + toggleButtonSize)) {
+      if (lx.isAutoTransitionEnabled()) {
+        lx.disableAutoTransition();
+        println("Auto pattern transition disabled");
+      } else {
+        lx.enableAutoTransition(60000);
+        println("Auto pattern transition enabled");        
+      }
+      return;
+    }
+    
     if (mouseY > tempoY) {
       if (mouseY - tempoY < tempoHeight) {
         lx.tempo.tap();
@@ -361,20 +463,20 @@ class OverlayUI {
     } else if ((mouseY >= firstEffectKnobY) && (mouseY < firstEffectKnobY + knobSize + knobLabelHeight)) {
       effectKnobIndex = (mouseX - leftTextPos) / (knobSize + knobSpacing);
     } else if (mouseY > firstEffectY) {
-      int effectIndex = (mouseY - firstEffectY) / lineHeight;
+      int effectIndex = objectClickIndex(firstEffectY);
       if (effectIndex < effects.length) {
-        if (activeEffectIndex == effectIndex) {
+        if (effects[effectIndex] == glucose.getSelectedEffect()) {
           effects[effectIndex].enable();
           releaseEffect = effectIndex;
         }
-        activeEffectIndex = effectIndex;        
+        glucose.setSelectedEffect(effectIndex);
       }
     } else if ((mouseY >= firstTransitionKnobY) && (mouseY < firstTransitionKnobY + knobSize + knobLabelHeight)) {
       transitionKnobIndex = (mouseX - leftTextPos) / (knobSize + knobSpacing);
     } else if (mouseY > firstTransitionY) {
-      int transitionIndex = (mouseY - firstTransitionY) / lineHeight;
+      int transitionIndex = objectClickIndex(firstTransitionY);
       if (transitionIndex < transitions.length) {
-        activeTransitionIndex = transitionIndex;
+        glucose.setSelectedTransition(transitionIndex);
       }
     } else if ((mouseY >= firstPatternKnobY) && (mouseY < firstPatternKnobY + 2*(knobSize+knobLabelHeight) + knobSpacing)) {
       patternKnobIndex = (mouseX - leftTextPos) / (knobSize + knobSpacing);
@@ -382,30 +484,44 @@ class OverlayUI {
         patternKnobIndex += glucose.NUM_PATTERN_KNOBS / 2;
       }      
     } else if (mouseY > firstPatternY) {
-      int patternIndex = (mouseY - firstPatternY) / lineHeight;
-      if (patternIndex < patterns.length) {
-        patterns[patternIndex].setTransition(transitions[activeTransitionIndex]);
-        lx.goIndex(patternIndex);
+      if ((patterns.length > PATTERN_LIST_LENGTH) && (mouseX > width - scrollWidth)) {
+        patternScrolling = true;
+      } else {
+        int patternIndex = objectClickIndex(firstPatternY);
+        if (patternIndex < patterns.length) {
+          if (lx.isAutoTransitionEnabled() && (mouseX > eligibleLeft)) {
+            patterns[patternIndex + patternScrollPos].toggleEligible();
+          } else { 
+            lx.goIndex(patternIndex + patternScrollPos);
+          }
+        }
       }
     }
   }
   
+  int scrolldy = 0;
   public void mouseDragged() {
     int dy = lastY - mouseY;
+    scrolldy += dy;
     lastY = mouseY;
     if (patternKnobIndex >= 0 && patternKnobIndex < glucose.NUM_PATTERN_KNOBS) {
-      LXParameter p = glucose.patternKnobs[patternKnobIndex];
+      LXParameter p = glucose.patternKnobs.get(patternKnobIndex);
       p.setValue(constrain(p.getValuef() + dy*.01, 0, 1));
-    } else if (effectKnobIndex >= 0 && effectKnobIndex < NUM_EFFECT_KNOBS) {
-      LXParameter p = effectKnobs[effectKnobIndex];
+    } else if (effectKnobIndex >= 0 && effectKnobIndex < glucose.NUM_EFFECT_KNOBS) {
+      LXParameter p = glucose.effectKnobs.get(effectKnobIndex);
       p.setValue(constrain(p.getValuef() + dy*.01, 0, 1));
-    } else if (transitionKnobIndex >= 0 && transitionKnobIndex < NUM_TRANSITION_KNOBS) {
-      LXParameter p = transitionKnobs[transitionKnobIndex];
+    } else if (transitionKnobIndex >= 0 && transitionKnobIndex < glucose.NUM_TRANSITION_KNOBS) {
+      LXParameter p = glucose.transitionKnobs.get(transitionKnobIndex);
       p.setValue(constrain(p.getValuef() + dy*.01, 0, 1));
+    } else if (patternScrolling) {
+      int scroll = scrolldy / lineHeight;
+      scrolldy = scrolldy % lineHeight;
+      patternScrollPos = constrain(patternScrollPos - scroll, 0, patterns.length - PATTERN_LIST_LENGTH);
     }
   }
     
   public void mouseReleased() {
+    patternScrolling = false;
     tempoDown = false;
     if (releaseEffect >= 0) {
       effects[releaseEffect].trigger();
@@ -413,23 +529,497 @@ class OverlayUI {
     }
   }
   
+  public void mouseWheel(int delta) {
+    if (mouseY > firstPatternY) {
+      int patternIndex = objectClickIndex(firstPatternY);
+      if (patternIndex < PATTERN_LIST_LENGTH) {
+        patternScrollPos = constrain(patternScrollPos + delta, 0, patterns.length - PATTERN_LIST_LENGTH);
+      }
+    }
+  }
+  
 }
 
-void mousePressed() {
-  if (mouseX > ui.leftPos) {
-    ui.mousePressed();
+/**
+ * UI for control of mapping.
+ */
+class MappingUI extends OverlayUI {
+  
+  private MappingTool mappingTool;
+  
+  private final String MAPPING_MODE_ALL = "All On";
+  private final String MAPPING_MODE_CHANNEL = "Channel";
+  private final String MAPPING_MODE_SINGLE_CUBE = "Single Cube";
+  
+  private final String[] mappingModes = {
+    MAPPING_MODE_ALL,
+    MAPPING_MODE_CHANNEL,
+    MAPPING_MODE_SINGLE_CUBE
+  };
+  private final Method mappingModeStateMethod;
+  
+  private final String CUBE_MODE_ALL = "All Strips";
+  private final String CUBE_MODE_SINGLE_STRIP = "Single Strip";
+  private final String CUBE_MODE_STRIP_PATTERN = "Strip Pattern";
+  private final String[] cubeModes = {
+    CUBE_MODE_ALL,
+    CUBE_MODE_SINGLE_STRIP,
+    CUBE_MODE_STRIP_PATTERN
+  };
+  private final Method cubeModeStateMethod;  
+
+  private final String CHANNEL_MODE_RED = "Red";
+  private final String CHANNEL_MODE_GREEN = "Green";
+  private final String CHANNEL_MODE_BLUE = "Blue";
+  private final String[] channelModes = {
+    CHANNEL_MODE_RED,
+    CHANNEL_MODE_GREEN,
+    CHANNEL_MODE_BLUE,    
+  };
+  private final Method channelModeStateMethod;
+  
+  private int firstMappingY;
+  private int firstCubeY;
+  private int firstChannelY;
+  private int channelFieldY;
+  private int cubeFieldY;
+  private int stripFieldY;
+  
+  private boolean dragCube;
+  private boolean dragStrip;
+  private boolean dragChannel;
+
+  MappingUI(MappingTool mappingTool) {
+    this.mappingTool = mappingTool;
+    try {
+      mappingModeStateMethod = getClass().getMethod("getMappingState", Object.class);
+      channelModeStateMethod = getClass().getMethod("getChannelState", Object.class);
+      cubeModeStateMethod = getClass().getMethod("getCubeState", Object.class);
+    } catch (Exception x) {
+      throw new RuntimeException(x);
+    }    
   }
-}
+  
+  public int getMappingState(Object mappingMode) {
+    boolean active = false;
+    if (mappingMode == MAPPING_MODE_ALL) {
+      active = mappingTool.mappingMode == mappingTool.MAPPING_MODE_ALL;
+    } else if (mappingMode == MAPPING_MODE_CHANNEL) {
+      active = mappingTool.mappingMode == mappingTool.MAPPING_MODE_CHANNEL;
+    } else if (mappingMode == MAPPING_MODE_SINGLE_CUBE) {
+      active = mappingTool.mappingMode == mappingTool.MAPPING_MODE_SINGLE_CUBE;
+    }
+    return active ? STATE_ACTIVE : STATE_DEFAULT;
+  }
+  
+  public int getChannelState(Object channelMode) {
+    boolean active = false;
+    if (channelMode == CHANNEL_MODE_RED) {
+      active = mappingTool.channelModeRed;
+    } else if (channelMode == CHANNEL_MODE_GREEN) {
+      active = mappingTool.channelModeGreen;
+    } else if (channelMode == CHANNEL_MODE_BLUE) {
+      active = mappingTool.channelModeBlue;
+    }
+    return active ? STATE_ACTIVE : STATE_DEFAULT;
+  }
+  
+  public int getCubeState(Object cubeMode) {
+    boolean active = false;
+    if (cubeMode == CUBE_MODE_ALL) {
+      active = mappingTool.cubeMode == mappingTool.CUBE_MODE_ALL;
+    } else if (cubeMode == CUBE_MODE_SINGLE_STRIP) {
+      active = mappingTool.cubeMode == mappingTool.CUBE_MODE_SINGLE_STRIP;
+    } else if (cubeMode == CUBE_MODE_STRIP_PATTERN) {
+      active = mappingTool.cubeMode == mappingTool.CUBE_MODE_STRIP_PATTERN;
+    }
+    return active ? STATE_ACTIVE : STATE_DEFAULT;
+  }
+  
+  public void draw() {
+    drawLogoAndBackground();
+    
+    int yPos = 0;
+    firstMappingY = yPos + lineHeight + 6;    
+    yPos = drawObjectList(yPos, "MAPPING MODE", mappingModes, mappingModes, mappingModeStateMethod);
+    yPos += sectionSpacing;
+
+    firstCubeY = yPos + lineHeight + 6;    
+    yPos = drawObjectList(yPos, "CUBE MODE", cubeModes, cubeModes, cubeModeStateMethod);
+    yPos += sectionSpacing;
 
-void mouseReleased() {
-  if (mouseX > ui.leftPos) {
-    ui.mouseReleased();
+    firstChannelY = yPos + lineHeight + 6;    
+    yPos = drawObjectList(yPos, "CHANNELS", channelModes, channelModes, channelModeStateMethod);    
+    yPos += sectionSpacing;
+    
+    channelFieldY = yPos + lineHeight + 6;
+    yPos = drawValueField(yPos, "CHANNEL ID", mappingTool.channelIndex + 1);
+    yPos += sectionSpacing;
+
+    cubeFieldY = yPos + lineHeight + 6;
+    yPos = drawValueField(yPos, "CUBE ID", glucose.model.getRawIndexForCube(mappingTool.cubeIndex));
+    yPos += sectionSpacing;
+
+    stripFieldY = yPos + lineHeight + 6;
+    yPos = drawValueField(yPos, "STRIP ID", mappingTool.stripIndex + 1);
+    
+    drawToggleTip("Tap 'm' to return");    
+  }
+  
+  private int drawValueField(int yPos, String label, int value) {
+    yPos += lineHeight;
+    textAlign(LEFT);
+    textFont(titleFont);
+    fill(titleColor);
+    text(label, leftTextPos, yPos);
+    fill(0);
+    yPos += 6;
+    rect(leftTextPos, yPos, w-8, lineHeight);
+    yPos += lineHeight;
+
+    fill(#999999);
+    textAlign(CENTER);
+    textFont(itemFont);
+    text("" + value, leftTextPos + (w-8)/2, yPos - 5);
+    
+    return yPos;    
   }
-}
 
-void mouseDragged() {
-  if (mouseX > ui.leftPos) {
-    ui.mouseDragged();
+  private int lastY;
+  
+  public void mousePressed() {
+    dragCube = dragStrip = dragChannel = false;
+    lastY = mouseY;
+    
+    if (mouseX < leftPos) {
+      return;
+    }
+    
+    if (mouseY >= stripFieldY) {
+      if (mouseY < stripFieldY + lineHeight) {
+        dragStrip = true;
+      }
+    } else if (mouseY >= cubeFieldY) {
+      if (mouseY < cubeFieldY + lineHeight) {
+        dragCube = true;
+      }
+    } else if (mouseY >= channelFieldY) {
+      if (mouseY < channelFieldY + lineHeight) {
+        dragChannel = true;
+      }
+    } else if (mouseY >= firstChannelY) {
+      int index = objectClickIndex(firstChannelY);
+      switch (index) {
+        case 0: mappingTool.channelModeRed = !mappingTool.channelModeRed; break;
+        case 1: mappingTool.channelModeGreen = !mappingTool.channelModeGreen; break;
+        case 2: mappingTool.channelModeBlue = !mappingTool.channelModeBlue; break;
+      }
+    } else if (mouseY >= firstCubeY) {
+      int index = objectClickIndex(firstCubeY);
+      switch (index) {
+        case 0: mappingTool.cubeMode = mappingTool.CUBE_MODE_ALL; break;
+        case 1: mappingTool.cubeMode = mappingTool.CUBE_MODE_SINGLE_STRIP; break;
+        case 2: mappingTool.cubeMode = mappingTool.CUBE_MODE_STRIP_PATTERN; break;
+      }
+    } else if (mouseY >= firstMappingY) {
+      int index = objectClickIndex(firstMappingY);
+      switch (index) {
+        case 0: mappingTool.mappingMode = mappingTool.MAPPING_MODE_ALL; break;
+        case 1: mappingTool.mappingMode = mappingTool.MAPPING_MODE_CHANNEL; break;
+        case 2: mappingTool.mappingMode = mappingTool.MAPPING_MODE_SINGLE_CUBE; break;
+      }
+    }
+  }
+
+  public void mouseReleased() {}
+  public void mouseWheel(int delta) {}
+
+  public void mouseDragged() {
+    final int DRAG_THRESHOLD = 5;
+    int dy = lastY - mouseY;
+    if (abs(dy) >= DRAG_THRESHOLD) {
+      lastY = mouseY;
+      if (dragCube) {
+        if (dy < 0) {
+          mappingTool.decCube();
+        } else {
+          mappingTool.incCube();
+        }
+      } else if (dragStrip) {
+        if (dy < 0) {
+          mappingTool.decStrip();
+        } else {
+          mappingTool.incStrip();
+        }
+      } else if (dragChannel) {
+        if (dy < 0) {
+          mappingTool.decChannel();
+        } else {
+          mappingTool.incChannel();
+        }
+      }
+    }
+    
   }
 }
 
+class DebugUI {
+  
+  final ChannelMapping[] channelList;
+  final int debugX = 5;
+  final int debugY = 5;
+  final int debugXSpacing = 28;
+  final int debugYSpacing = 21;
+  final int[][] debugState;
+  final int[] indexState;
+  
+  final int ERROR_STATE_USED = 0;
+  final int ERROR_STATE_DUPLICATED = 1;
+  
+  final int CUBE_STATE_UNUSED = 0;
+  final int CUBE_STATE_USED = 1;
+  
+  final int DEBUG_STATE_ANIM = 0;
+  final int DEBUG_STATE_WHITE = 1;
+  final int DEBUG_STATE_OFF = 2;
+  
+  DebugUI(PandaMapping[] pandaMappings) {
+    int totalChannels = pandaMappings.length * PandaMapping.CHANNELS_PER_BOARD;
+    debugState = new int[totalChannels+1][ChannelMapping.CUBES_PER_CHANNEL+1];
+    indexState = new int[glucose.model.cubes.size()+1];
+    
+    channelList = new ChannelMapping[totalChannels];
+    int channelIndex = 0;
+    for (PandaMapping pm : pandaMappings) {
+      for (ChannelMapping channel : pm.channelList) {
+        channelList[channelIndex++] = channel;
+      }
+    }
+    for (int i = 0; i < debugState.length; ++i) {
+      for (int j = 0; j < debugState[i].length; ++j) {
+        debugState[i][j] = DEBUG_STATE_ANIM;
+      }
+    }
+    
+    for (int rawIndex = 0; rawIndex < glucose.model.cubes.size()+1; rawIndex++) {
+      indexState[rawIndex] = CUBE_STATE_UNUSED;
+    }
+    for (ChannelMapping channel : channelList) {
+      for (int rawCubeIndex : channel.objectIndices) {
+        if (rawCubeIndex > 0)
+          indexState[rawCubeIndex]++;
+      }
+    }
+  }
+  
+  void draw() {    
+    noStroke();
+    int xBase = debugX;
+    int yPos = debugY;
+    
+    fill(#000000);
+    rect(0, 0, debugX + 5*debugXSpacing, height);
+    
+    int channelNum = 0;
+    for (ChannelMapping channel : channelList) {
+      int xPos = xBase;
+      drawNumBox(xPos, yPos, channelNum+1, debugState[channelNum][0]);
+      xPos += debugXSpacing;
+      
+      switch (channel.mode) {
+        case ChannelMapping.MODE_CUBES:
+          int stateIndex = 0;
+          boolean first = true;
+          for (int rawCubeIndex : channel.objectIndices) {
+            if (rawCubeIndex < 0) {
+              break;
+            }
+            if (first) {
+              first = false;
+            } else {
+              stroke(#999999);          
+              line(xPos - 12, yPos + 8, xPos, yPos + 8);
+            }
+            drawNumBox(xPos, yPos, rawCubeIndex, debugState[channelNum][stateIndex+1], indexState[rawCubeIndex]);
+            ++stateIndex;
+            xPos += debugXSpacing;            
+          }
+          break;
+        case ChannelMapping.MODE_BASS:
+          drawNumBox(xPos, yPos, "B", debugState[channelNum][1]);
+          break;
+        case ChannelMapping.MODE_SPEAKER:
+          drawNumBox(xPos, yPos, "S" + channel.objectIndices[0], debugState[channelNum][1]);
+          break;
+        case ChannelMapping.MODE_STRUTS_AND_FLOOR:
+          drawNumBox(xPos, yPos, "F", debugState[channelNum][1]);
+          break;
+        case ChannelMapping.MODE_NULL:
+          break;
+        default:
+          throw new RuntimeException("Unhandled channel mapping mode: " + channel.mode);
+      }          
+      
+      yPos += debugYSpacing;
+      ++channelNum;
+    }
+    drawNumBox(xBase, yPos, "A", debugState[channelNum][0]);
+    yPos += debugYSpacing * 2;
+   
+    noFill();
+    stroke(#CCCCCC);
+    rect(xBase, yPos, 100, 16);
+    fill(#CCCCCC);
+    text("Unused Cubes",  xBase + 5, yPos + 12);
+    yPos += debugYSpacing;
+    
+    int x_index = 0;
+    for (int rawIndex = 1; rawIndex < glucose.model.cubes.size()+1; rawIndex++) {
+      if (indexState[rawIndex] == 0) {
+        drawNumBox(xBase + (x_index * debugXSpacing), yPos, rawIndex, 0, 2);
+        x_index++;
+        if (x_index > 4) {
+          x_index = 0;
+          yPos += debugYSpacing + 2;
+        }
+      }
+    }
+  }
+
+  
+  void drawNumBox(int xPos, int yPos, int label, int state) {
+    drawNumBox(xPos, yPos, "" + label, state);
+  }
+  
+  void drawNumBox(int xPos, int yPos, int label, int state, int cube_state) {
+    if (cube_state > 1) {
+      fill(#FF0000);
+      rect(xPos-2, yPos-2, 20, 20);
+    }
+    drawNumBox(xPos, yPos, label, state);
+  }
+  
+  void drawNumBox(int xPos, int yPos, String label, int state) {
+    noFill();
+    color textColor = #cccccc;
+    switch (state) {
+      case DEBUG_STATE_ANIM:
+        noStroke();
+        fill(#880000);
+        rect(xPos, yPos, 16, 8);
+        fill(#000088);
+        rect(xPos, yPos+8, 16, 8);
+        noFill();
+        stroke(textColor);
+        rect(xPos, yPos, 16, 16); 
+        break;
+      case DEBUG_STATE_WHITE:
+        stroke(textColor);
+        fill(#e9e9e9);
+        rect(xPos, yPos, 16, 16);
+        textColor = #333333;
+        break;
+      case DEBUG_STATE_OFF:
+        stroke(textColor);
+        rect(xPos, yPos, 16, 16);
+        break;
+    }
+    
+    noStroke();
+    fill(textColor);
+    text(label, xPos + 2, yPos + 12);
+  
+  }
+  
+  void maskColors(color[] colors) {
+    color white = #FFFFFF;
+    color off = #000000;
+    int channelIndex = 0;
+    int state;
+    for (ChannelMapping channel : channelList) {
+      switch (channel.mode) {
+        case ChannelMapping.MODE_CUBES:
+          int cubeIndex = 1;
+          for (int rawCubeIndex : channel.objectIndices) {
+            if (rawCubeIndex >= 0) {
+              state = debugState[channelIndex][cubeIndex];
+              if (state != DEBUG_STATE_ANIM) {
+                color debugColor = (state == DEBUG_STATE_WHITE) ? white : off;
+                Cube cube = glucose.model.getCubeByRawIndex(rawCubeIndex);
+                for (Point p : cube.points) {
+                  colors[p.index] = debugColor;
+                }
+              }
+            }
+            ++cubeIndex;
+          }
+          break;
+            
+         case ChannelMapping.MODE_BASS:
+           state = debugState[channelIndex][1];
+           if (state != DEBUG_STATE_ANIM) {
+              color debugColor = (state == DEBUG_STATE_WHITE) ? white : off;
+              for (Strip s : glucose.model.bassBox.boxStrips) {
+                for (Point p : s.points) {
+                  colors[p.index] = debugColor;
+                }
+              }
+           }
+           break;
+
+         case ChannelMapping.MODE_STRUTS_AND_FLOOR:
+           state = debugState[channelIndex][1];
+           if (state != DEBUG_STATE_ANIM) {
+              color debugColor = (state == DEBUG_STATE_WHITE) ? white : off;
+              for (Point p : glucose.model.boothFloor.points) {
+                colors[p.index] = debugColor;
+              }
+              for (Strip s : glucose.model.bassBox.struts) {
+                for (Point p : s.points) {
+                  colors[p.index] = debugColor;
+                }
+              }
+           }
+           break;
+           
+         case ChannelMapping.MODE_SPEAKER:
+           state = debugState[channelIndex][1];
+           if (state != DEBUG_STATE_ANIM) {
+              color debugColor = (state == DEBUG_STATE_WHITE) ? white : off;
+              for (Point p : glucose.model.speakers.get(channel.objectIndices[0]).points) {
+                colors[p.index] = debugColor;
+              }
+           }
+           break;
+           
+         case ChannelMapping.MODE_NULL:
+           break;
+           
+        default:
+          throw new RuntimeException("Unhandled channel mapping mode: " + channel.mode);           
+      }
+      ++channelIndex;
+    }
+  }
+  
+  void mousePressed() {
+    int dx = (mouseX - debugX) / debugXSpacing;
+    int dy = (mouseY - debugY) / debugYSpacing;
+    if ((dy >= 0) && (dy < debugState.length)) {
+      if ((dx >= 0) && (dx < debugState[dy].length)) {
+        int newState = debugState[dy][dx] = (debugState[dy][dx] + 1) % 3;
+        if (dy == debugState.length-1) {
+          for (int[] states : debugState) {
+            for (int i = 0; i < states.length; ++i) {
+              states[i] = newState;
+            }
+          }
+        } else if (dx == 0) {
+          for (int i = 0; i < debugState[dy].length; ++i) {
+            debugState[dy][i] = newState;
+          }
+        }
+      }
+    }
+  }    
+}