Overhaul UI code to make it more flexible and less redrawing, little hierarchy framework
authorMark Slee <mcslee@Mark-Slees-MacBook-Pro.local>
Tue, 17 Sep 2013 01:07:06 +0000 (18:07 -0700)
committerMark Slee <mcslee@Mark-Slees-MacBook-Pro.local>
Thu, 19 Sep 2013 07:00:34 +0000 (00:00 -0700)
13 files changed:
DanKaminsky.pde
DanUtil.pde [changed mode: 0755->0644]
MarkSlee.pde [changed mode: 0755->0644]
SugarCubes.pde
TestPatterns.pde [changed mode: 0755->0644]
_DebugUI.pde [new file with mode: 0644]
_Internals.pde
_Overlay.pde [deleted file]
_PandaDriver.pde
_UIFramework.pde [new file with mode: 0644]
_UIImplementation.pde [new file with mode: 0644]
code/GLucose.jar
code/HeronLX.jar

index 59bde733721a99feaacfc1f495b482896a6bdd2c..3fbd86b30ca753d56f98ca5c2f1872b344414a11 100644 (file)
@@ -215,7 +215,7 @@ import processing.serial.*;
 
 List<LXParameter> gparams;
 
-class DualBlender extends SCEffect {
+  class DualBlender extends SCEffect {
   int lastSeen;
   BasicParameter p1 = new BasicParameter("p1", 0);
   BasicParameter p2 = new BasicParameter("p2", 0);  
@@ -250,9 +250,9 @@ class DualBlender extends SCEffect {
     if(p==p7) { gparams.get(6).setValue(p.getValuef()); }
     if(p==p8) { gparams.get(7).setValue(p.getValuef()); }    
   }
-  
+    
   void doApply(int[] colors){
-    if(doDual==true){
+    if (enabled) {
       //gplay.onActive();
       gplay.go(millis()-lastSeen);
       lastSeen=millis();
old mode 100755 (executable)
new mode 100644 (file)
index d9990dd..9577673
@@ -80,7 +80,7 @@ public class DPat extends SCPattern
        int             unmapRow (int a)                                        { return btwn(a,0 , 4) ? a+53 : a;                      }
        void            SetLight (int row, int col, int clr){ if (midiout != null) midiout.sendNoteOn(col, unmapRow(row), clr); }
        void            keypad   (int row, int col)                     { println(row + " " + col); }
-       void            onInactive()                                            { bIsActive=false; DanTextLine1 = ""; DanTextLine2 = "";}
+       void            onInactive()                                            { bIsActive=false; DanTextLine1 = ""; DanTextLine2 = ""; uiDebugText.setText(""); }
        void            onActive  ()                                            { bIsActive=true;
                zSpinHue = 0;
                for (int i=0; i<paramlist.size(); i++) ((_DhP)paramlist.get(i)).reset();
@@ -105,6 +105,8 @@ public class DPat extends SCPattern
 
                DanTextLine2  = "SLIDERS: ";
                for (int i=0; i<8; i++) if (SliderText[i] != "") { DanTextLine2 += SliderText[i] + ": " + Sliders[i] + "     "; }
+
+               uiDebugText.setText(DanTextLine1, DanTextLine2);
        }
 
        void    run(int deltaMs) {
@@ -259,4 +261,4 @@ public class DPat extends SCPattern
                return P;
        }
 }
-//----------------------------------------------------------------------------------------------------------------------------------\r
+//----------------------------------------------------------------------------------------------------------------------------------
old mode 100755 (executable)
new mode 100644 (file)
index a67f719..adc86d0
@@ -720,6 +720,9 @@ class ColorFuckerEffect extends SCEffect {
   }
   
   public void doApply(int[] colors) {
+    if (!enabled) {
+      return;
+    }
     float bMod = bright.getValuef();
     float sMod = sat.getValuef();
     float hMod = hueShift.getValuef();
index 76d0390d28f3a3fc453350ee76f6d933d28a9460..ec68668b854b8f1aa5525e38553ef01ad6cdb84e 100644 (file)
@@ -119,8 +119,8 @@ LXEffect[] effects(GLucose glucose) {
   return new LXEffect[] {
     new FlashEffect(lx),
     new BoomEffect(glucose),
-    // new DesaturationEffect(lx),
-    // new ColorFuckerEffect(glucose),
     new DualBlender(glucose),
+    // new DesaturationEffect(lx),
+    new ColorFuckerEffect(glucose),
   };
 }
old mode 100755 (executable)
new mode 100644 (file)
index b2e49f1..b566e33
@@ -315,6 +315,10 @@ class MappingTool extends TestPattern {
     numChannels = pandaMappings.length * PandaMapping.CHANNELS_PER_BOARD;
     setChannel();
   }
+
+  public int numChannels() {
+    return numChannels;
+  }
   
   private void setChannel() {
     activePanda = pandaMappings[channelIndex / PandaMapping.CHANNELS_PER_BOARD];
@@ -367,7 +371,7 @@ class MappingTool extends TestPattern {
       switch (mappingMode) {
         case MAPPING_MODE_ALL: cubeOn = true; break;
         case MAPPING_MODE_SINGLE_CUBE: cubeOn = (cubeIndex == ci); break;
-        case MAPPING_MODE_CHANNEL: cubeOn = (channelIndex > 0); break;
+        case MAPPING_MODE_CHANNEL: cubeOn = (indexOfCubeInChannel > 0); break;
       }
       if (cubeOn) {
         if (mappingMode == MAPPING_MODE_CHANNEL) {
@@ -408,7 +412,10 @@ class MappingTool extends TestPattern {
       }
       ++ci;
     }
-    
+  }
+  
+  public void setCube(int index) {
+    cubeIndex = index % model.cubes.size();
   }
   
   public void incCube() {
@@ -421,6 +428,11 @@ class MappingTool extends TestPattern {
       cubeIndex += model.cubes.size();
     }
   }
+  
+  public void setChannel(int index) {
+    channelIndex = index % numChannels;
+    setChannel();
+  }
 
   public void incChannel() {
     channelIndex = (channelIndex + 1) % numChannels;
@@ -432,6 +444,10 @@ class MappingTool extends TestPattern {
     setChannel();    
   }
   
+  public void setStrip(int index) {
+    stripIndex = index % Cube.STRIPS_PER_CUBE;
+  }
+  
   public void incStrip() {
     stripIndex = (stripIndex + 1) % Cube.STRIPS_PER_CUBE;
   }
@@ -440,7 +456,7 @@ class MappingTool extends TestPattern {
     stripIndex = (stripIndex + Cube.STRIPS_PER_CUBE - 1) % Cube.STRIPS_PER_CUBE;
   }
   
-  public void keyPressed() {
+  public void keyPressed(UIMapping uiMapping) {
     switch (keyCode) {
       case UP: if (mappingMode == MAPPING_MODE_CHANNEL) incChannel(); else incCube(); break;
       case DOWN: if (mappingMode == MAPPING_MODE_CHANNEL) decChannel(); else decCube(); break;
@@ -452,5 +468,10 @@ class MappingTool extends TestPattern {
       case 'g': channelModeGreen = !channelModeGreen; break;
       case 'b': channelModeBlue = !channelModeBlue; break;
     }
+    uiMapping.setChannelID(channelIndex+1);
+    uiMapping.setCubeID(cubeIndex+1);
+    uiMapping.setStripID(stripIndex+1);
+    uiMapping.redraw();
   }
+
 }
diff --git a/_DebugUI.pde b/_DebugUI.pde
new file mode 100644 (file)
index 0000000..5b99d43
--- /dev/null
@@ -0,0 +1,283 @@
+/**
+ *     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 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 CUBE_STATE_UNUSED = 0;
+  final int CUBE_STATE_USED = 1;
+  final int CUBE_STATE_DUPLICATED = 2;
+  
+  final int DEBUG_STATE_ANIM = 0;
+  final int DEBUG_STATE_WHITE = 1;
+  final int DEBUG_STATE_OFF = 2;
+  final int DEBUG_STATE_UNUSED = 3;  
+  
+  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;
+    
+    textSize(10);
+    
+    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();
+    fill(#CCCCCC);
+    text("Unused Cubes",  xBase, yPos + 12);
+    yPos += debugYSpacing;
+    
+    int xIndex = 0;
+    for (int rawIndex = 1; rawIndex <= glucose.model.cubes.size(); ++rawIndex) {
+      if (indexState[rawIndex] == CUBE_STATE_UNUSED) {
+        drawNumBox(xBase + (xIndex * debugXSpacing), yPos, rawIndex, DEBUG_STATE_UNUSED);
+        ++xIndex;
+        if (xIndex > 4) {
+          xIndex = 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, String label, int state) {
+    drawNumBox(xPos, yPos, "" + label, state, CUBE_STATE_USED);
+  }
+
+  void drawNumBox(int xPos, int yPos, int label, int state, int cubeState) {
+    drawNumBox(xPos, yPos, "" + label, state, cubeState);
+  }
+  
+  void drawNumBox(int xPos, int yPos, String label, int state, int cubeState) {
+    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);
+        break;
+      case DEBUG_STATE_WHITE:
+        stroke(textColor);
+        fill(#e9e9e9);
+        textColor = #333333;
+        break;
+      case DEBUG_STATE_OFF:
+        stroke(textColor);
+        break;
+      case DEBUG_STATE_UNUSED:
+        stroke(textColor);
+        fill(#880000);
+        break;
+    }
+    
+    if (cubeState >= CUBE_STATE_DUPLICATED) {
+      stroke(textColor = #FF0000);
+    }
+
+    rect(xPos, yPos, 16, 16);     
+    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;
+    }
+  }
+  
+  boolean mousePressed() {
+    int dx = (mouseX - debugX) / debugXSpacing;
+    int dy = (mouseY - debugY) / debugYSpacing;
+    if ((dy < 0) || (dy >= debugState.length)) {
+      return false;
+    }
+    if ((dx < 0) || (dx >= debugState[dy].length)) {
+      return false;
+    }
+    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;
+      }
+    }
+    return true;
+  }    
+}
+
index 5035ed59b6f8aeef7f4c7f232466929222924211..57a01612b36da97acb27361b3fc21324bccc1716 100644 (file)
@@ -48,17 +48,28 @@ MappingTool mappingTool;
 LXPattern[] patterns;
 LXTransition[] transitions;
 LXEffect[] effects;
-OverlayUI ui;
-ControlUI controlUI;
-MappingUI mappingUI;
 PandaDriver[] pandaBoards;
 boolean mappingMode = false;
 boolean debugMode = false;
 DebugUI debugUI;
+String displayMode;
+
+UIContext[] overlays;
+UIPatternDeck uiPatternA;
+UIMapping uiMapping;
+UIDebugText uiDebugText;
 
 // Camera variables
 float eyeR, eyeA, eyeX, eyeY, eyeZ, midX, midY, midZ;
 
+LXPattern[] _patterns(GLucose glucose) {
+  LXPattern[] patterns = patterns(glucose);
+  for (LXPattern p : patterns) {
+    p.setTransition(new DissolveTransition(glucose.lx).setDuration(1000));
+  }
+  return patterns;
+}
+
 void setup() {
   startMillis = lastMillis = millis();
 
@@ -76,12 +87,15 @@ void setup() {
   logTime("Built GLucose engine");
   
   // Set the patterns
-  glucose.lx.setPatterns(patterns = patterns(glucose));
+  Engine engine = lx.engine;
+  glucose.setTransitions(transitions = transitions(glucose));
+  logTime("Built transitions");
+  engine.setPatterns(patterns = _patterns(glucose));
+  engine.addDeck(_patterns(glucose));
+  engine.getDeck(1).setBlendTransition(transitions[0]);
   logTime("Built patterns");
   glucose.lx.addEffects(effects = effects(glucose));
   logTime("Built effects");
-  glucose.setTransitions(transitions = transitions(glucose));
-  logTime("Built transitions");
     
   // Build output driver
   PandaMapping[] pandaMappings = buildPandaList();
@@ -94,9 +108,20 @@ void setup() {
   logTime("Built PandaDriver");
   
   // Build overlay UI
-  ui = controlUI = new ControlUI();
-  mappingUI = new MappingUI(mappingTool);
   debugUI = new DebugUI(pandaMappings);
+  overlays = new UIContext[] {
+    uiPatternA = new UIPatternDeck(lx.engine.getDeck(0), "PATTERN A", 4, 4, 140, 344),
+    new UICrossfader(4, 352, 140, 152),
+    new UIOutput(4, 508, 140, 122),
+    
+    new UIPatternDeck(lx.engine.getDeck(1), "PATTERN B", width-144, 4, 140, 344),
+    new UIEffects(width-144, 352, 140, 144),
+    new UITempo(width-144, 498, 140, 50),
+    
+    uiDebugText = new UIDebugText(4, height-64, width-8, 44),
+    uiMapping = new UIMapping(mappingTool, 4, 4, 140, 344),
+  };
+  uiMapping.setVisible(false);
   logTime("Built overlay UI");
     
   // MIDI devices
@@ -107,12 +132,12 @@ void setup() {
   logTime("Setup MIDI devices");
     
   // Setup camera
-  midX = TRAILER_WIDTH/2. + 20;
+  midX = TRAILER_WIDTH/2.;
   midY = glucose.model.yMax/2;
   midZ = TRAILER_DEPTH/2.;
   eyeR = -290;
   eyeA = .15;
-  eyeY = midY + 20;
+  eyeY = midY + 70;
   eyeX = midX + eyeR*sin(eyeA);
   eyeZ = midZ + eyeR*cos(eyeA);
   addMouseWheelListener(new java.awt.event.MouseWheelListener() { 
@@ -152,7 +177,12 @@ void logTime(String evt) {
 void draw() {
   // Draws the simulation and the 2D UI overlay
   background(40);
-  color[] colors = glucose.getColors();
+  color[] colors = glucose.getColors();;
+  if (displayMode == "A") {
+    colors = lx.engine.getDeck(0).getColors();
+  } else if (displayMode == "B") {
+    colors = lx.engine.getDeck(1).getColors();
+  }
   if (debugMode) {
     debugUI.maskColors(colors);
   }
@@ -163,6 +193,8 @@ void draw() {
     0, -1, 0
   );
 
+  translate(0, 10, 0);
+
   noStroke();
   fill(#141414);
   drawBox(0, -TRAILER_HEIGHT, 0, 0, 0, 0, TRAILER_WIDTH, TRAILER_HEIGHT, TRAILER_DEPTH, TRAILER_HEIGHT/2.);
@@ -197,16 +229,13 @@ void draw() {
   }
   endShape();
   
-  // 2D Overlay
-  camera();
-  javax.media.opengl.GL gl = ((PGraphicsOpenGL)g).beginGL();
-  gl.glClear(javax.media.opengl.GL.GL_DEPTH_BUFFER_BIT);
-  ((PGraphicsOpenGL)g).endGL();
-  strokeWeight(1);
+  // 2D Overlay UI
   drawUI();
-  
+    
+  // Send output colors
+  color[] sendColors = glucose.getColors();
   if (debugMode) {
-    debugUI.draw();
+    debugUI.maskColors(colors);
   }
   
   // Gamma correction here. Apply a cubic to the brightness
@@ -333,28 +362,37 @@ void drawBox(float x, float y, float z, float rx, float ry, float rz, float xd,
 }
 
 void drawUI() {
+  camera();
+  javax.media.opengl.GL gl = ((PGraphicsOpenGL)g).beginGL();
+  gl.glClear(javax.media.opengl.GL.GL_DEPTH_BUFFER_BIT);
+  ((PGraphicsOpenGL)g).endGL();
+  strokeWeight(1);
+
   if (uiOn) {
-    ui.draw();
-  } else {
-    ui.drawHelpTip();
+    for (UIContext context : overlays) {
+      context.draw();
+    }
+  }
+  
+  // Always draw FPS meter
+  fill(#555555);
+  textSize(9);
+  textAlign(LEFT, BASELINE);
+  text("FPS: " + ((int) (frameRate*10)) / 10. + " / " + targetFramerate + " (-/+)", 4, height-4);
+
+  if (debugMode) {
+    debugUI.draw();
   }
-  ui.drawFPS();
-  ui.drawDanText();
 }
 
 boolean uiOn = true;
-int restoreToIndex = -1;
-
-boolean doDual = false;
+LXPattern restoreToPattern = null;
 
 void keyPressed() {
   if (mappingMode) {
-    mappingTool.keyPressed();
+    mappingTool.keyPressed(uiMapping);
   }
   switch (key) {
-    case 'w':
-       doDual = !doDual;
-       break;
     case '-':
     case '_':
       frameRate(--targetFramerate);
@@ -369,20 +407,14 @@ void keyPressed() {
       break;
     case 'm':
       mappingMode = !mappingMode;
+      uiPatternA.setVisible(!mappingMode);
+      uiMapping.setVisible(mappingMode);
       if (mappingMode) {
-        LXPattern pattern = lx.getPattern();
-        for (int i = 0; i < patterns.length; ++i) {
-          if (pattern == patterns[i]) {
-            restoreToIndex = i;
-            break;
-          }
-        }
-        ui = mappingUI;
+        restoreToPattern = lx.getPattern();
         lx.setPatterns(new LXPattern[] { mappingTool });
       } else {
-        ui = controlUI;
         lx.setPatterns(patterns);
-        lx.goIndex(restoreToIndex);
+        lx.goPattern(restoreToPattern);
       }
       break;
     case 'p':
@@ -398,20 +430,25 @@ void keyPressed() {
 
 int mx, my;
 void mousePressed() {
-  ui.mousePressed();
-  if (mouseX < ui.leftPos) {
-    if (debugMode) {
-      debugUI.mousePressed();
-    }    
-    mx = mouseX;
-    my = mouseY;
+  boolean debugged = false;
+  if (debugMode) {
+    debugged = debugUI.mousePressed();
+  }
+  if (!debugged) {
+    for (UIContext context : overlays) {
+      context.mousePressed(mouseX, mouseY);
+    }
   }
+  mx = mouseX;
+  my = mouseY;
 }
 
 void mouseDragged() {
-  if (mouseX > ui.leftPos) {
-    ui.mouseDragged();
-  } else {
+  boolean dragged = false;
+  for (UIContext context : overlays) {
+    dragged |= context.mouseDragged(mouseX, mouseY);
+  }
+  if (!dragged) {
     int dx = mouseX - mx;
     int dy = mouseY - my;
     mx = mouseX;
@@ -424,13 +461,20 @@ void mouseDragged() {
 }
 
 void mouseReleased() {
-  ui.mouseReleased();
+  for (UIContext context : overlays) {
+    context.mouseReleased(mouseX, mouseY);
+  }
+
+  // ui.mouseReleased();
 }
  
 void mouseWheel(int delta) {
-  if (mouseX > ui.leftPos) {
-    ui.mouseWheel(delta);
-  } else {
+  boolean wheeled = false;
+  for (UIContext context : overlays) {
+    wheeled |= context.mouseWheel(mouseX, mouseY, delta);
+  }
+  
+  if (!wheeled) {
     eyeR = constrain(eyeR - delta, -500, -80);
     eyeX = midX + eyeR*sin(eyeA);
     eyeZ = midZ + eyeR*cos(eyeA);
diff --git a/_Overlay.pde b/_Overlay.pde
deleted file mode 100644 (file)
index 1b13b03..0000000
+++ /dev/null
@@ -1,1028 +0,0 @@
-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.
- */
-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);
-    noStroke();
-    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 int firstPatternY;
-  private int firstPatternKnobY;
-  private int firstTransitionY;
-  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;
-
-  ControlUI() {    
-    patternNames = classNameArray(patterns, "Pattern");
-    transitionNames = classNameArray(transitions, "Transition");
-    effectNames = classNameArray(effects, "Effect");
-
-    try {
-      patternStateMethod = getClass().getMethod("getState", LXPattern.class);
-      effectStateMethod = getClass().getMethod("getState", LXEffect.class);
-      transitionStateMethod = getClass().getMethod("getState", LXTransition.class);
-    } catch (Exception x) {
-      throw new RuntimeException(x);
-    }    
-  }
-      
-  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, 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.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;
-
-    yPos += sectionSpacing;
-    firstTransitionY = yPos + lineHeight + 6;
-    yPos = drawObjectList(yPos, "TRANSITION", transitions, transitionNames, transitionStateMethod);
-    yPos += controlSpacing;
-    firstTransitionKnobY = yPos;
-    xPos = leftTextPos;
-    for (VirtualTransitionKnob knob : glucose.transitionKnobs) {
-      drawKnob(xPos, yPos, knobSize, knob);
-      xPos += knobSize + knobSpacing;
-    }
-    yPos += knobSize + knobLabelHeight;
-    
-    yPos += sectionSpacing;
-    firstEffectY = yPos + lineHeight + 6;
-    yPos = drawObjectList(yPos, "FX", effects, effectNames, effectStateMethod);
-    yPos += controlSpacing;
-    firstEffectKnobY = yPos;    
-    xPos = leftTextPos;
-    for (VirtualEffectKnob knob : glucose.effectKnobs) {    
-      drawKnob(xPos, yPos, knobSize, knob);
-      xPos += knobSize + knobSpacing;
-    }
-    yPos += knobSize + knobLabelHeight;
-    
-    yPos += sectionSpacing;
-    yPos = drawObjectList(yPos, "TEMPO", null, null, null);
-    yPos += 6;
-    tempoY = yPos;
-    stroke(#111111);
-    fill(tempoDown ? lightGreen : color(0, 0, 35 - 8*lx.tempo.rampf()));
-    rect(leftPos + 4, yPos, w - 8, tempoHeight);
-    fill(0);
-    textAlign(CENTER);
-    text("" + ((int)(lx.tempo.bpmf() * 100) / 100.), leftPos + w/2., yPos + tempoHeight - 6);
-    yPos += tempoHeight;
-    
-    drawToggleTip("Tap 'u' to hide");
-  }
-  
-  public LXParameter getOrNull(List<LXParameter> items, int index) {
-    if (index < items.size()) {
-      return items.get(index);
-    }
-    return null;
-  }
-  
-  public int getState(LXPattern p) {
-    if (p == lx.getPattern()) {
-      return STATE_ACTIVE;
-    } else if (p == lx.getNextPattern()) {
-      return STATE_PENDING;
-    }
-    return STATE_DEFAULT;
-  }
-  
-  public int getState(LXEffect e) {
-    if (e.isEnabled()) {
-      return STATE_PENDING;
-    } else if (e == glucose.getSelectedEffect()) {
-      return STATE_ACTIVE;
-    }
-    return STATE_DEFAULT;
-  }
-  
-  public int getState(LXTransition t) {
-    if (t == lx.getTransition()) {
-      return STATE_PENDING;
-    } else if (t == glucose.getSelectedTransition()) {
-      return STATE_ACTIVE;
-    }
-    return STATE_DEFAULT;
-  }
-  
-  private void drawKnob(int xPos, int yPos, int knobSize, LXParameter knob) {
-    final float knobValue = knob.getValuef();
-    String knobLabel = knob.getLabel();
-    if (knobLabel == null) {
-      knobLabel = "-";
-    } else if (knobLabel.length() > 4) {
-      knobLabel = knobLabel.substring(0, 4);
-    }
-    
-    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
-    // does a bunch of depth buffer intersection tests? Ellipse with a trapezoid cut out is faster
-    // arc(xPos + knobSize/2, yPos + knobSize/2, knobSize, knobSize, HALF_PI + knobIndent, HALF_PI + knobIndent + (TWO_PI-2*knobIndent));
-    ellipse(xPos + knobSize/2, yPos + knobSize/2, knobSize, knobSize);
-    
-    float endArc = HALF_PI + knobIndent + (TWO_PI-2*knobIndent)*knobValue;
-    fill(lightGreen);
-    arc(xPos + knobSize/2, yPos + knobSize/2, knobSize, knobSize, HALF_PI + knobIndent, endArc);
-    
-    // Mask notch out of knob
-    fill(color(0, 0, 30));
-    beginShape();
-    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);
-    fill(#999999);
-    textAlign(CENTER);
-    textFont(knobFont);
-    text(knobLabel, xPos + knobSize/2, yPos + knobSize + knobLabelHeight - 2);
-  }
-  
-  private int patternKnobIndex = -1;
-  private int transitionKnobIndex = -1;
-  private int effectKnobIndex = -1;
-  private boolean patternScrolling = false;
-  
-  private int lastY;
-  private int releaseEffect = -1;
-  private boolean tempoDown = false;
-
-  public void mousePressed() {
-    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();
-        tempoDown = true;
-      }
-    } else if ((mouseY >= firstEffectKnobY) && (mouseY < firstEffectKnobY + knobSize + knobLabelHeight)) {
-      effectKnobIndex = (mouseX - leftTextPos) / (knobSize + knobSpacing);
-    } else if (mouseY > firstEffectY) {
-      int effectIndex = objectClickIndex(firstEffectY);
-      if (effectIndex < effects.length) {
-        if (effects[effectIndex] == glucose.getSelectedEffect()) {
-          effects[effectIndex].enable();
-          releaseEffect = effectIndex;
-        }
-        glucose.setSelectedEffect(effectIndex);
-      }
-    } else if ((mouseY >= firstTransitionKnobY) && (mouseY < firstTransitionKnobY + knobSize + knobLabelHeight)) {
-      transitionKnobIndex = (mouseX - leftTextPos) / (knobSize + knobSpacing);
-    } else if (mouseY > firstTransitionY) {
-      int transitionIndex = objectClickIndex(firstTransitionY);
-      if (transitionIndex < transitions.length) {
-        glucose.setSelectedTransition(transitionIndex);
-      }
-    } else if ((mouseY >= firstPatternKnobY) && (mouseY < firstPatternKnobY + 2*(knobSize+knobLabelHeight) + knobSpacing)) {
-      patternKnobIndex = (mouseX - leftTextPos) / (knobSize + knobSpacing);
-      if (mouseY >= firstPatternKnobY + knobSize + knobLabelHeight + knobSpacing) {
-        patternKnobIndex += glucose.NUM_PATTERN_KNOBS / 2;
-      }      
-    } else if (mouseY > firstPatternY) {
-      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.get(patternKnobIndex);
-      p.setValue(constrain(p.getValuef() + dy*.01, 0, 1));
-    } 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 < 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();
-      releaseEffect = -1;      
-    }
-  }
-  
-  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);
-      }
-    }
-  }
-  
-}
-
-/**
- * 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;
-
-    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;    
-  }
-
-  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 CUBE_STATE_UNUSED = 0;
-  final int CUBE_STATE_USED = 1;
-  final int CUBE_STATE_DUPLICATED = 2;
-  
-  final int DEBUG_STATE_ANIM = 0;
-  final int DEBUG_STATE_WHITE = 1;
-  final int DEBUG_STATE_OFF = 2;
-  final int DEBUG_STATE_UNUSED = 3;  
-  
-  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();
-    fill(#CCCCCC);
-    text("Unused Cubes",  xBase, yPos + 12);
-    yPos += debugYSpacing;
-    
-    int xIndex = 0;
-    for (int rawIndex = 1; rawIndex <= glucose.model.cubes.size(); ++rawIndex) {
-      if (indexState[rawIndex] == CUBE_STATE_UNUSED) {
-        drawNumBox(xBase + (xIndex * debugXSpacing), yPos, rawIndex, DEBUG_STATE_UNUSED);
-        ++xIndex;
-        if (xIndex > 4) {
-          xIndex = 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, String label, int state) {
-    drawNumBox(xPos, yPos, "" + label, state, CUBE_STATE_USED);
-  }
-
-  void drawNumBox(int xPos, int yPos, int label, int state, int cubeState) {
-    drawNumBox(xPos, yPos, "" + label, state, cubeState);
-  }
-  
-  void drawNumBox(int xPos, int yPos, String label, int state, int cubeState) {
-    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);
-        break;
-      case DEBUG_STATE_WHITE:
-        stroke(textColor);
-        fill(#e9e9e9);
-        textColor = #333333;
-        break;
-      case DEBUG_STATE_OFF:
-        stroke(textColor);
-        break;
-      case DEBUG_STATE_UNUSED:
-        stroke(textColor);
-        fill(#880000);
-        break;
-    }
-    
-    if (cubeState >= CUBE_STATE_DUPLICATED) {
-      stroke(textColor = #FF0000);
-    }
-
-    rect(xPos, yPos, 16, 16);     
-    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;
-          }
-        }
-      }
-    }
-  }    
-}
index a65a0fe78ee5d79817f75b4e44cebfc9accc658e..2d28cd693e12bea5c300d0c604dcfbe0ba07f40a 100644 (file)
@@ -15,6 +15,13 @@ import oscP5.*;
  * will be moved into GLucose once stabilized.
  */
 public static class PandaDriver {
+  
+  interface Listener {
+    public void onToggle(boolean enabled);
+  }
+  
+  private Listener listener = null;
+  
   // IP address
   public final String ip;
   
@@ -35,23 +42,6 @@ public static class PandaDriver {
 
   private static final int NO_POINT = -1;
 
-  public PandaDriver(String ip) {
-    this.ip = ip;
-    
-    // Initialize our OSC output stuff
-    address = new NetAddress(ip, 9001);
-    message = new OscMessage("/shady/pointbuffer");
-    
-    // Build the array of points, initialize all to nothing
-    points = new int[PandaMapping.PIXELS_PER_BOARD];
-    for (int i = 0; i < points.length; ++i) {
-      points[i] = NO_POINT;
-    }
-  }
-
-  private final static int FORWARD = -1;
-  private final static int BACKWARD = -2;
-
   ////////////////////////////////////////////////////////////////
   //
   // READ THIS RIGHT NOW BEFORE YOU MODIFY THE BELOW!!!!!!!!!!!!!
@@ -85,6 +75,8 @@ public static class PandaDriver {
   //
   ////////////////////////////////////////////////////////////////
   
+  private final static int FORWARD = -1;
+  private final static int BACKWARD = -2;
 
   /**
    * These constant arrays indicate the order in which the strips of a cube
@@ -164,7 +156,21 @@ public static class PandaDriver {
       {3, BACKWARD },
     }
   };
-  
+
+  public PandaDriver(String ip) {
+    this.ip = ip;
+    
+    // Initialize our OSC output stuff
+    address = new NetAddress(ip, 9001);
+    message = new OscMessage("/shady/pointbuffer");
+    
+    // Build the array of points, initialize all to nothing
+    points = new int[PandaMapping.PIXELS_PER_BOARD];
+    for (int i = 0; i < points.length; ++i) {
+      points[i] = NO_POINT;
+    }
+  }
+    
   public PandaDriver(String ip, Model model, PandaMapping pm) {
     this(ip);
     
@@ -271,23 +277,31 @@ public static class PandaDriver {
     return pi;
   }
 
-  public void disable() {
-    if (enabled) {
-      enabled = false;
-      println("PandaBoard/" + ip + ": OFF");
+  public PandaDriver setListener(Listener listener) {
+    this.listener = listener;
+    return this;
+  }
+
+  public void setEnabled(boolean enabled) {
+    if (this.enabled != enabled) {
+      this.enabled = enabled;
+      println("PandaBoard/" + ip + ": " + (enabled ? "ON" : "OFF"));
+      if (listener != null) {
+        listener.onToggle(enabled);
+      }
     }
   }
+
+  public void disable() {
+    setEnabled(false);
+  }
   
   public void enable() {
-    if (!enabled) {
-      enabled = true;
-      println("PandaBoard/" + ip + ": ON");
-    }
+    setEnabled(true);
   }
 
   public void toggle() {
-    enabled = !enabled;
-    println("PandaBoard/" + ip + ": " + (enabled ? "ON" : "OFF"));    
+    setEnabled(!enabled);
   }
 
   public final void send(int[] colors) {
diff --git a/_UIFramework.pde b/_UIFramework.pde
new file mode 100644 (file)
index 0000000..a2946fb
--- /dev/null
@@ -0,0 +1,825 @@
+/**
+ *     DOUBLE BLACK DIAMOND        DOUBLE BLACK DIAMOND
+ *
+ *         //\\   //\\                 //\\   //\\  
+ *        ///\\\ ///\\\               ///\\\ ///\\\
+ *        \\\/// \\\///               \\\/// \\\///
+ *         \\//   \\//                 \\//   \\//
+ *
+ *        EXPERTS ONLY!!              EXPERTS ONLY!!
+ *
+ * Little UI framework in progress to handle mouse events, layout,
+ * redrawing, etc.
+ */
+
+final color lightGreen = #669966;
+final color lightBlue = #666699;
+final color bgGray = #444444;
+final color defaultTextColor = #999999;
+final PFont defaultItemFont = createFont("Lucida Grande", 11);
+final PFont defaultTitleFont = createFont("Myriad Pro", 10);
+
+public abstract class UIObject {
+  
+  protected final List<UIObject> children = new ArrayList<UIObject>();  
+
+  protected boolean needsRedraw = true;
+  protected boolean childNeedsRedraw = true;
+  
+  protected float x=0, y=0, w=0, h=0;
+  
+  public UIContainer parent = null;
+  
+  protected boolean visible = true;
+  
+  public UIObject() {}
+  
+  public UIObject(float x, float y, float w, float h) {
+    this.x = x;
+    this.y = y;
+    this.w = w;
+    this.h = h;
+  }
+
+  public boolean isVisible() {
+    return visible;
+  }
+
+  public UIObject setVisible(boolean visible) {
+    if (visible != this.visible) {
+      this.visible = visible;
+      redraw();
+    }
+    return this;
+  }
+
+  public final UIObject setPosition(float x, float y) {
+    this.x = x;
+    this.y = y;
+    redraw();
+    return this;
+  }
+  
+  public final UIObject setSize(float w, float h) {
+    this.w = w;
+    this.h = h;
+    redraw();
+    return this;
+  }
+
+  public final UIObject addToContainer(UIContainer c) {
+    c.children.add(this);
+    this.parent = c;
+    return this;
+  }
+  
+  public final UIObject removeFromContainer(UIContainer c) {
+    c.children.remove(this);
+    this.parent = null;
+    return this;
+  }
+  
+  public final UIObject redraw() {
+    _redraw();
+    UIObject p = this.parent;
+    while (p != null) {
+      p.childNeedsRedraw = true;
+      p = p.parent;
+    }
+    return this;
+  }
+  
+  private final void _redraw() {
+    needsRedraw = true;
+    for (UIObject child : children) {
+      childNeedsRedraw = true;
+      child._redraw();
+    }    
+  }
+    
+  public final void draw(PGraphics pg) {
+    if (!visible) {
+      return;
+    }
+    if (needsRedraw) {
+      needsRedraw = false;
+      onDraw(pg);
+    }
+    if (childNeedsRedraw) {
+      childNeedsRedraw = false;
+      for (UIObject child : children) {
+        if (needsRedraw || child.needsRedraw || child.childNeedsRedraw) {
+          pg.pushMatrix();
+          pg.translate(child.x, child.y);
+          child.draw(pg);
+          pg.popMatrix();
+        }
+      }
+    }
+  }
+  
+  public final boolean contains(float x, float y) {
+    return
+      (x >= this.x && x < (this.x + this.w)) &&
+      (y >= this.y && y < (this.y + this.h));
+  }
+  
+  protected void onDraw(PGraphics pg) {}
+  protected void onMousePressed(float mx, float my) {}
+  protected void onMouseReleased(float mx, float my) {}
+  protected void onMouseDragged(float mx, float my, float dx, float dy) {}  
+  protected void onMouseWheel(float mx, float my, float dx) {}  
+}
+
+public class UIContainer extends UIObject {
+  
+  private UIObject focusedChild = null;
+  
+  public UIContainer() {}
+  
+  public UIContainer(float x, float y, float w, float h) {
+    super(x, y, w, h);
+  }
+  
+  public UIContainer(UIObject[] children) {
+    for (UIObject child : children) {
+      child.addToContainer(this);
+    }
+  }
+  
+  protected void onMousePressed(float mx, float my) {
+    for (int i = children.size() - 1; i >= 0; --i) {
+      UIObject child = children.get(i);
+      if (child.contains(mx, my)) {
+        child.onMousePressed(mx - child.x, my - child.y);
+        focusedChild = child;
+        break;
+      }
+    }
+  }
+    
+  protected void onMouseReleased(float mx, float my) {
+    if (focusedChild != null) {
+      focusedChild.onMouseReleased(mx - focusedChild.x, my - focusedChild.y);
+    }
+    focusedChild = null;
+  }
+  
+  protected void onMouseDragged(float mx, float my, float dx, float dy) {
+    if (focusedChild != null) {
+      focusedChild.onMouseDragged(mx - focusedChild.x, my - focusedChild.y, dx, dy);
+    }
+  }
+  
+  protected void onMouseWheel(float mx, float my, float delta) {
+    for (UIObject child : children) {
+      if (child.contains(mx, my)) {
+        child.onMouseWheel(mx - child.x, mx - child.y, delta);
+      }
+    }
+  }
+  
+}
+
+public class UIContext extends UIContainer {
+  
+  final public PGraphics pg;
+  
+  UIContext(float x, float y, float w, float h) {
+    super(x, y, w, h);
+    pg = createGraphics((int)w, (int)h, JAVA2D);
+    pg.smooth();
+  }
+  
+  public void draw() {
+    if (!visible) {
+      return;
+    }
+    if (needsRedraw || childNeedsRedraw) {
+      pg.beginDraw();
+      draw(pg);
+      pg.endDraw();
+    }
+    image(pg, x, y);
+  }
+  
+  private float px, py;
+  private boolean dragging = false;
+  
+  public boolean mousePressed(float mx, float my) {
+    if (!visible) {
+      return false;
+    }
+    if (contains(mx, my)) {
+      dragging = true;
+      px = mx;
+      py = my;
+      onMousePressed(mx - x, my - y);
+      return true;
+    }
+    return false;
+  }
+  
+  public boolean mouseReleased(float mx, float my) {
+    if (!visible) {
+      return false;
+    }
+    dragging = false;
+    onMouseReleased(mx - x, my - y);
+    return true;
+  }
+  
+  public boolean mouseDragged(float mx, float my) {
+    if (!visible) {
+      return false;
+    }
+    if (dragging) {
+      float dx = mx - px;
+      float dy = my - py;
+      onMouseDragged(mx - x, my - y, dx, dy);
+      px = mx;
+      py = my;
+      return true;
+    }
+    return false;
+  }
+  
+  public boolean mouseWheel(float mx, float my, float delta) {
+    if (!visible) {
+      return false;
+    }
+    if (contains(mx, my)) {
+      onMouseWheel(mx - x, my - y, delta);
+      return true;
+    }
+    return false;
+  }
+}
+
+public class UIWindow extends UIContext {
+  
+  protected final static int titleHeight = 24;
+  
+  public UIWindow(String label, float x, float y, float w, float h) {
+    super(x, y, w, h);
+    new UILabel(6, 8, w-6, titleHeight-8) {
+      protected void onMouseDragged(float mx, float my, float dx, float dy) {
+        parent.x = constrain(parent.x + dx, 0, width - w);
+        parent.y = constrain(parent.y + dy, 0, height - h);
+      }
+    }.setLabel(label).setFont(defaultTitleFont).addToContainer(this);
+  }
+
+  protected void onDraw(PGraphics pg) {
+    pg.noStroke();
+    pg.fill(#444444);
+    pg.stroke(#292929);
+    pg.rect(0, 0, w-1, h-1);
+  }
+}
+
+public class UILabel extends UIObject {
+  
+  private PFont font = defaultTitleFont;
+  private color fontColor = #CCCCCC;
+  private String label = "";
+    
+  public UILabel(float x, float y, float w, float h) {
+    super(x, y, w, h);
+  }
+  
+  protected void onDraw(PGraphics pg) {
+    pg.textAlign(LEFT, TOP);
+    pg.textFont(font);
+    pg.fill(fontColor);
+    pg.text(label, 0, 0);
+  }
+  
+  public UILabel setFont(PFont font) {
+    this.font = font;
+    redraw();
+    return this;
+  }
+  
+  public UILabel setFontColor(color fontColor) {
+    this.fontColor = fontColor;
+    redraw();
+    return this;
+  }
+  
+  public UILabel setLabel(String label) {
+    this.label = label;
+    redraw();
+    return this;
+  }
+}
+
+public class UIButton extends UIObject {
+
+  private boolean active = false;
+  private boolean isMomentary = false;
+  private color borderColor = #666666;
+  private color inactiveColor = #222222;
+  private color activeColor = #669966;
+  private color labelColor = #999999;
+  private String label = "";
+   
+  public UIButton(float x, float y, float w, float h) {
+    super(x, y, w, h);
+  }
+  
+  public UIButton setMomentary(boolean momentary) {
+    isMomentary = momentary;
+    return this;
+  }
+  
+  protected void onDraw(PGraphics pg) {
+    pg.stroke(borderColor);
+    pg.fill(active ? activeColor : inactiveColor);
+    pg.rect(0, 0, w, h);
+    if (label != null && label.length() > 0) {
+      pg.fill(active ? #FFFFFF : labelColor);
+      pg.textFont(defaultItemFont);
+      pg.textAlign(CENTER);
+      pg.text(label, w/2, h-5);
+    }
+  }
+  
+  protected void onMousePressed(float mx, float my) {
+    if (isMomentary) {
+      setActive(true);
+    } else {
+      setActive(!active);
+    }
+  }
+  
+  protected void onMouseReleased(float mx, float my) {
+    if (isMomentary) {
+      setActive(false);
+    }
+  }
+  
+  public UIButton setActive(boolean active) {
+    this.active = active;
+    onToggle(active);
+    redraw();
+    return this;
+  }
+  
+  public UIButton toggle() {
+    return setActive(!active);
+  }
+  
+  protected void onToggle(boolean active) {}
+  
+  public UIButton setBorderColor(color borderColor) {
+    if (this.borderColor != borderColor) {
+      this.borderColor = borderColor;
+      redraw();
+    }
+    return this;
+  }
+  
+  public UIButton setActiveColor(color activeColor) {
+    if (this.activeColor != activeColor) {
+      this.activeColor = activeColor;
+      if (active) {
+        redraw();
+      }
+    }
+    return this;
+  }
+  
+  public UIButton setInactiveColor(color inactiveColor) {
+    if (this.inactiveColor != inactiveColor) {
+      this.inactiveColor = inactiveColor;
+      if (!active) {
+        redraw();
+      }
+    }
+    return this;
+  }
+  
+  public UIButton setLabelColor(color labelColor) {
+    if (this.labelColor != labelColor) {
+      this.labelColor = labelColor;
+      redraw();
+    }
+    return this;
+  }
+
+  public UIButton setLabel(String label) {
+    if (!this.label.equals(label)) {
+      this.label = label;
+      redraw();
+    }
+    return this;
+  }
+  
+  public void onMousePressed() {
+    setActive(!active);
+  }
+}
+
+public class UIToggleSet extends UIObject {
+
+  private String[] options;
+  private int[] boundaries;
+  private String value;
+  
+  public UIToggleSet(float x, float y, float w, float h) {
+    super(x, y, w, h);
+  }
+  
+  public UIToggleSet setOptions(String[] options) {
+    this.options = options;
+    boundaries = new int[options.length];
+    int totalLength = 0;
+    for (String s : options) {
+      totalLength += s.length();
+    }
+    int lengthSoFar = 0;
+    for (int i = 0; i < options.length; ++i) {
+      lengthSoFar += options[i].length();
+      boundaries[i] = (int) (lengthSoFar * w / totalLength);
+    }
+    value = options[0];
+    redraw();
+    return this;
+  }
+  
+  public String getValue() {
+    return value;
+  }
+  
+  public UIToggleSet setValue(String option) {
+    value = option;
+    onToggle(value);
+    redraw();
+    return this;
+  }
+  
+  public void onDraw(PGraphics pg) {
+    pg.stroke(#666666);
+    pg.fill(#222222);
+    pg.rect(0, 0, w, h);
+    for (int b : boundaries) {
+      pg.line(b, 1, b, h-1);
+    }
+    pg.noStroke();
+    pg.textAlign(CENTER);
+    pg.textFont(defaultItemFont);
+    int leftBoundary = 0;
+    
+    for (int i = 0; i < options.length; ++i) {
+      boolean isActive = options[i] == value;
+      if (isActive) {
+        pg.fill(lightGreen);
+        pg.rect(leftBoundary + 1, 1, boundaries[i] - leftBoundary - 1, h-1);
+      }
+      pg.fill(isActive ? #FFFFFF : #999999);
+      pg.text(options[i], (leftBoundary + boundaries[i]) / 2., h-6);
+      leftBoundary = boundaries[i];
+    }
+  }
+  
+  public void onMousePressed(float mx, float my) {
+    for (int i = 0; i < boundaries.length; ++i) {
+      if (mx < boundaries[i]) {
+        setValue(options[i]);
+        break;
+      }
+    }
+  }
+  
+  protected void onToggle(String option) {}
+  
+}
+
+
+public abstract class UIParameterControl extends UIObject implements LXParameter.Listener {
+  protected LXParameter parameter = null;
+    
+  protected UIParameterControl(float x, float y, float w, float h) {
+    super(x, y, w, h);
+  }
+  
+  public void onParameterChanged(LXParameter parameter) {
+    redraw();
+  }
+  
+  public UIParameterControl setParameter(LXParameter parameter) {
+    if (this.parameter != null) {
+      if (this.parameter instanceof LXListenableParameter) {
+        ((LXListenableParameter)this.parameter).removeListener(this);
+      }
+    }
+    this.parameter = parameter;
+    if (this.parameter != null) {
+      if (this.parameter instanceof LXListenableParameter) {
+        ((LXListenableParameter)this.parameter).addListener(this);
+      }
+    }
+    redraw();
+    return this;
+  }
+}
+
+public class UIParameterKnob extends UIParameterControl {
+  private int knobSize = 28;
+  private final float knobIndent = .4;  
+  private final int knobLabelHeight = 14;
+    
+  public UIParameterKnob(float x, float y) {
+    this(x, y, 0, 0);
+    setSize(knobSize, knobSize + knobLabelHeight);
+  }
+  
+  public UIParameterKnob(float x, float y, float w, float h) {
+    super(x, y, w, h);
+  }
+
+  protected void onDraw(PGraphics pg) {    
+    float knobValue = (parameter != null) ? parameter.getValuef() : 0;
+    
+    pg.ellipseMode(CENTER);
+    pg.noStroke();
+
+    pg.fill(bgGray);
+    pg.rect(0, 0, knobSize, knobSize);
+
+    // Full outer dark ring
+    pg.fill(#222222);    
+    pg.arc(knobSize/2, knobSize/2, knobSize, knobSize, HALF_PI + knobIndent, HALF_PI + knobIndent + (TWO_PI-2*knobIndent));
+
+    // Light ring indicating value
+    pg.fill(lightGreen);
+    pg.arc(knobSize/2, knobSize/2, knobSize, knobSize, HALF_PI + knobIndent, HALF_PI + knobIndent + knobValue*(TWO_PI-2*knobIndent));
+    
+    // Center circle of knob
+    pg.fill(#333333);
+    pg.ellipse(knobSize/2, knobSize/2, knobSize/2, knobSize/2);
+
+    String knobLabel = (parameter != null) ? parameter.getLabel() : null;
+    if (knobLabel == null) {
+      knobLabel = "-";
+    } else if (knobLabel.length() > 4) {
+      knobLabel = knobLabel.substring(0, 4);
+    }
+    pg.fill(#000000);
+    pg.rect(0, knobSize + 2, knobSize, knobLabelHeight - 2);
+    pg.fill(#999999);
+    pg.textAlign(CENTER);
+    pg.textFont(defaultTitleFont);
+    pg.text(knobLabel, knobSize/2, knobSize + knobLabelHeight - 2);
+  }
+  
+  public void onMouseDragged(float mx, float my, float dx, float dy) {
+    if (parameter != null) {
+      float value = constrain(parameter.getValuef() - dy / 100., 0, 1);
+      parameter.setValue(value);
+    }
+  }
+}
+
+public class UIParameterSlider extends UIParameterControl {
+  
+  private static final float handleWidth = 12;
+  
+  UIParameterSlider(float x, float y, float w, float h) {
+    super(x, y, w, h);
+  }
+  
+  protected void onDraw(PGraphics pg) {
+    pg.noStroke();
+    pg.fill(#333333);
+    pg.rect(0, 0, w, h);
+    pg.fill(#222222);
+    pg.rect(4, h/2-2, w-8, 4);
+    pg.fill(#666666);
+    pg.stroke(#222222);
+    pg.rect((int) (4 + parameter.getValuef() * (w-8-handleWidth)), 4, handleWidth, h-8);
+  }
+  
+  private boolean editing = false;
+  protected void onMousePressed(float mx, float my) {
+    float handleLeft = 4 + parameter.getValuef() * (w-8-handleWidth);
+    if (mx >= handleLeft && mx < handleLeft + handleWidth) {
+      editing = true;
+    }
+  }
+  
+  protected void onMouseReleased(float mx, float my) {
+    editing = false;
+  }
+  
+  protected void onMouseDragged(float mx, float my, float dx, float dy) {
+    if (editing) {
+      parameter.setValue(constrain((mx - handleWidth/2. - 4) / (w-8-handleWidth), 0, 1));
+    }
+  }
+}
+
+public class UIScrollList extends UIObject {
+
+  private List<ScrollItem> items = new ArrayList<ScrollItem>();
+
+  private PFont itemFont = defaultItemFont;
+  private int itemHeight = 20;
+  private color selectedColor = lightGreen;
+  private color pendingColor = lightBlue;
+  private int scrollOffset = 0;
+  private int numVisibleItems = 0;
+
+  private boolean hasScroll;
+  private float scrollYStart;
+  private float scrollYHeight;
+  
+  public UIScrollList(float x, float y, float w, float h) {
+    super(x, y, w, h);
+  }
+    
+  protected void onDraw(PGraphics pg) {
+    int yp = 0;
+    boolean even = true;
+    for (int i = 0; i < numVisibleItems; ++i) {
+      if (i + scrollOffset >= items.size()) {
+        break;
+      }
+      ScrollItem item = items.get(i + scrollOffset);
+      color itemColor;
+      color labelColor = #FFFFFF;
+      if (item.isSelected()) {
+        itemColor = selectedColor;
+      } else if (item.isPending()) {
+        itemColor = pendingColor;
+      } else {
+        labelColor = #000000;
+        itemColor = even ? #666666 : #777777;
+      }
+      pg.noStroke();
+      pg.fill(itemColor);
+      pg.rect(0, yp, w, itemHeight);
+      pg.fill(labelColor);
+      pg.textFont(itemFont);
+      pg.textAlign(LEFT, TOP);
+      pg.text(item.getLabel(), 6, yp+4);
+                  
+      yp += itemHeight;
+      even = !even;
+    }
+    if (hasScroll) {
+      pg.noStroke();
+      pg.fill(color(0, 0, 100, 15));
+      pg.rect(w-12, 0, 12, h);
+      pg.fill(#333333);
+      pg.rect(w-12, scrollYStart, 12, scrollYHeight);
+    }
+    
+  }
+  
+  private boolean scrolling = false;
+  private ScrollItem pressedItem = null;
+  
+  public void onMousePressed(float mx, float my) {
+    pressedItem = null;
+    if (hasScroll && mx >= w-12) {
+      if (my >= scrollYStart && my < (scrollYStart + scrollYHeight)) {
+        scrolling = true;
+        dAccum = 0;
+      }
+    } else {
+      int index = (int) my / itemHeight;
+      if (scrollOffset + index < items.size()) {
+        pressedItem = items.get(scrollOffset + index);
+        pressedItem.onMousePressed();
+        pressedItem.select();
+        redraw();
+      }
+    }
+  }
+  
+  public void onMouseReleased(float mx, float my) {
+    scrolling = false;
+    if (pressedItem != null) {
+      pressedItem.onMouseReleased();
+      redraw();
+    }    
+  }
+  
+  private float dAccum = 0;
+  public void onMouseDragged(float mx, float my, float dx, float dy) {
+    if (scrolling) {
+      dAccum += dy;
+      float scrollOne = h / items.size();
+      int offset = (int) (dAccum / scrollOne);
+      if (offset != 0) {
+        dAccum -= offset * scrollOne;
+        setScrollOffset(scrollOffset + offset);
+      }
+    }
+  }
+    
+  private float wAccum = 0;
+  public void onMouseWheel(float mx, float my, float delta) {
+    wAccum += delta;
+    int offset = (int) (wAccum / 5);
+    if (offset != 0) {
+      wAccum -= offset * 5;
+      setScrollOffset(scrollOffset + offset);
+    }
+  }
+  
+  public void setScrollOffset(int offset) {
+    scrollOffset = constrain(offset, 0, items.size() - numVisibleItems);
+    scrollYStart = (int) (scrollOffset * h / items.size());
+    scrollYHeight = (int) (numVisibleItems * h / (float) items.size());
+    redraw();
+  }
+  
+  public UIScrollList setItems(List<ScrollItem> items) {
+    this.items = items;
+    numVisibleItems = (int) (h / itemHeight);
+    hasScroll = items.size() > numVisibleItems;
+    setScrollOffset(0);
+    redraw();
+    return this;
+  }
+}
+
+public interface ScrollItem {
+  public boolean isSelected();
+  public boolean isPending();
+  public String getLabel();
+  public void select();
+  public void onMousePressed();
+  public void onMouseReleased();  
+}
+
+public abstract class AbstractScrollItem implements ScrollItem {
+  public boolean isPending() {
+    return false;
+  }
+  public void select() {}
+  public void onMousePressed() {}
+  public void onMouseReleased() {}
+}
+
+public class UIIntegerBox extends UIObject {
+  
+  private int minValue = 0;
+  private int maxValue = MAX_INT;
+  private int value = 0;
+  
+  UIIntegerBox(float x, float y, float w, float h) {
+    super(x, y, w, h);
+  }
+  
+  public UIIntegerBox setRange(int minValue, int maxValue) {
+    this.minValue = minValue;
+    this.maxValue = maxValue;
+    setValue(constrain(value, minValue, maxValue));
+    return this;
+  }
+  
+  protected void onDraw(PGraphics pg) {
+    pg.stroke(#999999);
+    pg.fill(#222222);
+    pg.rect(0, 0, w, h);
+    pg.textAlign(CENTER, CENTER);
+    pg.textFont(defaultItemFont);
+    pg.fill(#999999);
+    pg.text("" + value, w/2, h/2);
+  }
+  
+  protected void onValueChange(int value) {}
+  
+  float dAccum = 0;
+  protected void onMousePressed(float mx, float my) {
+    dAccum = 0;
+  }
+  
+  protected void onMouseDragged(float mx, float my, float dx, float dy) {
+    dAccum -= dy;
+    int offset = (int) (dAccum / 5);
+    dAccum = dAccum - (offset * 5);
+    setValue(value + offset);
+  }
+  
+  public int getValue() {
+    return value;
+  }
+  
+  public UIIntegerBox setValue(int value) {
+    if (this.value != value) {
+      int range = (maxValue - minValue + 1);
+      while (value < minValue) {
+        value += range;
+      }        
+      this.value = minValue + (value - minValue) % range;
+      this.onValueChange(this.value);
+      redraw();
+    }
+    return this;
+  }
+}
diff --git a/_UIImplementation.pde b/_UIImplementation.pde
new file mode 100644 (file)
index 0000000..c8b9930
--- /dev/null
@@ -0,0 +1,450 @@
+/**
+ *     DOUBLE BLACK DIAMOND        DOUBLE BLACK DIAMOND
+ *
+ *         //\\   //\\                 //\\   //\\  
+ *        ///\\\ ///\\\               ///\\\ ///\\\
+ *        \\\/// \\\///               \\\/// \\\///
+ *         \\//   \\//                 \\//   \\//
+ *
+ *        EXPERTS ONLY!!              EXPERTS ONLY!!
+ *
+ * Custom UI components using the framework.
+ */
+class UIPatternDeck extends UIWindow {
+    
+  Engine.Deck deck;
+  
+  public UIPatternDeck(Engine.Deck deck, String label, float x, float y, float w, float h) {
+    super(label, x, y, w, h);
+    this.deck = deck;
+    int yp = titleHeight;
+        
+    List<ScrollItem> items = new ArrayList<ScrollItem>();
+    for (LXPattern p : deck.getPatterns()) {
+      items.add(new PatternScrollItem(p));
+    }    
+    final UIScrollList patternList = new UIScrollList(1, yp, w-2, 160).setItems(items);
+    patternList.addToContainer(this);
+    yp += patternList.h + 10;
+    
+    final UIParameterKnob[] parameterKnobs = new UIParameterKnob[12];
+    for (int ki = 0; ki < parameterKnobs.length; ++ki) {
+      parameterKnobs[ki] = new UIParameterKnob(5 + 34*(ki % 4), yp + (ki/4) * 48);
+      parameterKnobs[ki].addToContainer(this);
+    }
+    
+    Engine.Listener lxListener = new Engine.Listener() {
+      public void patternWillChange(Engine.Deck deck, LXPattern pattern, LXPattern nextPattern) {
+        patternList.redraw();
+      }
+      public void patternDidChange(Engine.Deck deck, LXPattern pattern) {
+        patternList.redraw();
+        int pi = 0;
+        for (LXParameter parameter : pattern.getParameters()) {
+          if (pi >= parameterKnobs.length) {
+            break;
+          }
+          parameterKnobs[pi++].setParameter(parameter);
+        }
+        while (pi < parameterKnobs.length) {
+          parameterKnobs[pi++].setParameter(null);
+        }
+      }
+    };
+    
+    deck.addListener(lxListener);
+    lxListener.patternDidChange(deck, deck.getActivePattern());
+    
+  }
+  
+  class PatternScrollItem extends AbstractScrollItem {
+    
+    private LXPattern pattern;
+    private String label;
+    
+    PatternScrollItem(LXPattern pattern) {
+      this.pattern = pattern;
+      label = className(pattern, "Pattern");
+    }
+    
+    public String getLabel() {
+      return label;
+    }
+    
+    public boolean isSelected() {
+      return deck.getActivePattern() == pattern;
+    }
+    
+    public boolean isPending() {
+      return deck.getNextPattern() == pattern;
+    }
+    
+    public void select() {
+      deck.goPattern(pattern);
+    }
+  }
+}
+
+class UICrossfader extends UIWindow {
+    
+  public UICrossfader(float x, float y, float w, float h) {
+    super("CROSSFADER", x, y, w, h);
+
+    List<ScrollItem> items = new ArrayList<ScrollItem>();
+    for (LXTransition t : transitions) {
+      items.add(new TransitionScrollItem(t));
+    }    
+    new UIScrollList(1, titleHeight, w-2, 60).setItems(items).addToContainer(this);
+    new UIParameterSlider(6, titleHeight + 66, w-12, 24).setParameter(lx.engine.getDeck(1).getCrossfader()).addToContainer(this);
+    new UIToggleSet(6, 122, w-12, 20) {
+      protected void onToggle(String value) {
+        displayMode = value;
+      }
+    }.setOptions(new String[] { "A", "COMP", "B" }).setValue(displayMode = "COMP").addToContainer(this);
+  }
+}
+
+class TransitionScrollItem extends AbstractScrollItem {
+  private final LXTransition transition;
+  private String label;
+  
+  TransitionScrollItem(LXTransition transition) {
+    this.transition = transition;
+    label = className(transition, "Transition");
+  }
+  
+  public String getLabel() {
+    return label;
+  }
+  
+  public boolean isSelected() {
+    return transition == lx.engine.getDeck(1).getBlendTransition();
+  }
+  
+  public boolean isPending() {
+    return false;
+  }
+  
+  public void select() {
+    lx.engine.getDeck(1).setBlendTransition(transition);
+  }
+}
+
+class UIEffects extends UIWindow {
+  UIEffects(float x, float y, float w, float h) {
+    super("FX", x, y, w, h);
+
+    int yp = titleHeight;
+    List<ScrollItem> items = new ArrayList<ScrollItem>();
+    for (LXEffect fx : glucose.lx.getEffects()) {
+      items.add(new FXScrollItem(fx));
+    }    
+    final UIScrollList effectsList = new UIScrollList(1, yp, w-2, 60).setItems(items);
+    effectsList.addToContainer(this);
+    yp += effectsList.h + 10;
+    
+    final UIParameterKnob[] parameterKnobs = new UIParameterKnob[4];
+    for (int ki = 0; ki < parameterKnobs.length; ++ki) {
+      parameterKnobs[ki] = new UIParameterKnob(5 + 34*(ki % 4), yp + (ki/4) * 48);
+      parameterKnobs[ki].addToContainer(this);
+    }
+    
+    GLucose.EffectListener fxListener = new GLucose.EffectListener() {
+      public void effectSelected(LXEffect effect) {
+        int i = 0;
+        for (LXParameter p : effect.getParameters()) {
+          if (i >= parameterKnobs.length) {
+            break;
+          }
+          parameterKnobs[i++].setParameter(p);
+        }
+        while (i < parameterKnobs.length) {
+          parameterKnobs[i++].setParameter(null);
+        }
+      }
+    };
+    
+    glucose.addEffectListener(fxListener);
+    fxListener.effectSelected(glucose.getSelectedEffect());
+
+  }
+  
+  class FXScrollItem extends AbstractScrollItem {
+    
+    private LXEffect effect;
+    private String label;
+    
+    FXScrollItem(LXEffect effect) {
+      this.effect = effect;
+      label = className(effect, "Effect");
+    }
+    
+    public String getLabel() {
+      return label;
+    }
+    
+    public boolean isSelected() {
+      return !effect.isEnabled() && (glucose.getSelectedEffect() == effect);
+    }
+    
+    public boolean isPending() {
+      return effect.isEnabled();
+    }
+    
+    public void select() {
+      glucose.setSelectedEffect(effect);
+    }
+    
+    public void onMousePressed() {
+      if (glucose.getSelectedEffect() == effect) {
+        if (effect.isMomentary()) {
+          effect.enable();
+        } else {
+          effect.toggle();
+        }
+      }
+    }
+    
+    public void onMouseReleased() {
+      if (effect.isMomentary()) {
+        effect.disable();
+      }
+    }
+
+  }
+
+}
+
+class UIOutput extends UIWindow {
+  public UIOutput(float x, float y, float w, float h) {
+    super("OUTPUT", x, y, w, h);
+    float yp = titleHeight;
+    for (final PandaDriver panda : pandaBoards) {
+      final UIButton button = new UIButton(4, yp, w-8, 20) {
+        protected void onToggle(boolean active) {
+          panda.setEnabled(active);
+        }
+      }.setLabel(panda.ip);
+      button.addToContainer(this);
+      panda.setListener(new PandaDriver.Listener() {
+        public void onToggle(boolean active) {
+          button.setActive(active);
+        }
+      });
+      yp += 24;
+    }
+  }  
+}
+
+class UITempo extends UIWindow {
+  
+  private final UIButton tempoButton;
+  
+  UITempo(float x, float y, float w, float h) {
+    super("TEMPO", x, y, w, h);
+    tempoButton = new UIButton(4, titleHeight, w-8, 20) {
+      protected void onToggle(boolean active) {
+        if (active) {
+          lx.tempo.tap();
+        }
+      }
+    }.setMomentary(true);
+    tempoButton.addToContainer(this);
+  }
+  
+  public void draw() {
+    tempoButton.setLabel("" + ((int)(lx.tempo.bpm() * 10)) / 10.);
+    super.draw();
+    
+    // Overlay tempo thing with openGL, redraw faster than button UI
+    fill(color(0, 0, 24 - 8*lx.tempo.rampf()));
+    noStroke();
+    rect(x + 8, y + titleHeight + 5, 12, 12);
+  }
+}
+
+class UIMapping extends UIWindow {
+  
+  private static final String MAP_MODE_ALL = "ALL";
+  private static final String MAP_MODE_CHANNEL = "CHNL";
+  private static final String MAP_MODE_CUBE = "CUBE";
+  
+  private static final String CUBE_MODE_ALL = "ALL";
+  private static final String CUBE_MODE_STRIP = "SNGL";
+  private static final String CUBE_MODE_PATTERN = "PTRN";
+  
+  private final MappingTool mappingTool;
+  
+  private final UIIntegerBox channelBox;
+  private final UIIntegerBox cubeBox;
+  private final UIIntegerBox stripBox;
+  
+  UIMapping(MappingTool tool, float x, float y, float w, float h) {
+    super("MAPPING", x, y, w, h);
+    mappingTool = tool;
+    
+    int yp = titleHeight;
+    new UIToggleSet(4, yp, w-8, 20) {
+      protected void onToggle(String value) {
+        if (value == MAP_MODE_ALL) mappingTool.mappingMode = mappingTool.MAPPING_MODE_ALL;
+        else if (value == MAP_MODE_CHANNEL) mappingTool.mappingMode = mappingTool.MAPPING_MODE_CHANNEL;
+        else if (value == MAP_MODE_CUBE) mappingTool.mappingMode = mappingTool.MAPPING_MODE_SINGLE_CUBE;
+      }
+    }.setOptions(new String[] { MAP_MODE_ALL, MAP_MODE_CHANNEL, MAP_MODE_CUBE }).addToContainer(this);
+    yp += 24;
+    new UILabel(4, yp+8, w-8, 20).setLabel("CHANNEL ID").addToContainer(this);
+    yp += 24;
+    (channelBox = new UIIntegerBox(4, yp, w-8, 20) {
+      protected void onValueChange(int value) {
+        mappingTool.setChannel(value-1);
+      }
+    }).setRange(1, mappingTool.numChannels()).addToContainer(this);
+    yp += 24;
+    
+    new UILabel(4, yp+8, w-8, 20).setLabel("CUBE ID").addToContainer(this);
+    yp += 24;
+    (cubeBox = new UIIntegerBox(4, yp, w-8, 20) {
+      protected void onValueChange(int value) {
+        mappingTool.setCube(value-1);
+      }
+    }).setRange(1, glucose.model.cubes.size()).addToContainer(this);
+    yp += 24;
+    
+    new UILabel(4, yp+8, w-8, 20).setLabel("COLORS").addToContainer(this);
+    yp += 24;
+    
+    new UIScrollList(1, yp, w-2, 60).setItems(Arrays.asList(new ScrollItem[] {
+      new ColorScrollItem(ColorScrollItem.COLOR_RED),
+      new ColorScrollItem(ColorScrollItem.COLOR_GREEN),
+      new ColorScrollItem(ColorScrollItem.COLOR_BLUE),
+    })).addToContainer(this);
+    yp += 64;
+
+    new UILabel(4, yp+8, w-8, 20).setLabel("STRIP MODE").addToContainer(this);
+    yp += 24;
+    
+    new UIToggleSet(4, yp, w-8, 20) {
+      protected void onToggle(String value) {
+        if (value == CUBE_MODE_ALL) mappingTool.cubeMode = mappingTool.CUBE_MODE_ALL;
+        else if (value == CUBE_MODE_STRIP) mappingTool.cubeMode = mappingTool.CUBE_MODE_SINGLE_STRIP;
+        else if (value == CUBE_MODE_PATTERN) mappingTool.cubeMode = mappingTool.CUBE_MODE_STRIP_PATTERN;
+      }
+    }.setOptions(new String[] { CUBE_MODE_ALL, CUBE_MODE_STRIP, CUBE_MODE_PATTERN }).addToContainer(this);
+    
+    yp += 24;
+    new UILabel(4, yp+8, w-8, 20).setLabel("STRIP ID").addToContainer(this);
+    
+    yp += 24;
+    (stripBox = new UIIntegerBox(4, yp, w-8, 20) {
+      protected void onValueChange(int value) {
+        mappingTool.setStrip(value-1);
+      }
+    }).setRange(1, Cube.STRIPS_PER_CUBE).addToContainer(this);
+    
+  }
+  
+  public void setChannelID(int value) {
+    channelBox.setValue(value);
+  }
+
+  public void setCubeID(int value) {
+    cubeBox.setValue(value);
+  }
+
+  public void setStripID(int value) {
+    stripBox.setValue(value);
+  }
+  
+  class ColorScrollItem extends AbstractScrollItem {
+    
+    public static final int COLOR_RED = 1;
+    public static final int COLOR_GREEN = 2;
+    public static final int COLOR_BLUE = 3;
+    
+    private final int colorChannel;
+    
+    ColorScrollItem(int colorChannel) {
+      this.colorChannel = colorChannel;
+    }
+
+    public String getLabel() {
+      switch (colorChannel) {
+        case COLOR_RED: return "Red";
+        case COLOR_GREEN: return "Green";
+        case COLOR_BLUE: return "Blue";
+      }
+      return "";
+    }
+    
+    public boolean isSelected() {
+      switch (colorChannel) {
+        case COLOR_RED: return mappingTool.channelModeRed;
+        case COLOR_GREEN: return mappingTool.channelModeGreen;
+        case COLOR_BLUE: return mappingTool.channelModeBlue;
+      }
+      return false;
+    }
+    
+    public void select() {
+      switch (colorChannel) {
+        case COLOR_RED: mappingTool.channelModeRed = !mappingTool.channelModeRed; break;
+        case COLOR_GREEN: mappingTool.channelModeGreen = !mappingTool.channelModeGreen; break;
+        case COLOR_BLUE: mappingTool.channelModeBlue = !mappingTool.channelModeBlue; break;
+      }
+    }
+  }
+}
+
+class UIDebugText extends UIContext {
+  
+  private String line1 = "";
+  private String line2 = "";
+  
+  UIDebugText(float x, float y, float w, float h) {
+    super(x, y, w, h);
+  }
+
+  public UIDebugText setText(String line1) {
+    return setText(line1, "");
+  }
+  
+  public UIDebugText setText(String line1, String line2) {
+    if (!line1.equals(this.line1) || !line2.equals(this.line2)) {
+      this.line1 = line1;
+      this.line2 = line2;
+      setVisible(line1.length() + line2.length() > 0);
+      redraw();
+    }
+    return this;
+  }
+  
+  protected void onDraw(PGraphics pg) {
+    super.onDraw(pg);
+    if (line1.length() + line2.length() > 0) {
+      pg.noStroke();
+      pg.fill(#444444);
+      pg.rect(0, 0, w, h);
+      pg.textFont(defaultItemFont);
+      pg.textAlign(LEFT, TOP);
+      pg.fill(#cccccc);
+      pg.text(line1, 4, 4);
+      pg.text(line2, 4, 24);
+    }
+  }
+}
+
+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;
+}
index c36c6e992b80915841af721effbb5d42337da86e..6d09722e5cdbccf4cd146259475530dab83dc460 100755 (executable)
Binary files a/code/GLucose.jar and b/code/GLucose.jar differ
index 0445f9c712379acae4ce4499a2b08a9095cc2f02..145f480757192cea433b50366cc80e7fa57c6b2e 100755 (executable)
Binary files a/code/HeronLX.jar and b/code/HeronLX.jar differ