Change code to use new thread-safe lx color functions
[SugarCubes.git] / _UIFramework.pde
1 /**
2 * DOUBLE BLACK DIAMOND DOUBLE BLACK DIAMOND
3 *
4 * //\\ //\\ //\\ //\\
5 * ///\\\ ///\\\ ///\\\ ///\\\
6 * \\\/// \\\/// \\\/// \\\///
7 * \\// \\// \\// \\//
8 *
9 * EXPERTS ONLY!! EXPERTS ONLY!!
10 *
11 * Little UI framework in progress to handle mouse events, layout,
12 * redrawing, etc.
13 */
14
15 final color lightGreen = #669966;
16 final color lightBlue = #666699;
17 final color bgGray = #444444;
18 final color defaultTextColor = #999999;
19 final PFont defaultItemFont = createFont("Lucida Grande", 11);
20 final PFont defaultTitleFont = createFont("Myriad Pro", 10);
21
22 public abstract class UIObject {
23
24 protected final List<UIObject> children = new ArrayList<UIObject>();
25
26 protected boolean needsRedraw = true;
27 protected boolean childNeedsRedraw = true;
28
29 protected float x=0, y=0, w=0, h=0;
30
31 public UIContainer parent = null;
32
33 protected boolean visible = true;
34
35 public UIObject() {}
36
37 public UIObject(float x, float y, float w, float h) {
38 this.x = x;
39 this.y = y;
40 this.w = w;
41 this.h = h;
42 }
43
44 public boolean isVisible() {
45 return visible;
46 }
47
48 public UIObject setVisible(boolean visible) {
49 if (visible != this.visible) {
50 this.visible = visible;
51 redraw();
52 }
53 return this;
54 }
55
56 public final UIObject setPosition(float x, float y) {
57 this.x = x;
58 this.y = y;
59 redraw();
60 return this;
61 }
62
63 public final UIObject setSize(float w, float h) {
64 this.w = w;
65 this.h = h;
66 redraw();
67 return this;
68 }
69
70 public final UIObject addToContainer(UIContainer c) {
71 c.children.add(this);
72 this.parent = c;
73 return this;
74 }
75
76 public final UIObject removeFromContainer(UIContainer c) {
77 c.children.remove(this);
78 this.parent = null;
79 return this;
80 }
81
82 public final UIObject redraw() {
83 _redraw();
84 UIObject p = this.parent;
85 while (p != null) {
86 p.childNeedsRedraw = true;
87 p = p.parent;
88 }
89 return this;
90 }
91
92 private final void _redraw() {
93 needsRedraw = true;
94 for (UIObject child : children) {
95 childNeedsRedraw = true;
96 child._redraw();
97 }
98 }
99
100 public final void draw(PGraphics pg) {
101 if (!visible) {
102 return;
103 }
104 if (needsRedraw) {
105 needsRedraw = false;
106 onDraw(pg);
107 }
108 if (childNeedsRedraw) {
109 childNeedsRedraw = false;
110 for (UIObject child : children) {
111 if (needsRedraw || child.needsRedraw || child.childNeedsRedraw) {
112 pg.pushMatrix();
113 pg.translate(child.x, child.y);
114 child.draw(pg);
115 pg.popMatrix();
116 }
117 }
118 }
119 }
120
121 public final boolean contains(float x, float y) {
122 return
123 (x >= this.x && x < (this.x + this.w)) &&
124 (y >= this.y && y < (this.y + this.h));
125 }
126
127 protected void onDraw(PGraphics pg) {}
128 protected void onMousePressed(float mx, float my) {}
129 protected void onMouseReleased(float mx, float my) {}
130 protected void onMouseDragged(float mx, float my, float dx, float dy) {}
131 protected void onMouseWheel(float mx, float my, float dx) {}
132 }
133
134 public class UIContainer extends UIObject {
135
136 private UIObject focusedChild = null;
137
138 public UIContainer() {}
139
140 public UIContainer(float x, float y, float w, float h) {
141 super(x, y, w, h);
142 }
143
144 public UIContainer(UIObject[] children) {
145 for (UIObject child : children) {
146 child.addToContainer(this);
147 }
148 }
149
150 protected void onMousePressed(float mx, float my) {
151 for (int i = children.size() - 1; i >= 0; --i) {
152 UIObject child = children.get(i);
153 if (child.contains(mx, my)) {
154 child.onMousePressed(mx - child.x, my - child.y);
155 focusedChild = child;
156 break;
157 }
158 }
159 }
160
161 protected void onMouseReleased(float mx, float my) {
162 if (focusedChild != null) {
163 focusedChild.onMouseReleased(mx - focusedChild.x, my - focusedChild.y);
164 }
165 focusedChild = null;
166 }
167
168 protected void onMouseDragged(float mx, float my, float dx, float dy) {
169 if (focusedChild != null) {
170 focusedChild.onMouseDragged(mx - focusedChild.x, my - focusedChild.y, dx, dy);
171 }
172 }
173
174 protected void onMouseWheel(float mx, float my, float delta) {
175 for (UIObject child : children) {
176 if (child.contains(mx, my)) {
177 child.onMouseWheel(mx - child.x, mx - child.y, delta);
178 }
179 }
180 }
181
182 }
183
184 public class UIContext extends UIContainer {
185
186 final public PGraphics pg;
187
188 UIContext(float x, float y, float w, float h) {
189 super(x, y, w, h);
190 pg = createGraphics((int)w, (int)h, JAVA2D);
191 pg.smooth();
192 }
193
194 public void draw() {
195 if (!visible) {
196 return;
197 }
198 if (needsRedraw || childNeedsRedraw) {
199 pg.beginDraw();
200 draw(pg);
201 pg.endDraw();
202 }
203 image(pg, x, y);
204 }
205
206 private float px, py;
207 private boolean dragging = false;
208
209 public boolean mousePressed(float mx, float my) {
210 if (!visible) {
211 return false;
212 }
213 if (contains(mx, my)) {
214 dragging = true;
215 px = mx;
216 py = my;
217 onMousePressed(mx - x, my - y);
218 return true;
219 }
220 return false;
221 }
222
223 public boolean mouseReleased(float mx, float my) {
224 if (!visible) {
225 return false;
226 }
227 dragging = false;
228 onMouseReleased(mx - x, my - y);
229 return true;
230 }
231
232 public boolean mouseDragged(float mx, float my) {
233 if (!visible) {
234 return false;
235 }
236 if (dragging) {
237 float dx = mx - px;
238 float dy = my - py;
239 onMouseDragged(mx - x, my - y, dx, dy);
240 px = mx;
241 py = my;
242 return true;
243 }
244 return false;
245 }
246
247 public boolean mouseWheel(float mx, float my, float delta) {
248 if (!visible) {
249 return false;
250 }
251 if (contains(mx, my)) {
252 onMouseWheel(mx - x, my - y, delta);
253 return true;
254 }
255 return false;
256 }
257 }
258
259 public class UIWindow extends UIContext {
260
261 protected final static int titleHeight = 24;
262
263 public UIWindow(String label, float x, float y, float w, float h) {
264 super(x, y, w, h);
265 new UILabel(6, 8, w-6, titleHeight-8) {
266 protected void onMouseDragged(float mx, float my, float dx, float dy) {
267 parent.x = constrain(parent.x + dx, 0, width - w);
268 parent.y = constrain(parent.y + dy, 0, height - h);
269 }
270 }.setLabel(label).setFont(defaultTitleFont).addToContainer(this);
271 }
272
273 protected void onDraw(PGraphics pg) {
274 pg.noStroke();
275 pg.fill(#444444);
276 pg.stroke(#292929);
277 pg.rect(0, 0, w-1, h-1);
278 }
279 }
280
281 public class UILabel extends UIObject {
282
283 private PFont font = defaultTitleFont;
284 private color fontColor = #CCCCCC;
285 private String label = "";
286
287 public UILabel(float x, float y, float w, float h) {
288 super(x, y, w, h);
289 }
290
291 protected void onDraw(PGraphics pg) {
292 pg.textAlign(LEFT, TOP);
293 pg.textFont(font);
294 pg.fill(fontColor);
295 pg.text(label, 0, 0);
296 }
297
298 public UILabel setFont(PFont font) {
299 this.font = font;
300 redraw();
301 return this;
302 }
303
304 public UILabel setFontColor(color fontColor) {
305 this.fontColor = fontColor;
306 redraw();
307 return this;
308 }
309
310 public UILabel setLabel(String label) {
311 this.label = label;
312 redraw();
313 return this;
314 }
315 }
316
317 public class UICheckbox extends UIButton {
318
319 private boolean firstDraw = true;
320
321 public UICheckbox(float x, float y, float w, float h) {
322 super(x, y, w, h);
323 setMomentary(false);
324 }
325
326 public void onDraw(PGraphics pg) {
327 pg.stroke(borderColor);
328 pg.fill(active ? activeColor : inactiveColor);
329 pg.rect(0, 0, h, h);
330 if (firstDraw) {
331 pg.fill(labelColor);
332 pg.textFont(defaultItemFont);
333 pg.textAlign(LEFT, CENTER);
334 pg.text(label, h + 4, h/2);
335 firstDraw = false;
336 }
337 }
338
339 }
340
341 public class UIButton extends UIObject {
342
343 protected boolean active = false;
344 protected boolean isMomentary = false;
345 protected color borderColor = #666666;
346 protected color inactiveColor = #222222;
347 protected color activeColor = #669966;
348 protected color labelColor = #999999;
349 protected String label = "";
350
351 public UIButton(float x, float y, float w, float h) {
352 super(x, y, w, h);
353 }
354
355 public UIButton setMomentary(boolean momentary) {
356 isMomentary = momentary;
357 return this;
358 }
359
360 protected void onDraw(PGraphics pg) {
361 pg.stroke(borderColor);
362 pg.fill(active ? activeColor : inactiveColor);
363 pg.rect(0, 0, w, h);
364 if (label != null && label.length() > 0) {
365 pg.fill(active ? #FFFFFF : labelColor);
366 pg.textFont(defaultItemFont);
367 pg.textAlign(CENTER);
368 pg.text(label, w/2, h-5);
369 }
370 }
371
372 protected void onMousePressed(float mx, float my) {
373 if (isMomentary) {
374 setActive(true);
375 } else {
376 setActive(!active);
377 }
378 }
379
380 protected void onMouseReleased(float mx, float my) {
381 if (isMomentary) {
382 setActive(false);
383 }
384 }
385
386 public boolean isActive() {
387 return active;
388 }
389
390 public UIButton setActive(boolean active) {
391 this.active = active;
392 onToggle(active);
393 redraw();
394 return this;
395 }
396
397 public UIButton toggle() {
398 return setActive(!active);
399 }
400
401 protected void onToggle(boolean active) {}
402
403 public UIButton setBorderColor(color borderColor) {
404 if (this.borderColor != borderColor) {
405 this.borderColor = borderColor;
406 redraw();
407 }
408 return this;
409 }
410
411 public UIButton setActiveColor(color activeColor) {
412 if (this.activeColor != activeColor) {
413 this.activeColor = activeColor;
414 if (active) {
415 redraw();
416 }
417 }
418 return this;
419 }
420
421 public UIButton setInactiveColor(color inactiveColor) {
422 if (this.inactiveColor != inactiveColor) {
423 this.inactiveColor = inactiveColor;
424 if (!active) {
425 redraw();
426 }
427 }
428 return this;
429 }
430
431 public UIButton setLabelColor(color labelColor) {
432 if (this.labelColor != labelColor) {
433 this.labelColor = labelColor;
434 redraw();
435 }
436 return this;
437 }
438
439 public UIButton setLabel(String label) {
440 if (!this.label.equals(label)) {
441 this.label = label;
442 redraw();
443 }
444 return this;
445 }
446
447 public void onMousePressed() {
448 setActive(!active);
449 }
450 }
451
452 public class UIToggleSet extends UIObject {
453
454 private String[] options;
455 private int[] boundaries;
456 private String value;
457
458 public UIToggleSet(float x, float y, float w, float h) {
459 super(x, y, w, h);
460 }
461
462 public UIToggleSet setOptions(String[] options) {
463 this.options = options;
464 boundaries = new int[options.length];
465 int totalLength = 0;
466 for (String s : options) {
467 totalLength += s.length();
468 }
469 int lengthSoFar = 0;
470 for (int i = 0; i < options.length; ++i) {
471 lengthSoFar += options[i].length();
472 boundaries[i] = (int) (lengthSoFar * w / totalLength);
473 }
474 value = options[0];
475 redraw();
476 return this;
477 }
478
479 public String getValue() {
480 return value;
481 }
482
483 public UIToggleSet setValue(String option) {
484 value = option;
485 onToggle(value);
486 redraw();
487 return this;
488 }
489
490 public void onDraw(PGraphics pg) {
491 pg.stroke(#666666);
492 pg.fill(#222222);
493 pg.rect(0, 0, w, h);
494 for (int b : boundaries) {
495 pg.line(b, 1, b, h-1);
496 }
497 pg.noStroke();
498 pg.textAlign(CENTER);
499 pg.textFont(defaultItemFont);
500 int leftBoundary = 0;
501
502 for (int i = 0; i < options.length; ++i) {
503 boolean isActive = options[i] == value;
504 if (isActive) {
505 pg.fill(lightGreen);
506 pg.rect(leftBoundary + 1, 1, boundaries[i] - leftBoundary - 1, h-1);
507 }
508 pg.fill(isActive ? #FFFFFF : #999999);
509 pg.text(options[i], (leftBoundary + boundaries[i]) / 2., h-6);
510 leftBoundary = boundaries[i];
511 }
512 }
513
514 public void onMousePressed(float mx, float my) {
515 for (int i = 0; i < boundaries.length; ++i) {
516 if (mx < boundaries[i]) {
517 setValue(options[i]);
518 break;
519 }
520 }
521 }
522
523 protected void onToggle(String option) {}
524
525 }
526
527
528 public abstract class UIParameterControl extends UIObject implements LXParameter.Listener {
529 protected LXParameter parameter = null;
530
531 protected UIParameterControl(float x, float y, float w, float h) {
532 super(x, y, w, h);
533 }
534
535 public void onParameterChanged(LXParameter parameter) {
536 redraw();
537 }
538
539 public UIParameterControl setParameter(LXParameter parameter) {
540 if (this.parameter != null) {
541 if (this.parameter instanceof LXListenableParameter) {
542 ((LXListenableParameter)this.parameter).removeListener(this);
543 }
544 }
545 this.parameter = parameter;
546 if (this.parameter != null) {
547 if (this.parameter instanceof LXListenableParameter) {
548 ((LXListenableParameter)this.parameter).addListener(this);
549 }
550 }
551 redraw();
552 return this;
553 }
554 }
555
556 public class UIParameterKnob extends UIParameterControl {
557 private int knobSize = 28;
558 private final float knobIndent = .4;
559 private final int knobLabelHeight = 14;
560
561 public UIParameterKnob(float x, float y) {
562 this(x, y, 0, 0);
563 setSize(knobSize, knobSize + knobLabelHeight);
564 }
565
566 public UIParameterKnob(float x, float y, float w, float h) {
567 super(x, y, w, h);
568 }
569
570 protected void onDraw(PGraphics pg) {
571 float knobValue = (parameter != null) ? parameter.getValuef() : 0;
572
573 pg.ellipseMode(CENTER);
574 pg.noStroke();
575
576 pg.fill(bgGray);
577 pg.rect(0, 0, knobSize, knobSize);
578
579 // Full outer dark ring
580 pg.fill(#222222);
581 pg.arc(knobSize/2, knobSize/2, knobSize, knobSize, HALF_PI + knobIndent, HALF_PI + knobIndent + (TWO_PI-2*knobIndent));
582
583 // Light ring indicating value
584 pg.fill(lightGreen);
585 pg.arc(knobSize/2, knobSize/2, knobSize, knobSize, HALF_PI + knobIndent, HALF_PI + knobIndent + knobValue*(TWO_PI-2*knobIndent));
586
587 // Center circle of knob
588 pg.fill(#333333);
589 pg.ellipse(knobSize/2, knobSize/2, knobSize/2, knobSize/2);
590
591 String knobLabel = (parameter != null) ? parameter.getLabel() : null;
592 if (knobLabel == null) {
593 knobLabel = "-";
594 } else if (knobLabel.length() > 4) {
595 knobLabel = knobLabel.substring(0, 4);
596 }
597 pg.fill(#000000);
598 pg.rect(0, knobSize + 2, knobSize, knobLabelHeight - 2);
599 pg.fill(#999999);
600 pg.textAlign(CENTER);
601 pg.textFont(defaultTitleFont);
602 pg.text(knobLabel, knobSize/2, knobSize + knobLabelHeight - 2);
603 }
604
605 public void onMouseDragged(float mx, float my, float dx, float dy) {
606 if (parameter != null) {
607 float value = constrain(parameter.getValuef() - dy / 100., 0, 1);
608 parameter.setValue(value);
609 }
610 }
611 }
612
613 public class UIParameterSlider extends UIParameterControl {
614
615 private static final float handleWidth = 12;
616
617 UIParameterSlider(float x, float y, float w, float h) {
618 super(x, y, w, h);
619 }
620
621 protected void onDraw(PGraphics pg) {
622 pg.noStroke();
623 pg.fill(#333333);
624 pg.rect(0, 0, w, h);
625 pg.fill(#222222);
626 pg.rect(4, h/2-2, w-8, 4);
627 pg.fill(#666666);
628 pg.stroke(#222222);
629 pg.rect((int) (4 + parameter.getValuef() * (w-8-handleWidth)), 4, handleWidth, h-8);
630 }
631
632 private boolean editing = false;
633 private long lastClick = 0;
634 private float doubleClickMode = 0;
635 private float doubleClickX = 0;
636 protected void onMousePressed(float mx, float my) {
637 long now = millis();
638 float handleLeft = 4 + parameter.getValuef() * (w-8-handleWidth);
639 if (mx >= handleLeft && mx < handleLeft + handleWidth) {
640 editing = true;
641 } else {
642 if ((now - lastClick) < 300 && abs(mx - doubleClickX) < 3) {
643 parameter.setValue(doubleClickMode);
644 }
645 doubleClickX = mx;
646 if (mx < w*.25) {
647 doubleClickMode = 0;
648 } else if (mx > w*.75) {
649 doubleClickMode = 1;
650 } else {
651 doubleClickMode = 0.5;
652 }
653 }
654 lastClick = now;
655 }
656
657 protected void onMouseReleased(float mx, float my) {
658 editing = false;
659 }
660
661 protected void onMouseDragged(float mx, float my, float dx, float dy) {
662 if (editing) {
663 parameter.setValue(constrain((mx - handleWidth/2. - 4) / (w-8-handleWidth), 0, 1));
664 }
665 }
666 }
667
668 public class UIScrollList extends UIObject {
669
670 private List<ScrollItem> items = new ArrayList<ScrollItem>();
671
672 private PFont itemFont = defaultItemFont;
673 private int itemHeight = 20;
674 private color selectedColor = lightGreen;
675 private color pendingColor = lightBlue;
676 private int scrollOffset = 0;
677 private int numVisibleItems = 0;
678
679 private boolean hasScroll;
680 private float scrollYStart;
681 private float scrollYHeight;
682
683 public UIScrollList(float x, float y, float w, float h) {
684 super(x, y, w, h);
685 }
686
687 protected void onDraw(PGraphics pg) {
688 int yp = 0;
689 boolean even = true;
690 for (int i = 0; i < numVisibleItems; ++i) {
691 if (i + scrollOffset >= items.size()) {
692 break;
693 }
694 ScrollItem item = items.get(i + scrollOffset);
695 color itemColor;
696 color labelColor = #FFFFFF;
697 if (item.isSelected()) {
698 itemColor = selectedColor;
699 } else if (item.isPending()) {
700 itemColor = pendingColor;
701 } else {
702 labelColor = #000000;
703 itemColor = #707070;
704 }
705 float factor = even ? .92 : 1.08;
706 itemColor = lx.scaleBrightness(itemColor, factor);
707
708 pg.noStroke();
709 pg.fill(itemColor);
710 pg.rect(0, yp, w, itemHeight);
711 pg.fill(labelColor);
712 pg.textFont(itemFont);
713 pg.textAlign(LEFT, TOP);
714 pg.text(item.getLabel(), 6, yp+4);
715
716 yp += itemHeight;
717 even = !even;
718 }
719 if (hasScroll) {
720 pg.noStroke();
721 pg.fill(0x26ffffff);
722 pg.rect(w-12, 0, 12, h);
723 pg.fill(#333333);
724 pg.rect(w-12, scrollYStart, 12, scrollYHeight);
725 }
726
727 }
728
729 private boolean scrolling = false;
730 private ScrollItem pressedItem = null;
731
732 public void onMousePressed(float mx, float my) {
733 pressedItem = null;
734 if (hasScroll && mx >= w-12) {
735 if (my >= scrollYStart && my < (scrollYStart + scrollYHeight)) {
736 scrolling = true;
737 dAccum = 0;
738 }
739 } else {
740 int index = (int) my / itemHeight;
741 if (scrollOffset + index < items.size()) {
742 pressedItem = items.get(scrollOffset + index);
743 pressedItem.onMousePressed();
744 redraw();
745 }
746 }
747 }
748
749 public void onMouseReleased(float mx, float my) {
750 scrolling = false;
751 if (pressedItem != null) {
752 pressedItem.onMouseReleased();
753 redraw();
754 }
755 }
756
757 private float dAccum = 0;
758 public void onMouseDragged(float mx, float my, float dx, float dy) {
759 if (scrolling) {
760 dAccum += dy;
761 float scrollOne = h / items.size();
762 int offset = (int) (dAccum / scrollOne);
763 if (offset != 0) {
764 dAccum -= offset * scrollOne;
765 setScrollOffset(scrollOffset + offset);
766 }
767 }
768 }
769
770 private float wAccum = 0;
771 public void onMouseWheel(float mx, float my, float delta) {
772 wAccum += delta;
773 int offset = (int) (wAccum / 5);
774 if (offset != 0) {
775 wAccum -= offset * 5;
776 setScrollOffset(scrollOffset + offset);
777 }
778 }
779
780 public void setScrollOffset(int offset) {
781 scrollOffset = constrain(offset, 0, max(0, items.size() - numVisibleItems));
782 scrollYStart = round(scrollOffset * h / items.size());
783 scrollYHeight = round(numVisibleItems * h / items.size());
784 redraw();
785 }
786
787 public UIScrollList setItems(List<ScrollItem> items) {
788 this.items = items;
789 numVisibleItems = (int) (h / itemHeight);
790 hasScroll = items.size() > numVisibleItems;
791 setScrollOffset(0);
792 redraw();
793 return this;
794 }
795 }
796
797 public interface ScrollItem {
798 public boolean isSelected();
799 public boolean isPending();
800 public String getLabel();
801 public void onMousePressed();
802 public void onMouseReleased();
803 }
804
805 public abstract class AbstractScrollItem implements ScrollItem {
806 public boolean isPending() {
807 return false;
808 }
809 public void select() {}
810 public void onMousePressed() {}
811 public void onMouseReleased() {}
812 }
813
814 public class UIIntegerBox extends UIObject {
815
816 private int minValue = 0;
817 private int maxValue = MAX_INT;
818 private int value = 0;
819
820 UIIntegerBox(float x, float y, float w, float h) {
821 super(x, y, w, h);
822 }
823
824 public UIIntegerBox setRange(int minValue, int maxValue) {
825 this.minValue = minValue;
826 this.maxValue = maxValue;
827 setValue(constrain(value, minValue, maxValue));
828 return this;
829 }
830
831 protected void onDraw(PGraphics pg) {
832 pg.stroke(#666666);
833 pg.fill(#222222);
834 pg.rect(0, 0, w, h);
835 pg.textAlign(CENTER, CENTER);
836 pg.textFont(defaultItemFont);
837 pg.fill(#999999);
838 pg.text("" + value, w/2, h/2);
839 }
840
841 protected void onValueChange(int value) {}
842
843 float dAccum = 0;
844 protected void onMousePressed(float mx, float my) {
845 dAccum = 0;
846 }
847
848 protected void onMouseDragged(float mx, float my, float dx, float dy) {
849 dAccum -= dy;
850 int offset = (int) (dAccum / 5);
851 dAccum = dAccum - (offset * 5);
852 setValue(value + offset);
853 }
854
855 public int getValue() {
856 return value;
857 }
858
859 public UIIntegerBox setValue(int value) {
860 if (this.value != value) {
861 int range = (maxValue - minValue + 1);
862 while (value < minValue) {
863 value += range;
864 }
865 this.value = minValue + (value - minValue) % range;
866 this.onValueChange(this.value);
867 redraw();
868 }
869 return this;
870 }
871 }