diff --git a/VERSION b/VERSION
index dd2601c..cdae3ef 100644
--- a/VERSION
+++ b/VERSION
@@ -1,3 +1,9 @@
+0.2.1
+October 22, 2020
+- COVID lockdown
+- Improvements to fixture/structure/JSON system
+- Many tweaks and improvements, new patterns/effects
+
0.2.0
May 28, 2020
- Lots of API cleanups
@@ -6,12 +12,12 @@ May 28, 2020
0.1.2
August 20, 2018
-- Fix bug with onActive/onInactive messages when pattern is deleted.
+-Fix bug with onActive/onInactive messages when pattern is deleted.
0.1.1
August 15, 2018
-- Dropping alpha tag, updating release packaging with VERSION and LICENSE files.
+-Dropping alpha tag, updating release packaging with VERSION and LICENSE files.
0.1.0-alpha
August 13, 2018
-- Initial release cut for tracking versioned changes across the LX repositories.
+-Initial release cut for tracking versioned changes across the LX repositories.
diff --git a/assets/icons/icon-lfo.psd b/assets/icons/icon-lfo.psd
index 1e63c8c..5b591bd 100644
Binary files a/assets/icons/icon-lfo.psd and b/assets/icons/icon-lfo.psd differ
diff --git a/pom.xml b/pom.xml
index 3165010..9aa1ea6 100644
--- a/pom.xml
+++ b/pom.xml
@@ -4,7 +4,7 @@
heronarts
glx
- 0.2.0
+ 0.2.1
jar
diff --git a/resources/cert/chromatik-0.2.1.pem b/resources/cert/chromatik-0.2.1.pem
new file mode 100644
index 0000000..e14dc64
--- /dev/null
+++ b/resources/cert/chromatik-0.2.1.pem
@@ -0,0 +1,32 @@
+-----BEGIN CERTIFICATE-----
+MIIFejCCA2ICCQCrNTZvV8zjLDANBgkqhkiG9w0BAQsFADB/MQswCQYDVQQGEwJV
+UzELMAkGA1UECAwCQ0ExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xEjAQBgNVBAoM
+CUNocm9tYXRpazEVMBMGA1UEAwwMY2hyb21hdGlrLmNvMSAwHgYJKoZIhvcNAQkB
+FhFtYXJrQGNocm9tYXRpay5jbzAeFw0yMDA1MzAwMTIzMjVaFw0yNTA1MjkwMTIz
+MjVaMH8xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTEWMBQGA1UEBwwNU2FuIEZy
+YW5jaXNjbzESMBAGA1UECgwJQ2hyb21hdGlrMRUwEwYDVQQDDAxjaHJvbWF0aWsu
+Y28xIDAeBgkqhkiG9w0BCQEWEW1hcmtAY2hyb21hdGlrLmNvMIICIjANBgkqhkiG
+9w0BAQEFAAOCAg8AMIICCgKCAgEA12C3wnA4laYnLwoZEmfKdHbuzV2SCdUtT5yU
+FGC3HbWuZZQoJdcbWQ7apUR6Bno0S70JzTDWxMuWI28DLyDoDXLuFxuqAiv+Kr6g
+K3aeSqNdiuY8PZ9AKqmgCpMK9DRsRQKNRqoY1tDJzyqXF+drcWGqM4ZU+25BRf+x
+SsxF0LTpKQ0N7mlwTSKqfnsPQQCAnHdpkA2RhWGe5m0q5v6lez0iw9ukhxBzTsRr
+xfNwt+u8YvwtzeLkzf6RLJKw1UYtbtShWBNf+IutfD2JRO8olOxCvnBjzlQDx3up
+ZiOjQjVmGeZbBsdvDJHdADnAvJN1Cr0q2DXKZFAK/r0J2gzHJuUH0N/zPiY/UXM+
+HpeGG6U+cG7xsQrBtWdx1539NqVSYR+DYUpiJYodOJubEuAf3LYkUtoJEF+L/QEp
+Zi4Fb7LuQ13WLEQL6gufmfI7L3kkN9qED8BBSQXKCFVGMlvDXHrcuGbSnkTqqIOk
+u6amibSTjHZzoWZVL4jsQ+WiW5lm7XYGgQ8q1+Cvda578EDIIcNBTQN4Ro00K2Qz
+m+/RIz71+qTHRK/1u09/tIZk1SY94J6b1xgzx4ZPobmus9vVbyDl1/FWjfWmO3JI
+AfhgMIgyp0dfyfgDR+GQxKdyprHjdzglNZGyOTX0hWWVfkAOqhVo/7lqnw4T3LBw
+pg1nlOMCAwEAATANBgkqhkiG9w0BAQsFAAOCAgEAJN5X0g/rPajKVi9UlzytLcC9
+I8HUVEZ5QHkLXSsI399sm87Muf/Lzs7tJoJ2Gs1hZ+z0eC8/ghc9xU8K/h58e+Ls
+YEmLObF5jvUWXJTRzqql/X9ZDy/u85rybBAC0HMvEZKIUlGN9JbOC3KO8q43nhcR
+088oEmrOGiJhSucO5rGwgJRVfQadZeGITg3FMPMHatlFiq3/WlM11ENwA2f65g8w
+FcfbadoOpNd+92w9si0NqYE2OTrHjqn9b/2VddNhbgISyHVcoLEy/Fij17zeQOZf
+jUL6vb5P81hHajep3gMotQlgh0BLgxhRcF45tPzJzgn4HmO2dJb7XEl0cqKMSQ5H
+2bOtTh45vFPaioHCwENEEqv6nVANUQzLfsHWlrWFsbiPm1mfnVC3LTOk3s4kXpEh
+AGXojBpWD1xsxdszxDzt4teYFc1SInM2f+m8bIl4zoEylFlDxKpR2SNgvdU++Gy3
+Luny1KbhKye3Hg3a4MAWoe4YNbwptWY01gjNwP/o6kkd00v78hBs8ULtDOG7SoXG
+ZlCiFOSBsck086vHxe3ZgjsAGfDUv3TDAmQKaQy/hO9rvdYdDWgQiaP5L5Xddea7
+fely2DIX0CWfpvl3nQj0/xETNzU+PeMIy2XyaRetCVA3l2XpFGs5RxT/7gh+6H4Q
+iaDUBY64Ol281/jN9mc=
+-----END CERTIFICATE-----
diff --git a/resources/icons/icon-lfo@2x.png b/resources/icons/icon-lfo@2x.png
index de09742..f2835e9 100644
Binary files a/resources/icons/icon-lfo@2x.png and b/resources/icons/icon-lfo@2x.png differ
diff --git a/resources/images/chromatik@2x.png b/resources/images/chromatik@2x.png
index c7dcd3a..718fe9b 100644
Binary files a/resources/images/chromatik@2x.png and b/resources/images/chromatik@2x.png differ
diff --git a/src/main/java/heronarts/glx/GLX.java b/src/main/java/heronarts/glx/GLX.java
index 9ef9bdf..c17841a 100644
--- a/src/main/java/heronarts/glx/GLX.java
+++ b/src/main/java/heronarts/glx/GLX.java
@@ -53,11 +53,13 @@
import heronarts.glx.ui.vg.VGraphics;
import heronarts.lx.LX;
import heronarts.lx.LXEngine;
-import heronarts.lx.command.LXCommand;
-import heronarts.lx.model.LXModel;
+import heronarts.lx.utils.LXUtils;
public class GLX extends LX {
+ private static final int MIN_WINDOW_WIDTH = 820;
+ private static final int MIN_WINDOW_HEIGHT = 480;
+
private long window;
private long handCursor;
@@ -118,11 +120,7 @@ public static class Flags extends LX.Flags {
public final Flags flags;
protected GLX(Flags flags) throws IOException {
- this(flags, null);
- }
-
- protected GLX(Flags flags, LXModel model) throws IOException {
- super(flags, model);
+ super(flags);
this.flags = flags;
// Get initial window size from preferences
@@ -445,6 +443,8 @@ private void initializeWindow() {
log("Using BGFX renderer: " + rendererName);
}
+ private boolean setWindowSizeLimits = true;
+
private void setUIZoom(float uiScale) {
this.uiZoom = uiScale;
this.uiWidth = this.frameBufferWidth / this.systemContentScaleX / this.uiZoom;
@@ -454,6 +454,7 @@ private void setUIZoom(float uiScale) {
this.vg.notifyContentScaleChanged();
this.ui.resize();
this.ui.redraw();
+ this.setWindowSizeLimits = true;
}
protected void setWindowSize(int windowWidth, int windowHeight) {
@@ -493,6 +494,22 @@ private void loop() {
boolean failed = false;
while (!glfwWindowShouldClose(this.window)) {
+
+ // Update window size limits
+ if (this.setWindowSizeLimits) {
+ this.setWindowSizeLimits = false;
+ int minWindowWidth = (int) (MIN_WINDOW_WIDTH / this.cursorScaleX);
+ int minWindowHeight = (int) (MIN_WINDOW_HEIGHT / this.cursorScaleY);
+ glfwSetWindowSizeLimits(this.window, minWindowWidth, minWindowHeight, GLFW_DONT_CARE, GLFW_DONT_CARE);
+ if (this.windowWidth < minWindowWidth || this.windowHeight < minWindowHeight) {
+ glfwSetWindowSize(
+ this.window,
+ LXUtils.max(this.windowWidth, minWindowWidth),
+ LXUtils.max(this.windowHeight, minWindowHeight)
+ );
+ }
+ }
+
// Poll for input events
this.inputDispatch.poll();
@@ -578,36 +595,14 @@ public void reloadContent() {
this.ui.contextualHelpText.setValue("External content libraries reloaded");
}
- // Prevent stacking up multiple dialogs
- private volatile boolean dialogShowing = false;
-
public void showSaveProjectDialog() {
- if (this.dialogShowing) {
- return;
- }
- new Thread() {
- @Override
- public void run() {
- try (MemoryStack stack = MemoryStack.stackPush()) {
- PointerBuffer aFilterPatterns = stack.mallocPointer(1);
- aFilterPatterns.put(stack.UTF8("*.lxp"));
- aFilterPatterns.flip();
- dialogShowing = true;
- String path = tinyfd_saveFileDialog(
- "Save Project",
- getMediaFolder(LX.Media.PROJECTS).toString() + File.separator + "default.lxp",
- aFilterPatterns,
- "LX Project files (*.lxp)"
- );
- dialogShowing = false;
- if (path != null) {
- engine.addTask(() -> {
- saveProject(new File(path));
- });
- }
- }
- }
- }.start();
+ showSaveFileDialog(
+ "Save Project",
+ "Project File",
+ new String[] { "lxp" },
+ getMediaFolder(LX.Media.PROJECTS).toString() + File.separator + "default.lxp",
+ (path) -> { saveProject(new File(path)); }
+ );
}
public void showOpenProjectDialog() {
@@ -615,65 +610,69 @@ public void showOpenProjectDialog() {
return;
}
confirmChangesSaved("open another project", () -> {
- new Thread() {
- @Override
- public void run() {
- try (MemoryStack stack = MemoryStack.stackPush()) {
- PointerBuffer aFilterPatterns = stack.mallocPointer(1);
- aFilterPatterns.put(stack.UTF8("*.lxp"));
- aFilterPatterns.flip();
- dialogShowing = true;
- String path = tinyfd_openFileDialog(
- "Open Project",
- new File(getMediaFolder(LX.Media.PROJECTS), ".").toString(),
- aFilterPatterns,
- "LX Project files (*.lxp)",
- false
- );
- dialogShowing = false;
- if (path != null) {
- engine.addTask(() -> {
- openProject(new File(path));
- });
- }
- }
- }
- }.start();
+ showOpenFileDialog(
+ "Open Project",
+ "Project File",
+ new String[] { "lxp" },
+ new File(getMediaFolder(LX.Media.PROJECTS), ".").toString(),
+ (path) -> { openProject(new File(path)); }
+ );
});
}
- public void showOpenAudioDialog() {
+ public void showSaveScheduleDialog() {
+ showSaveFileDialog(
+ "Save Schedule",
+ "Schedule File",
+ new String[] { "lxs" },
+ getMediaFolder(LX.Media.PROJECTS).toString() + File.separator + "default.lxs",
+ (path) -> { this.scheduler.saveSchedule(new File(path)); }
+ );
+ }
+
+ public void showAddScheduleEntryDialog() {
if (this.dialogShowing) {
return;
}
- new Thread() {
- @Override
- public void run() {
- try (MemoryStack stack = MemoryStack.stackPush()) {
- PointerBuffer aFilterPatterns = stack.mallocPointer(2);
- aFilterPatterns.put(stack.UTF8("*.wav"));
- aFilterPatterns.put(stack.UTF8("*.aiff"));
- aFilterPatterns.flip();
- dialogShowing = true;
- String path = tinyfd_openFileDialog(
- "Open Audio File",
- new File(getMediaPath(), ".").toString(),
- aFilterPatterns,
- "Audio files (*.wav/aiff)",
- false
- );
- dialogShowing = false;
- if (path != null) {
- engine.addTask(() -> {
- engine.audio.output.file.setValue(path);
- });
- }
- }
- }
- }.start();
+ showOpenFileDialog(
+ "Add Project to Schedule",
+ "Project File",
+ new String[] { "lxp" },
+ new File(getMediaFolder(LX.Media.PROJECTS), ".").toString(),
+ (path) -> { this.scheduler.addEntry(new File(path)); }
+ );
}
- public void showExportModelDialog() {
+ public void showOpenScheduleDialog() {
+ if (this.dialogShowing) {
+ return;
+ }
+ showOpenFileDialog(
+ "Open Schedule",
+ "Schedule File",
+ new String[] { "lxs" },
+ new File(getMediaFolder(LX.Media.PROJECTS), ".").toString(),
+ (path) -> { this.scheduler.openSchedule(new File(path)); }
+ );
+ }
+
+ public interface FileDialogCallback {
+ public void fileDialogCallback(String path);
+ }
+
+ // Prevent stacking up multiple dialogs
+ private volatile boolean dialogShowing = false;
+
+ /**
+ * Show a save file dialog
+ *
+ * @param dialogTitle Dialog title
+ * @param fileType File type description
+ * @param extensions Valid file extensions
+ * @param defaultPath Default file path
+ * @param success Callback on successful invocation
+ */
+ public void showSaveFileDialog(String dialogTitle, String fileType, String[] extensions, String defaultPath, FileDialogCallback success) {
if (this.dialogShowing) {
return;
}
@@ -681,34 +680,39 @@ public void showExportModelDialog() {
@Override
public void run() {
try (MemoryStack stack = MemoryStack.stackPush()) {
- PointerBuffer aFilterPatterns = stack.mallocPointer(1);
- aFilterPatterns.put(stack.UTF8("*.lxm"));
+ PointerBuffer aFilterPatterns = stack.mallocPointer(extensions.length);
+ for (String extension : extensions) {
+ aFilterPatterns.put(stack.UTF8("*." + extension));
+ }
aFilterPatterns.flip();
dialogShowing = true;
String path = tinyfd_saveFileDialog(
- "Export Model",
- getMediaFolder(LX.Media.MODELS).toString() + File.separator + "Model.lxm",
+ dialogTitle,
+ defaultPath,
aFilterPatterns,
- "LX Model files (*.lxm)"
- );
- dialogShowing = false;
- if (path != null) {
- engine.addTask(() -> {
- structure.exportModel(new File(path));
- });
- }
+ fileType + " (*." + String.join("/", extensions) + ")"
+ );
+ dialogShowing = false;
+ if (path != null) {
+ engine.addTask(() -> {
+ success.fileDialogCallback(path);
+ });
+ }
}
}
}.start();
}
- public void showNewModelDialog() {
- showConfirmDialog("Are you sure you wish to clear the current model?", () -> {
- this.command.perform(new LXCommand.Structure.NewModel(this.structure));
- });
- }
-
- public void showImportModelDialog() {
+ /**
+ * Show an open file dialog
+ *
+ * @param dialogTitle Dialog title
+ * @param fileType File type description
+ * @param extensions Valid file extensions
+ * @param defaultPath Default file path
+ * @param success Callback on successful invocation
+ */
+ public void showOpenFileDialog(String dialogTitle, String fileType, String[] extensions, String defaultPath, FileDialogCallback success) {
if (this.dialogShowing) {
return;
}
@@ -716,27 +720,28 @@ public void showImportModelDialog() {
@Override
public void run() {
try (MemoryStack stack = MemoryStack.stackPush()) {
- PointerBuffer aFilterPatterns = stack.mallocPointer(1);
- aFilterPatterns.put(stack.UTF8("*.lxm"));
+ PointerBuffer aFilterPatterns = stack.mallocPointer(extensions.length);
+ for (String extension : extensions) {
+ aFilterPatterns.put(stack.UTF8("*." + extension));
+ }
aFilterPatterns.flip();
dialogShowing = true;
String path = tinyfd_openFileDialog(
- "Import Model",
- new File(getMediaFolder(LX.Media.MODELS), ".").toString(),
+ dialogTitle,
+ defaultPath,
aFilterPatterns,
- "LX Model files (*.lxm)",
+ fileType + " (*." + String.join("/", extensions) + ")",
false
);
dialogShowing = false;
if (path != null) {
engine.addTask(() -> {
- structure.importModel(new File(path));
+ success.fileDialogCallback(path);
});
}
}
}
}.start();
-
}
@Override
@@ -747,7 +752,7 @@ protected void showConfirmUnsavedProjectDialog(String message, Runnable confirm)
);
}
- protected void showConfirmDialog(String message, Runnable confirm) {
+ public void showConfirmDialog(String message, Runnable confirm) {
this.ui.showContextOverlay(new UIDialogBox(this.ui,
message,
new String[] { "No", "Yes" },
diff --git a/src/main/java/heronarts/glx/GLXUtils.java b/src/main/java/heronarts/glx/GLXUtils.java
index e54ff55..415b21c 100644
--- a/src/main/java/heronarts/glx/GLXUtils.java
+++ b/src/main/java/heronarts/glx/GLXUtils.java
@@ -86,8 +86,8 @@ public int get(int x, int y) {
public int getNormalized(float x, float y) {
return get(
- (int) (x * (this.width-1)),
- (int) (y * (this.height-1))
+ (int) (x * (this.width - .5f)),
+ (int) (y * (this.height - .5f))
);
}
}
@@ -118,20 +118,40 @@ public static ByteBuffer loadShader(GLX glx, String name) throws IOException {
return loadResource(path + name + ".bin");
}
+ /**
+ * Gets an input stream for the resource at the given path
+ *
+ * @param resourcePath
+ * @return
+ * @throws IOException
+ */
+ public static InputStream loadResourceStream(String resourcePath) throws IOException {
+ Path path = Paths.get(resourcePath);
+ if (Files.isReadable(path)) {
+ return Files.newInputStream(path);
+ }
+
+ URL url = GLXUtils.class.getResource(resourcePath);
+ if (url == null) {
+ throw new IOException("Resource not found: " + resourcePath);
+ }
+ return url.openStream();
+ }
+
/**
* Loads the resource at the given path into a newly allocated buffer. The buffer is owned by
* the caller and must be freed explicitly.
*
- * @param path Path to the resource
+ * @param resourcePath Path to the resource
* @return Buffer allocated by MemoryUtil
* @throws IOException If there is an error loading the resource
*/
- public static ByteBuffer loadResource(String path) throws IOException {
+ public static ByteBuffer loadResource(String resourcePath) throws IOException {
ByteBuffer resource = null;
- Path file = Paths.get(path);
- if (Files.isReadable(file)) {
+ Path path = Paths.get(resourcePath);
+ if (Files.isReadable(path)) {
try (
- SeekableByteChannel fc = Files.newByteChannel(file);
+ SeekableByteChannel fc = Files.newByteChannel(path);
) {
resource = MemoryUtil.memAlloc((int) fc.size() + 1);
while (fc.read(resource) != -1);
@@ -143,9 +163,9 @@ public static ByteBuffer loadResource(String path) throws IOException {
}
}
- URL url = GLXUtils.class.getResource(path);
+ URL url = GLXUtils.class.getResource(resourcePath);
if (url == null) {
- throw new IOException("Resource not found: " + path);
+ throw new IOException("Resource not found: " + resourcePath);
}
int resourceSize = url.openConnection().getContentLength();
resource = MemoryUtil.memAlloc(resourceSize);
diff --git a/src/main/java/heronarts/glx/ui/CustomDeviceUI.java b/src/main/java/heronarts/glx/ui/CustomDeviceUI.java
deleted file mode 100644
index bc3f409..0000000
--- a/src/main/java/heronarts/glx/ui/CustomDeviceUI.java
+++ /dev/null
@@ -1,23 +0,0 @@
-/**
- * Copyright 2013- Mark C. Slee, Heron Arts LLC
- *
- * This file is part of the LX Studio software library. By using
- * LX, you agree to the terms of the LX Studio Software License
- * and Distribution Agreement, available at: http://lx.studio/license
- *
- * Please note that the LX license is not open-source. The license
- * allows for free, non-commercial use.
- *
- * HERON ARTS MAKES NO WARRANTY, EXPRESS, IMPLIED, STATUTORY, OR
- * OTHERWISE, AND SPECIFICALLY DISCLAIMS ANY WARRANTY OF
- * MERCHANTABILITY, NON-INFRINGEMENT, OR FITNESS FOR A PARTICULAR
- * PURPOSE, WITH RESPECT TO THE SOFTWARE.
- *
- * @author Mark C. Slee
- */
-
-package heronarts.glx.ui;
-
-public interface CustomDeviceUI {
- public abstract void buildDeviceUI(UI ui, UI2dContainer device);
-}
diff --git a/src/main/java/heronarts/glx/ui/UI.java b/src/main/java/heronarts/glx/ui/UI.java
index 7aafd1a..1994d48 100644
--- a/src/main/java/heronarts/glx/ui/UI.java
+++ b/src/main/java/heronarts/glx/ui/UI.java
@@ -589,7 +589,7 @@ private boolean isMapping() {
return this.midiMapping || this.modulationSourceMapping || this.modulationTargetMapping || this.triggerSourceMapping || this.triggerTargetMapping;
}
- void setMouseoverHelpText(String helpText) {
+ public void setMouseoverHelpText(String helpText) {
if (!isMapping()) {
this.contextualHelpText.setValue(helpText);
}
diff --git a/src/main/java/heronarts/glx/ui/UI3dContext.java b/src/main/java/heronarts/glx/ui/UI3dContext.java
index cca1752..5f30c7e 100644
--- a/src/main/java/heronarts/glx/ui/UI3dContext.java
+++ b/src/main/java/heronarts/glx/ui/UI3dContext.java
@@ -53,6 +53,7 @@ public class UI3dContext extends UIObject implements LXSerializable, UILayer, UI
public static final int NUM_CAMERA_POSITIONS = 6;
public static interface MovementListener {
+ public void reset();
public void translate(float x, float y, float z);
public void rotate(float theta, float phi);
}
@@ -123,10 +124,9 @@ public String toString() {
/**
* Perspective of view
*/
- public final BoundedParameter perspective = (BoundedParameter)
- new BoundedParameter("Perspective", 60, 15, 150)
- .setExponent(2)
- .setDescription("Camera perspective factor");
+ public final BoundedParameter perspective = new BoundedParameter("Perspective", 60, 15, 150)
+ .setExponent(2)
+ .setDescription("Camera perspective factor");
/**
* Depth of perspective field, exponential factor of radius by exp(10, Depth)
diff --git a/src/main/java/heronarts/glx/ui/UITheme.java b/src/main/java/heronarts/glx/ui/UITheme.java
index 22e1ef7..c461356 100644
--- a/src/main/java/heronarts/glx/ui/UITheme.java
+++ b/src/main/java/heronarts/glx/ui/UITheme.java
@@ -53,6 +53,8 @@ public class UITheme {
private int focusSelectionColor = 0xff393939;
private int errorColor = 0xffff0000;
+ private int secondaryListItemColor = 0xff666666;
+
private int darkBackgroundColor = 0xff191919;
private int darkFocusBackgroundColor = 0xff292929;
@@ -450,6 +452,24 @@ public int getErrorColor() {
return this.errorColor;
}
+ /**
+ * Primary list item selection color
+ *
+ * @return Color of main item selected in list
+ */
+ public int getPrimaryListItemColor() {
+ return getPrimaryColor();
+ }
+
+ /**
+ * Secondary list item selection color
+ *
+ * @return Color of secondary item selected in list
+ */
+ public int getSecondaryListItemColor() {
+ return this.secondaryListItemColor;
+ }
+
/**
* Gets highlight color
*
@@ -470,7 +490,6 @@ public UITheme setSurfaceColor(int color) {
return this;
}
-
/**
* Gets highlight color
*
diff --git a/src/main/java/heronarts/glx/ui/component/UIButton.java b/src/main/java/heronarts/glx/ui/component/UIButton.java
index 54a18b8..3797afa 100644
--- a/src/main/java/heronarts/glx/ui/component/UIButton.java
+++ b/src/main/java/heronarts/glx/ui/component/UIButton.java
@@ -42,12 +42,21 @@ public Action(float w, float h) {
this(0, 0, w, h);
}
+ public Action(float w, float h, String label) {
+ this(0, 0, w, h, label);
+ }
+
public Action(float x, float y, float w, float h) {
super(x, y, w, h);
setBorderRounding(8);
setMomentary(true);
}
+ public Action(float x, float y, float w, float h, String label) {
+ this(x, y, w, h);
+ setLabel(label);
+ }
+
}
public static class Trigger extends UIButton {
@@ -70,6 +79,8 @@ public Trigger(UI ui, BooleanParameter trigger, float x, float y) {
}
}
+ private LXParameter controlTarget = null;
+
protected boolean active = false;
protected boolean isMomentary = false;
@@ -513,12 +524,30 @@ public UIButton setInactiveIcon(VGraphics.Image inactiveIcon) {
return this;
}
+ /**
+ * Sets an explicit control target for the button, which may or may not match
+ * its other parameter behavior. Useful for buttons that need to perform a
+ * custom LXCommand rather than explicitly change parameter value, but still
+ * should be mappable for modulation and MIDI.
+ *
+ * @param controlTarget Control target
+ * @return this
+ */
+ public UIButton setControlTarget(LXParameter controlTarget) {
+ this.controlTarget = controlTarget;
+ return this;
+ }
+
@Override
public LXParameter getControlTarget() {
+ if (this.controlTarget != null) {
+ // If one is explicitly set, doesn't have to match the rest
+ return this.controlTarget;
+ }
if (isMappable()) {
if (this.enumParameter != null) {
if (this.enumParameter.getParent() != null) {
- return this.enumParameter;
+ return this.enumParameter.isMappable() ? this.enumParameter : null;
}
} else {
return getTriggerParameter();
diff --git a/src/main/java/heronarts/glx/ui/component/UICheckbox.java b/src/main/java/heronarts/glx/ui/component/UICheckbox.java
new file mode 100644
index 0000000..c8299b3
--- /dev/null
+++ b/src/main/java/heronarts/glx/ui/component/UICheckbox.java
@@ -0,0 +1,240 @@
+package heronarts.glx.ui.component;
+
+import java.util.Objects;
+
+import heronarts.glx.event.KeyEvent;
+import heronarts.glx.event.MouseEvent;
+import heronarts.glx.ui.UI;
+import heronarts.glx.ui.UIControlTarget;
+import heronarts.glx.ui.UIFocus;
+import heronarts.glx.ui.UITriggerSource;
+import heronarts.glx.ui.UITriggerTarget;
+import heronarts.glx.ui.vg.VGraphics;
+import heronarts.lx.command.LXCommand;
+import heronarts.lx.parameter.BooleanParameter;
+import heronarts.lx.parameter.LXListenableNormalizedParameter;
+import heronarts.lx.parameter.LXParameter;
+import heronarts.lx.parameter.LXParameterListener;
+
+public class UICheckbox extends UIParameterComponent implements UIControlTarget, UITriggerSource, UITriggerTarget, UIFocus {
+
+ public final static int DEFAULT_WIDTH = 10;
+ public final static int DEFAULT_HEIGHT = 10;
+
+ protected boolean active = false;
+ protected boolean isMomentary = false;
+
+ private boolean triggerable = false;
+
+ protected boolean enabled = true;
+
+ private BooleanParameter parameter = null;
+
+ private final LXParameterListener parameterListener = (p) -> {
+ setActive(this.parameter.isOn(), false);
+ };
+
+ public UICheckbox() {
+ this(0, 0, DEFAULT_WIDTH, DEFAULT_HEIGHT);
+ }
+
+ public UICheckbox(float w, BooleanParameter p) {
+ this(0, 0, w, w, p);
+ }
+
+ public UICheckbox(float x, float y) {
+ this(x, y, DEFAULT_WIDTH, DEFAULT_HEIGHT);
+ }
+
+ public UICheckbox(float x, float y, BooleanParameter p) {
+ this(x, y, DEFAULT_WIDTH, DEFAULT_HEIGHT, p);
+ }
+
+ public UICheckbox(float x, float y, float w, float h) {
+ this(x, y, w, h, null);
+ }
+
+ public UICheckbox(float x, float y, float w, float h, BooleanParameter p) {
+ super(x, y, w, h);
+ setParameter(p);
+ }
+
+ public UICheckbox setEnabled(boolean enabled) {
+ if (this.enabled != enabled) {
+ this.enabled = enabled;
+ redraw();
+ }
+ return this;
+ }
+
+ public UICheckbox setTriggerable(boolean triggerable) {
+ this.triggerable = triggerable;
+ return this;
+ }
+
+ @Override
+ public String getDescription() {
+ if (this.parameter != null) {
+ return UIParameterControl.getDescription(this.parameter);
+ }
+ return super.getDescription();
+ }
+
+ @Override
+ public LXListenableNormalizedParameter getParameter() {
+ return this.parameter;
+ }
+
+ public UICheckbox removeParameter() {
+ if (this.parameter != null) {
+ this.parameter.removeListener(this.parameterListener);
+ }
+ this.parameter = null;
+ return this;
+ }
+
+ public UICheckbox setParameter(BooleanParameter parameter) {
+ Objects.requireNonNull(parameter, "Cannot set null UICheckbox.setParameter() - use removeParameter() instead");
+ if (parameter != this.parameter) {
+ removeParameter();
+ if (parameter != null) {
+ this.parameter = parameter;
+ this.parameter.addListener(this.parameterListener);
+ setMomentary(this.parameter.getMode() == BooleanParameter.Mode.MOMENTARY);
+ setActive(this.parameter.isOn(), false);
+ }
+ }
+ return this;
+ }
+
+ public UICheckbox setMomentary(boolean momentary) {
+ this.isMomentary = momentary;
+ return this;
+ }
+
+ @Override
+ protected void onDraw(UI ui, VGraphics vg) {
+ // A lighter gray background color when the button is disabled, or it's engaged
+ // with a mouse press but the mouse has moved off the active button
+ int color = this.enabled ? ui.theme.getControlTextColor() : ui.theme.getControlDisabledColor();
+
+ vg.beginPath();
+ vg.strokeColor(color);
+ vg.rect(1.5f, 1.5f, this.width-3, this.height-3);
+ vg.stroke();
+
+ if (this.active) {
+ vg.beginPath();
+ vg.fillColor(color);
+ vg.rect(3, 3, this.width - 6, this.height - 6);
+ vg.fill();
+ }
+ }
+
+ @Override
+ protected void onMousePressed(MouseEvent mouseEvent, float mx, float my) {
+ if (this.enabled) {
+ setActive(this.isMomentary ? true : !this.active);
+ }
+ }
+
+ @Override
+ protected void onMouseReleased(MouseEvent mouseEvent, float mx, float my) {
+ if (this.enabled) {
+ if (this.isMomentary) {
+ setActive(false);
+ }
+ }
+ }
+
+ @Override
+ protected void onKeyPressed(KeyEvent keyEvent, char keyChar, int keyCode) {
+ if ((keyCode == java.awt.event.KeyEvent.VK_SPACE) || (keyCode == java.awt.event.KeyEvent.VK_ENTER)) {
+ keyEvent.consume();
+ if (this.enabled) {
+ setActive(this.isMomentary ? true : !this.active);
+ }
+ }
+ }
+
+ @Override
+ protected void onKeyReleased(KeyEvent keyEvent, char keyChar, int keyCode) {
+ if ((keyCode == java.awt.event.KeyEvent.VK_SPACE) || (keyCode == java.awt.event.KeyEvent.VK_ENTER)) {
+ keyEvent.consume();
+ if (this.enabled && this.isMomentary) {
+ setActive(false);
+ }
+ }
+ }
+
+ public boolean isActive() {
+ return this.active;
+ }
+
+ public UICheckbox setActive(boolean active) {
+ return setActive(active, true);
+ }
+
+ protected UICheckbox setActive(boolean active, boolean pushToParameter) {
+ if (this.active != active) {
+ this.active = active;
+ if (pushToParameter) {
+ if (this.parameter != null) {
+ if (this.isMomentary) {
+ this.parameter.setValue(active);
+ } else {
+ getLX().command.perform(new LXCommand.Parameter.SetNormalized(this.parameter, active));
+ }
+ }
+ }
+ onToggle(active);
+ redraw();
+ }
+ return this;
+ }
+
+ public UICheckbox toggle() {
+ return setActive(!this.active);
+ }
+
+ /**
+ * Subclasses may override this to handle changes to the button's state
+ *
+ * @param active Whether button is active
+ */
+ protected void onToggle(boolean active) {
+ }
+
+
+ @Override
+ public LXParameter getControlTarget() {
+ if (isMappable()) {
+ if (this.parameter != null) {
+ if (this.parameter.getParent() != null) {
+ return this.parameter;
+ }
+ } else {
+ return getTriggerParameter();
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public BooleanParameter getTriggerSource() {
+ return this.triggerable ? getTriggerParameter() : null;
+ }
+
+ @Override
+ public BooleanParameter getTriggerTarget() {
+ return this.triggerable ? getTriggerParameter() : null;
+ }
+
+ private BooleanParameter getTriggerParameter() {
+ if (this.parameter != null && this.parameter.isMappable() && this.parameter.getParent() != null) {
+ return this.parameter;
+ }
+ return null;
+ }
+
+}
diff --git a/src/main/java/heronarts/glx/ui/component/UICollapsibleSection.java b/src/main/java/heronarts/glx/ui/component/UICollapsibleSection.java
index 04afa21..d5a3fed 100644
--- a/src/main/java/heronarts/glx/ui/component/UICollapsibleSection.java
+++ b/src/main/java/heronarts/glx/ui/component/UICollapsibleSection.java
@@ -103,18 +103,12 @@ public void onDraw(UI ui, VGraphics vg) {
vg.fill();
vg.fillColor(ui.theme.getControlTextColor());
- if (this.expanded) {
- vg.beginPath();
- vg.moveTo(this.width-7, 8.5f);
- vg.lineTo(this.width-13, 8.5f);
- vg.lineTo(this.width-10, 12.5f);
- vg.closePath();
- vg.fill();
- } else {
- vg.beginPath();
- vg.circle(this.width-10, 10, 2);
- vg.fill();
+ vg.beginPath();
+ vg.rect(width-13, 9, 6, 2);
+ if (!this.expanded) {
+ vg.rect(width-11, 7, 2, 6);
}
+ vg.fill();
}
/**
diff --git a/src/main/java/heronarts/glx/ui/component/UIColorPicker.java b/src/main/java/heronarts/glx/ui/component/UIColorPicker.java
index fee62bc..d228a29 100644
--- a/src/main/java/heronarts/glx/ui/component/UIColorPicker.java
+++ b/src/main/java/heronarts/glx/ui/component/UIColorPicker.java
@@ -47,6 +47,8 @@ public enum Corner {
private UIColorOverlay uiColorOverlay = null;
+ private boolean enabled = true;
+
public UIColorPicker(ColorParameter color) {
this(UIKnob.WIDTH, UIKnob.WIDTH, color);
}
@@ -81,6 +83,11 @@ protected void run() {
}
}
+ public UIColorPicker setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ return this;
+ }
+
private final LXParameterListener redrawSwatch = (p) -> {
if (this.uiColorOverlay != null) {
this.uiColorOverlay.swatch.redraw();
@@ -138,20 +145,24 @@ private void showOverlay() {
@Override
public void onMousePressed(MouseEvent mouseEvent, float mx, float my) {
- mouseEvent.consume();
- showOverlay();
+ if (this.enabled) {
+ mouseEvent.consume();
+ showOverlay();
+ }
super.onMousePressed(mouseEvent, mx, my);
}
@Override
public void onKeyPressed(KeyEvent keyEvent, char keyChar, int keyCode) {
- if (keyCode == KeyEvent.VK_ENTER || keyCode == KeyEvent.VK_SPACE) {
- keyEvent.consume();
- showOverlay();
- } else if (keyCode == KeyEvent.VK_ESCAPE) {
- if ((this.uiColorOverlay != null) && (this.uiColorOverlay.isVisible())) {
+ if (this.enabled) {
+ if (keyCode == KeyEvent.VK_ENTER || keyCode == KeyEvent.VK_SPACE) {
keyEvent.consume();
- hideOverlay();
+ showOverlay();
+ } else if (keyCode == KeyEvent.VK_ESCAPE) {
+ if ((this.uiColorOverlay != null) && (this.uiColorOverlay.isVisible())) {
+ keyEvent.consume();
+ hideOverlay();
+ }
}
}
super.onKeyPressed(keyEvent, keyChar, keyCode);
diff --git a/src/main/java/heronarts/glx/ui/component/UIDoubleBox.java b/src/main/java/heronarts/glx/ui/component/UIDoubleBox.java
index 9423fbc..a875203 100644
--- a/src/main/java/heronarts/glx/ui/component/UIDoubleBox.java
+++ b/src/main/java/heronarts/glx/ui/component/UIDoubleBox.java
@@ -59,11 +59,18 @@ public UIDoubleBox(float w, BoundedParameter parameter) {
this(0, 0, w, parameter);
}
+ public UIDoubleBox(float w, float h, BoundedParameter parameter) {
+ this(0, 0, w, h, parameter);
+ }
+
public UIDoubleBox(float x, float y, float w, BoundedParameter parameter) {
- this(x, y, w, DEFAULT_HEIGHT);
- setParameter(parameter);
+ this(x, y, w, DEFAULT_HEIGHT, parameter);
}
+ public UIDoubleBox(float x, float y, float w, float h, BoundedParameter parameter) {
+ this(x, y, w, h);
+ setParameter(parameter);
+ }
public UIDoubleBox setEditMultiplier(double editMultiplier) {
this.editMultiplier = editMultiplier;
diff --git a/src/main/java/heronarts/glx/ui/component/UIInputBox.java b/src/main/java/heronarts/glx/ui/component/UIInputBox.java
index 8d601f2..3850e64 100644
--- a/src/main/java/heronarts/glx/ui/component/UIInputBox.java
+++ b/src/main/java/heronarts/glx/ui/component/UIInputBox.java
@@ -48,6 +48,7 @@ public interface ProgressIndicator {
protected boolean returnKeyEdit = true;
private boolean immediateEdit = false;
+ private boolean mousePressEdit = false;
private ProgressIndicator progressMeter;
private int progressPixels = 0;
@@ -120,6 +121,11 @@ public UIInputBox enableImmediateEdit(boolean immediateEdit) {
return this;
}
+ public UIInputBox enableMousePressEdit(boolean mousePressEdit) {
+ this.mousePressEdit = mousePressEdit;
+ return this;
+ }
+
protected abstract String getValueString();
protected abstract void saveEditBuffer();
@@ -353,6 +359,11 @@ protected void onMousePressed(MouseEvent mouseEvent, float mx, float my) {
this.mouseDragSetValue = new LXCommand.Parameter.SetValue(parameter, 0);
}
}
+ if (this.enabled && this.editable && !this.editing && this.mousePressEdit) {
+ mouseEvent.consume();
+ edit();
+ redraw();
+ }
}
@Override
diff --git a/src/main/java/heronarts/glx/ui/component/UIIntegerBox.java b/src/main/java/heronarts/glx/ui/component/UIIntegerBox.java
index ed76988..3fd0edf 100644
--- a/src/main/java/heronarts/glx/ui/component/UIIntegerBox.java
+++ b/src/main/java/heronarts/glx/ui/component/UIIntegerBox.java
@@ -36,10 +36,8 @@ public class UIIntegerBox extends UINumberBox implements UIControlTarget {
protected DiscreteParameter parameter = null;
protected int editMultiplier = 1;
- private final LXParameterListener parameterListener = new LXParameterListener() {
- public void onParameterChanged(LXParameter p) {
- setValue(parameter.getValuei(), false);
- }
+ private final LXParameterListener parameterListener = (p) -> {
+ setValue(this.parameter.getValuei(), false);
};
public UIIntegerBox() {
@@ -64,6 +62,11 @@ public String getDescription() {
return UIParameterControl.getDescription(this.parameter);
}
+ @Override
+ public LXParameter getParameter() {
+ return this.parameter;
+ }
+
public UIIntegerBox setParameter(final DiscreteParameter parameter) {
if (this.parameter != null) {
this.parameter.removeListener(this.parameterListener);
diff --git a/src/main/java/heronarts/glx/ui/component/UIParameterComponent.java b/src/main/java/heronarts/glx/ui/component/UIParameterComponent.java
index 631cb3d..102306c 100644
--- a/src/main/java/heronarts/glx/ui/component/UIParameterComponent.java
+++ b/src/main/java/heronarts/glx/ui/component/UIParameterComponent.java
@@ -22,9 +22,7 @@ protected UIParameterComponent(float x, float y, float w, float h) {
super(x, y, w, h);
}
- public LXParameter getParameter() {
- return null;
- }
+ public abstract LXParameter getParameter();
public UIParameterComponent setUseCommandEngine(boolean useCommandEngine) {
this.useCommandEngine = useCommandEngine;
diff --git a/src/main/java/heronarts/glx/ui/component/UITextBox.java b/src/main/java/heronarts/glx/ui/component/UITextBox.java
index 5eae556..dc95adc 100644
--- a/src/main/java/heronarts/glx/ui/component/UITextBox.java
+++ b/src/main/java/heronarts/glx/ui/component/UITextBox.java
@@ -35,10 +35,8 @@ public class UITextBox extends UIInputBox implements UICopy, UIPaste {
private String value = NO_VALUE;
private StringParameter parameter = null;
- private final LXParameterListener parameterListener = new LXParameterListener() {
- public void onParameterChanged(LXParameter p) {
- setValue(parameter.getString(), false);
- }
+ private final LXParameterListener parameterListener = (p) -> {
+ setValue(this.parameter.getString(), false);
};
public UITextBox() {
@@ -49,6 +47,11 @@ public UITextBox(float x, float y, float w, float h) {
super(x, y, w, h);
}
+ @Override
+ public LXParameter getParameter() {
+ return this.parameter;
+ }
+
public UITextBox setParameter(StringParameter parameter) {
if (this.parameter != null) {
this.parameter.removeListener(this.parameterListener);
@@ -134,7 +137,7 @@ protected boolean isValidCharacter(char keyChar) {
protected void onMousePressed(MouseEvent mouseEvent, float mx, float my) {
super.onMousePressed(mouseEvent, mx, my);
if (this.enabled && !this.editing) {
- if (mouseEvent.getCount() == 2) {
+ if (mouseEvent.getButton() == MouseEvent.BUTTON_LEFT && mouseEvent.getCount() == 2) {
mouseEvent.consume();
this.edit();
redraw();
diff --git a/src/main/java/heronarts/glx/ui/vg/VGraphics.java b/src/main/java/heronarts/glx/ui/vg/VGraphics.java
index 64df9c4..2736438 100644
--- a/src/main/java/heronarts/glx/ui/vg/VGraphics.java
+++ b/src/main/java/heronarts/glx/ui/vg/VGraphics.java
@@ -229,7 +229,7 @@ public boolean isStale() {
public Paint getPaint() {
if (this.buffer == null) {
- throw new IllegalStateException("Cannot use VGraphics.Framebuffer.getPaint() before initialize()");
+ GLX.error("Cannot use VGraphics.Framebuffer.getPaint() before initialize()");
}
return this.paint;
}