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; }