// Menus tab has ResponsiveText, Menu, Menu-derived classes. // See tab LoopState for OSC, the display's state machine, and note looping. /** ResponsiveText holds 1 or more chars in a String * and checks boundbox with a mouse press. **/ class ResponsiveText { String mytext ; PFont font ; int pointSize ; int x ; int y ; Menu menu ; ResponsiveText(String mytext, PFont font, int pointSize, int x, int y, Menu menu) { this.mytext = mytext ; this.font = font ; this.pointSize = pointSize ; this.x = x ; this.y = y ; this.menu = menu ; } // Call setMyFont() before printing or pixel width,height measurements. void setMyFont() { if (font != null) { textFont(font, pointSize) ; } else { textSize(pointSize); } textAlign(LEFT, TOP); } void display(boolean highlight) { setMyFont(); // set for display if (highlight) { // Underline the highlighted text using // Processing's line library function in the same color. // You have the x,y location of the left,top of the // text; call getWidth() and getHeight() to determine // the right and bottom coordinates of the text; put // the underline a little below the bottom. stroke(60, 99, 99); // yellow strokeWeight(4); fill(60, 99, 99); // yellow line(x, y+getHeight()-2, x+getWidth(), y+getHeight()-2); } else { fill(180, 99, 99); // cyan } text(mytext, x, y); } boolean isInBoundingBox(int eventX, int eventY) { setMyFont(); // set for determining extents int right = x + round(textWidth(mytext)); int bottom = y + round(textAscent()+textDescent()); return (eventX >= x && eventX <= right && eventY >= y && eventY <= bottom); } int getWidth() { setMyFont(); return round(textWidth(mytext)); } int getHeight() { setMyFont(); return (round(textAscent()+textDescent())); } int getX() { return x ; } int getY() { return y ; } String getText() { return mytext ; } // setText works only if the incoming text has same number of chars void setText(String incoming) { if (mytext.length() == incoming.length()) { mytext = incoming ; } else { println("WARNING, ResponsiveText failed attempt to replace " + mytext.length() + "-character text '" + mytext + "' with " + incoming.length() + "-character text '" + incoming + " at " + x + "," + y); } } } /* Menu does most of the work of menu construction & display. */ abstract class Menu { String title ; boolean isSubmit ; boolean isCancel ; ResponsiveText submitButton = null ; ResponsiveText cancelButton = null ; ResponsiveText [][] buttons ; int [] activeRowPerColumn ; int maxcolumns = 0 ; String mytext ; PFont font ; int pointSize ; int leftx = 0 ; // columnsOfMulticharFields is indexed [COLUMN][ROW] because // brush names come in one array [1][numberOfBrushes], and // commands come in another [2][numberOfCommands]. This // approach generalizes to > 0 multi-character commands. Menu(String title, boolean isSubmit, boolean isCancel, String [][] columnsOfMulticharFields, // May be null!!! String [] rowsOfLetters, String [] rowsOfWords, PFont font, int pointSize, float rowSpacing, float colSpacing) { this.title = title ; this.isSubmit = isSubmit ; this.isCancel = isCancel ; this.font = font ; this.pointSize = pointSize ; setMyFont(); ResponsiveText fatText = new ResponsiveText("X", font, pointSize, -1, -1, null); int fatWidth = fatText.getWidth(); int verticalHeight = fatText.getHeight(); leftx = round(textWidth("i")) ; // start over at left int tmpx = leftx, tmpy = round(textAscent()+textDescent()) * 2 ; // Leave room for title if (isSubmit) { submitButton = new ResponsiveText(" Submit ", font, pointSize, tmpx, tmpy, this); tmpx += round(colSpacing * submitButton.getWidth()); } if (isCancel) { cancelButton = new ResponsiveText(" Cancel ", font, pointSize, tmpx, tmpy, this); } if (isSubmit || isCancel) { tmpy += round((textAscent()+textDescent())*rowSpacing) ; } tmpx = leftx ; // start over at left if (rowsOfWords != null) { rowsOfWords = rowsOfWords.clone(); // Do not mutate the original array parameter. columnsOfMulticharFields = null ; rowsOfLetters = null ; buttons = new ResponsiveText [ rowsOfWords.length ][1]; maxcolumns = 1 ; int maxlen = -1 ; for (int row = 0; row < rowsOfWords.length ; row++) { maxlen = max(maxlen, rowsOfWords[row].length()); } for (int row = 0; row < rowsOfWords.length ; row++) { while (rowsOfWords[row].length() < maxlen) { rowsOfWords[row] = " " + rowsOfWords[row]; } buttons[row] = new ResponsiveText [1]; buttons[row][0] = new ResponsiveText(rowsOfWords[row], font, pointSize, tmpx, tmpy, this); tmpx = leftx ; tmpy += verticalHeight ; } } else if (columnsOfMulticharFields == null) { buttons = new ResponsiveText [ rowsOfLetters.length ][]; for (int row = 0; row < rowsOfLetters.length; row++) { buttons[row] = new ResponsiveText [ rowsOfLetters[row].length() ]; maxcolumns = max(maxcolumns, rowsOfLetters[row].length()); for (int column = 0; column < rowsOfLetters[row].length(); column++) { buttons[row][column] = new ResponsiveText(""+rowsOfLetters[row].charAt(column), font, pointSize, tmpx, tmpy, this); tmpx += round(colSpacing * fatWidth); } tmpx = leftx ; tmpy += verticalHeight ; } } else { buttons = new ResponsiveText [ rowsOfLetters.length + columnsOfMulticharFields.length ][]; int rowcount = rowsOfLetters.length; String [] padding = new String [ columnsOfMulticharFields.length ]; String manyspaces = " "; for (int flds = 0; flds < columnsOfMulticharFields.length; flds++) { rowcount = max(rowcount, columnsOfMulticharFields[flds].length); padding[flds] = manyspaces.substring(0, // pad to fixed length of field columnsOfMulticharFields[flds][0].length()); } buttons = new ResponsiveText [ rowcount ][]; for (int row = 0; row < rowcount; row++) { if (row < rowsOfLetters.length) { buttons[row] = new ResponsiveText [ rowsOfLetters[row].length() + columnsOfMulticharFields.length ]; maxcolumns = max(maxcolumns, rowsOfLetters[row].length() + columnsOfMulticharFields.length); } else { buttons[row] = new ResponsiveText[ columnsOfMulticharFields.length ]; maxcolumns = max(maxcolumns, columnsOfMulticharFields.length); } for (int column = 0; column < columnsOfMulticharFields.length; column++) { //println("DEBUG1 ROW " + row + " COL " + column); if (row < buttons.length && column < buttons[row].length && column < columnsOfMulticharFields.length && row < columnsOfMulticharFields[column].length) { // DEBUG 11/11/2018 buttons[row][column] = new ResponsiveText( columnsOfMulticharFields[column][row], font, pointSize, tmpx, tmpy, this); } //println("AFTER DEBUG1 ROW " + row + " COL " + column); tmpx += round(colSpacing * fatWidth * padding[column].length()); } if (row < rowsOfLetters.length) { for (int col = 0; col < rowsOfLetters[row].length(); col++) { int column = col + columnsOfMulticharFields.length; buttons[row][column] = new ResponsiveText("" +rowsOfLetters[row].charAt(col), font, pointSize, tmpx, tmpy, this); tmpx += round(colSpacing * fatWidth); } } tmpx = leftx ; tmpy += verticalHeight ; } } activeRowPerColumn = new int [ maxcolumns ] ; } void setMyFont() { if (font != null) { textFont(font, pointSize) ; } else { textSize(pointSize); } textAlign(LEFT, TOP); } void display() { setMyFont(); fill(0, 0, 99); // white text(title, leftx, 10); if (submitButton != null) { submitButton.display(true); } if (cancelButton != null) { cancelButton.display(true); } for (int row = 0; row < buttons.length; row++) { for (int column = 0; column < buttons[row].length; column++) { // println("DEBUG2 ROW " + row + " COL " + column); if (buttons[row][column] != null) { // DEBUG 11/11/2018 // println("DEBUG3 activeRowPerColumn.length: " + activeRowPerColumn.length); if (activeRowPerColumn[column] == row) { buttons[row][column].display(true); } else { buttons[row][column].display(false); } } // println("DEBUG2 AFTER ROW " + row + " COL " + column); } } } abstract void doSubmit(); abstract void doCancel(); void respondToMouseEvent(int eventX, int eventY) { if (submitButton != null && submitButton.isInBoundingBox(eventX, eventY)) { doSubmit(); } else if (cancelButton != null && cancelButton.isInBoundingBox(eventX, eventY)) { doCancel(); } else { for (int row = 0; row < buttons.length; row++) { for (int column = 0; column < buttons[row].length; column++) { if (buttons[row][column] != null // DEBUG 11/11/2018 && buttons[row][column].isInBoundingBox(eventX, eventY)) { //println("DEBUG MATCHED BUTTON AT ROW " + row + "," //+ " COL " + column + "," + buttons[row][column].getText()); activeRowPerColumn[column] = row ; //this.display(); return ; } } } } } } String [] SetServerMIDIChannelNumber = { "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15" }; class SetClientMidiChannel extends Menu { String errorString = null ; SetClientMidiChannel() { super("Set your Client MIDI channel", true, false, // Cancel not acceptable here. null, null, SetServerMIDIChannelNumber ,globalFont, round(globalPointSize*1.0), 1.5, 1.5); } void display() { super.display(); if (errorString != null) { printDEBUG("SetClientMidiChannel error: " + errorString); } } void doSubmit() { println("DEBUG doSubmit for SetClientMidiChannel"); int ch = -1 ; String chstring = ""; for (int column = 0; column < maxcolumns; column++) { String piece = buttons[activeRowPerColumn[column]][column].getText(); if (! "-".equals(piece)) { chstring += piece ; } } chstring = chstring.trim(); // println("DEBUG SETTING Client MIDI channel: " + chstring); try { ch = Integer.parseInt(chstring); if (ch < 1 || ch >= 16) { // throw new Exception("Invalid MIDI channel: " + ch); errorString = "Invalid MIDI channel: " + ch ; printDEBUG("SetClientMidiChannel error: " + errorString); oscClientMidiChan = -1 ; } else { oscClientMidiChan = ch ; errorString = null ; println("DEBUG, setting MIDI channel to " + oscClientMidiChan); globalMenu = null ; if (! showingKeyboard) { // happens when chan is changed during play showingKeyboard = showedKeyboard ; } } } catch (Exception xxx) { errorString = "ERROR, cannot parse midi channel " + chstring + ", " + xxx.getMessage(); printDEBUG("SetClientMidiChannel error: " + errorString); oscClientMidiChan = -1 ; } } void doCancel() { // no way to cancel this Menu } } class SetClientTonic extends Menu { String errorString = null ; SetClientTonic() { super("Set your Client tonic note", true, false, // Cancel not acceptable here. null, null, noteName, globalFont, round(globalPointSize*1.0), 1.5, 1.5); } void display() { super.display(); if (errorString != null) { printDEBUG("SetClientTonic error: " + errorString); } } void doSubmit() { println("DEBUG doSubmit for SetClientTonic"); String tonicString = ""; for (int column = 0; column < maxcolumns; column++) { String piece = buttons[activeRowPerColumn[column]][column].getText(); if (! "-".equals(piece)) { tonicString += piece ; } } tonicString = tonicString.trim(); // println("DEBUG SETTING Client MIDI channel: " + chstring); try { tonic = -1 ; for (int t = 0 ; t < noteName.length ; t++) { if (noteName[t].trim().equals(tonicString)) { tonic = t ; println("DEBUG set tonic to " + t); showingKeyboard = showedKeyboard = true ; globalMenu = null ; setupNotes(); break ; } } if (tonic < 0) { throw new RuntimeException("Cannot set tonic from '" + tonicString + "'"); } } catch (Exception xxx) { errorString = xxx.getMessage(); printDEBUG("SetClientTonic error: " + errorString); tonic = -1 ; } } void doCancel() { // no way to cancel this Menu } } class SetClientScale extends Menu { String errorString = null ; SetClientScale() { super("Set your Client scale", true, false, // Cancel not acceptable here. null, null, scaleName, globalFont, round(globalPointSize*1.0), 1.5, 1.5); } void display() { super.display(); if (errorString != null) { printDEBUG("SetClientScale error: " + errorString); } } void doSubmit() { println("DEBUG doSubmit for SetClientScale"); String scaleString = ""; for (int column = 0; column < maxcolumns; column++) { String piece = buttons[activeRowPerColumn[column]][column].getText(); if (! "-".equals(piece)) { scaleString += piece ; } } scaleString = scaleString.trim(); println("DEBUG SETTING Client scale: " + scaleString); try { scaleix = -1 ; for (int t = 0 ; t < scaleName.length ; t++) { if (scaleName[t].trim().equals(scaleString)) { scaleix = t ; println("DEBUG doSubmit() set scale to " + scaleix); //<>// showingKeyboard = showedKeyboard = true ; globalMenu = null ; setupNotes(); break ; } } if (scaleix < 0) { throw new RuntimeException("Cannot set scale from '" + scaleString + "'"); } } catch (Exception xxx) { errorString = xxx.getMessage(); printDEBUG("SetClientScale error: " + errorString); scaleix = -1 ; //<>// } } void doCancel() { // no way to cancel this Menu } } final String [] loopStateNames = { "clear", "record", "play", "clearmine", "clearall", "clearexit" }; class SetClientLoop extends Menu { String errorString = null ; SetClientLoop() { super("Set your loop state", true, false, // Cancel not acceptable here. null, null, loopStateNames, globalFont, round(globalPointSize*1.0), 1.5, 1.5); } void display() { super.display(); if (errorString != null) { printDEBUG(errorString); } } /* enum LoopType { CLEAR, RECORD, PLAY, UNKNOWN }; LoopType loopState = LoopType.CLEAR ; long loopNowTime = 0L, loopStartTime = 0L, loopMaxTime = 0L ; class LoopData { final long notetime ; // in msecs final String command ; final int pitch ; final int velocity ; final int channel ; LoopData(long notetime, String command, int pitch, int velocity, int channel) { this.notetime = notetime ; this.command = command ; this.pitch = pitch ; this.velocity = velocity ; this.channel = channel ; } } final List loopSequence = new LinkedList(); int loopSequenceStep = 0 ; */ void doSubmit() { println("DEBUG doSubmit for SetClientLoop"); String loopString = ""; for (int column = 0; column < maxcolumns; column++) { String piece = buttons[activeRowPerColumn[column]][column].getText(); if (! "-".equals(piece)) { loopString += piece ; } } loopString = loopString.trim(); println("DEBUG SETTING Client scale: " + loopString); try { if ("record".equals(loopString)) { loopState = LoopType.RECORD; loopSequence.clear(); loopStartTime = System.currentTimeMillis(); loopSequenceStep = 0 ; } else if ("play".equals(loopString)) { if (loopState != LoopType.PLAY) { // otherwise, just let it keep playing loopState = LoopType.PLAY ; loopStartTime = System.currentTimeMillis(); loopSequenceStep = 0 ; } } else { loopState = LoopType.CLEAR; while (loopSequenceStep < loopSequence.size()) { LoopData ready = loopSequence.get(loopSequenceStep) ; if (ready.command.equals("noteoff")) { sendOSCMessage(ready.command, ready.channel, ready.pitch, ready.velocity); // avoid stuck notes } loopSequenceStep++ ; } loopSequence.clear(); loopStartTime = 0L; loopSequenceStep = 0 ; if ("clearmine".equals(loopString) || "clearall".equals(loopString)) { sendOSCMessage(loopString, oscClientMidiChan, 0, 0); } else if ("clearexit".equals(loopString)) { sendOSCMessage("clearmine", oscClientMidiChan, 0, 0); exit(); } } println("DEBUG doSubmit() set loop state to " + loopString); showingKeyboard = showedKeyboard = true ; globalMenu = null ; } catch (Exception xxx) { errorString = xxx.getMessage(); printDEBUG("SetClientLoop error: " + errorString); loopState = LoopType.CLEAR; loopSequence.clear(); loopStartTime = 0L; loopSequenceStep = 0 ; } finally { enterLoopMenu = false ; } } void doCancel() { // no way to cancel this Menu } } String [] SetServerAddressPortStrings = { // 4 digit number "---.---.---.---:-----", "000.000.000.000:00000", "111.111.111.111:11111", "222.222.222.222:22222", "333.333.333.333:33333", "444.444.444.444:44444", "555.555.555.555:55555", "666.666.666.666:66666", "777.777.777.777:77777", "888.888.888.888:88888", "999.999.999.999:99999" }; class SetServerAddressPortMenu extends Menu { SetServerAddressPortMenu() { super("Set Server IP Address", true, false, // Cancel not acceptable here. null, SetServerAddressPortStrings, null, globalFont, round(globalPointSize*1.0), 1.5, 1.5); } void doSubmit() { println("DEBUG doSubmit for SetServerAddressPortMenu"); String ipstring = "", portstring = "" ; boolean doingPort = false ; for (int column = 0; column < maxcolumns; column++) { String piece = buttons[activeRowPerColumn[column]][column].getText(); if (! "-".equals(piece)) { if (":".equals(piece)) { doingPort = true ; } else if (doingPort) { portstring += piece ; } else { ipstring += piece ; } } } println("DEBUG SETTING SERVER IP ADDRESS: " + ipstring + ":" + portstring); try { SERVERIPADDR = ipstring ; SERVERPORT = Integer.parseInt(portstring); serverRemoteLocation = new NetAddress(SERVERIPADDR, SERVERPORT); sendOSCMessage("client", -1, -1, -1); // ignore trailing args for client // globalMenu = null ; // We hope it worked, this menu no longer needed. // NO! Let the first reply from the server do this step. println("DEBUG CLIENT SENT " + CommandPrefix + "client to server"); } catch (Exception xxx) { println("ERROR, cannot parse server port " + SERVERPORT + ", " + xxx.getMessage()); SERVERIPADDR = null ; SERVERPORT = -1 ; } } void doCancel() { // no way to cancel this Menu } }