package org.mt4j.components.visibleComponents.widgets.menus; import org.mt4j.components.MTComponent; import org.mt4j.components.bounds.BoundsArbitraryPlanarPolygon; import org.mt4j.components.interfaces.IclickableButton; import org.mt4j.components.visibleComponents.shapes.AbstractShape; import org.mt4j.components.visibleComponents.widgets.buttons.*; import org.mt4j.input.gestureAction.DefaultButtonClickAction; import org.mt4j.input.inputProcessors.IGestureEventListener; import org.mt4j.input.inputProcessors.MTGestureEvent; import org.mt4j.input.inputProcessors.componentProcessors.dragProcessor.DragProcessor; import org.mt4j.input.inputProcessors.componentProcessors.rotateProcessor.RotateProcessor; import org.mt4j.input.inputProcessors.componentProcessors.scaleProcessor.ScaleProcessor; import org.mt4j.input.inputProcessors.componentProcessors.tapProcessor.TapEvent; import org.mt4j.input.inputProcessors.componentProcessors.tapProcessor.TapProcessor; import org.mt4j.util.MTColor; import org.mt4j.util.math.FastMath; import org.mt4j.util.math.Vector3D; import org.mt4j.util.math.Vertex; import org.mt4j.util.xml.svg.SVGLoader; import processing.core.PApplet; public class MTCircularItem extends MTGLButton { protected static float DEFAULT_WIDTH = 70; protected static float DEFAULT_MAX_WIDTH = 100f; protected static float DEFAULT_PADDING = 20f; private MTCircularItem parent=null; public enum States { CLOSED, FULL, PARTIAL }; private States state; private boolean selected = false; public float innerWidth = 30f; public float outerWidth = 120f; public float childrensOuterWidth = outerWidth; public float levelBuffer = 20f; public float arcWidth = 50f; public float fullArc = 360f; public float currentArc = 0f; private MTColor selectedColor = new MTColor(40f, 120f, 130f); private MTColor normalColor = new MTColor(30f, 80f, 90f); private float lastRotation = 0f; MTCircularItem children[]; /** * The constructor for MTCircularMenuItem. All that's really necessary is the Papplet * @param pa * @param children */ public MTCircularItem(PApplet pa) { this(pa, null); } /** * The constructor for MTCircularMenuItem can be passed an array of children, * in the order you wish them to be presented. They will be added as children * of this object, and adjusted accordingly. * @param pa * @param children */ public MTCircularItem(PApplet pa, MTCircularItem children[]) { this(pa,null,children); } /** * The constructor for MTCircularMenuItem can be passed an array of children, * in the order you wish them to be presented. They will be added as children * of this object, and adjusted accordingly. * @param pa * @param parent menu * @param children */ public MTCircularItem(PApplet pa, MTCircularItem parent, MTCircularItem children[]) { this(pa, parent, children, DEFAULT_MAX_WIDTH-DEFAULT_WIDTH, DEFAULT_MAX_WIDTH, DEFAULT_PADDING); } /** * This is the full constructor. It controls the initial size, and size of children, along with the parameters mentioned above * @param pa * @param parent * @param children * @param innerRadius * @param outerRadius * @param padding */ public MTCircularItem(PApplet pa, MTCircularItem parent, MTCircularItem children[], float innerRadius, float outerRadius, float padding) { super( new Vertex[]{ new Vertex(0.2f,0f), new Vertex(1f, 0f), new Vertex(0.8f, 0.7f), new Vertex(0.25f,0.3f), new Vertex(0.22f, 0.3f), new Vertex(0.2f,0f), }, pa); this.setVertices(getVerticies()); this.parent=parent; if (parent == null) { this.state=States.FULL; this.childrensOuterWidth = outerRadius; outerRadius = innerRadius; innerRadius = 1f; } else { this.state=States.PARTIAL; } this.setShape(innerRadius, outerRadius, arcWidth, 0f); setupStuff(pa); setupVisibleSelf(pa); this.children = children; addAllChildren(); //positionChildren(80f, 70f); addSelfAsListener(pa); this.position(); } /** * Iterates through all assigned children menu items, and adds them as MTchildren of * this */ public void addAllChildren() { if (children != null) { for (int i = 0; i < children.length; i++) { MTCircularItem item = children[i]; this.addChild(item); item.setState(States.PARTIAL); } this.setVisibilityOfChildren(isSelected()); } } private void setupVisibleSelf(PApplet pa) { //this.setSizeXYRelativeToParent(200,200); //this.setPositionRelativeToParent(new Vector3D(0,0)); this.setFillColor(normalColor); this.setStrokeColor(new MTColor(0,0,0)); this.setStrokeWeight(3); this.setVisible(true); } //several methods copied over from MTImageButton about setup and config... dunno yet private void setupStuff(PApplet pa) { /** This was copied over from the MTImageButton class **/ this.setGestureAllowance(DragProcessor.class, false); this.setGestureAllowance(RotateProcessor.class, false); this.setGestureAllowance(ScaleProcessor.class, false); this.setEnabled(true); this.setBoundsBehaviour(AbstractShape.BOUNDS_ONLY_CHECK); //Make clickable this.setGestureAllowance(TapProcessor.class, true); this.registerInputProcessor(new TapProcessor(pa)); this.addGestureListener(TapProcessor.class, new DefaultButtonClickAction(this)); //Draw this component and its children above //everything previously drawn and avoid z-fighting this.setDepthBufferDisabled(true); /** Done copying **/ } /** * * * * * * begin Getters and setters * * * * * * **/ public void setParent(MTCircularItem parent) { this.parent = parent; } public void setChildren(MTCircularItem[] children) { this.children = children; addAllChildren(); } public void setState(States state) { this.state = state; } public States getState() { return this.state; } /** * Generates a new set of verticies for MTPolygon based off of the extisting object parameters * requires a granularity level * @return */ public Vertex[] getVerticies(int granularity) { Vertex[] verts = new Vertex [granularity * 2 + 1]; for (int i=0; i<verts.length/2;i++) { verts[i] = rotate(outerWidth, arcWidth/(verts.length/2-1) * i); } for (int i=1; i<=verts.length/2;i++) { verts[verts.length-i-1] = rotate(innerWidth, arcWidth/(verts.length/2-1) * i); } verts[verts.length-1] = rotate(outerWidth, 0); return verts; } public Vertex[] getVerticies() { return getVerticies(30); } /** * This function changes the shape of the menu item, useful for recursing. * @param innerWidth * @param outerWidth * @param arc */ public void setArc(float arc) { arcWidth = arc; regenerateVerticies(); } /** * This function changes the shape of the menu item, useful for recursing. * @param innerWidth * @param outerWidth * @param arc */ public void setShape(float innerWidth, float outerWidth, float arc, float currentArc) { this.arcWidth = arc; this.innerWidth = innerWidth; this.outerWidth = outerWidth; this.currentArc = currentArc; regenerateVerticies(); swapHoverover(isSelected()); } /** * sets all of the children to the given visibility, called when we're clicked * @param visible */ public void setVisibilityOfChildren(boolean visible) { if (children != null) { for (MTCircularItem m : children) { m.setVisible(visible); } } } public int getNumberOfChildren() { if (children != null) return children.length; else return 0; } /** * This function is for placement of items. Every time we want to position something, * we need to figure out how many circular menu items are visible on the same level. * this function allows us to count recursively to get an accurate number. * @return */ public int countVisibleItems() { if (this.isVisible()) return 1 + countVisibleChildren(); else { return 0; } } /** * This helper function for countVisibleItems iterates through children, * summing together the visible children * @return */ public int countVisibleChildren() { if (children == null) return 0; int visibleItemCount = 0; for (MTCircularItem childitem : children) { visibleItemCount += childitem.countVisibleItems(); } return visibleItemCount; } @Override public boolean isSelected() { // TODO Auto-generated method stub return selected; } public void setSelected(boolean selected, TapEvent tapevent) { setSelected(selected); this.fireActionPerformed(tapevent); } @Override public void setSelected(boolean selected) { System.out.println(this+"We're "+selected+" selected!"); swapHoverover(selected); setVisibilityOfChildren(selected); this.handleSelectEvent(this, selected); } public void setSelectedColor(MTColor selectedColor) { this.selectedColor = selectedColor; } public void setNormalColor(MTColor normalColor) { this.normalColor = normalColor; } /** * * * * * * end Getters and setters * * * * * * **/ //internal method private Vertex rotate(float radius, float arc) { return new Vertex(radius * FastMath.cos(arc*FastMath.DEG_TO_RAD), radius * FastMath.sin(arc*FastMath.DEG_TO_RAD)); } /** * reset our current shape */ public void regenerateVerticies() { this.setVertices(getVerticies()); this.setBoundingShape(new BoundsArbitraryPlanarPolygon(this,getVerticies())); } /** * Adds this menu item as a gesture listener for tap clicks of itself * @param app */ private void addSelfAsListener(PApplet app) { this.addGestureListener(TapProcessor.class, this); } /** * This stuff all deals with the placement of the buttons about its parent. very complicated. */ /** * if passed with no parameters, we assume we just use the current values of the object * @return */ public float position() { return position(innerWidth, outerWidth, 0.0f, arcWidth); } /** * Based on the state, this positions the menu item, and children items, recursively. * Nothing will be drawn closer than minDistance, nor farther than maxDistance * The startingArc parameter is where the menu will start to draw things counterclockwise * the defaultArc is the size of the menu if its children aren't visible * @param distanceFromCenter * @param maxDistance * @param startingArc * @param totalArc * @return The ending arc position of the last subelement of it */ public float position(float minDistance, float maxDistance, float startingArc, float defaultArc) { switch (state) { case FULL: return positionFull(minDistance, maxDistance); case PARTIAL: return positionPartial(minDistance, maxDistance, startingArc, defaultArc, levelBuffer); case CLOSED: return positionClosed(minDistance, maxDistance); } return 0.0f; } /** * Currently the closed state is not used, however in the future, this would be called to position it * @param minDistance * @param maxDistance * @return */ private float positionClosed(float minDistance, float maxDistance) { return 0.0f; } /** * This method will position the object so all of its children fan around it * @param minDistance * @param maxDistance * @return */ private float positionFull(float minDistance, float maxDistance) { //System.out.println("minDistance:" + minDistance); this.setShape(minDistance, maxDistance, fullArc, 0f); if (children != null && this.isSelected() && this.countVisibleChildren() != 0) { float defaultArc = fullArc/this.countVisibleChildren(); positionChildren(maxDistance, childrensOuterWidth, 0.0f, defaultArc, 0.0f); } return fullArc; } /** * Like positionFull, but instead, it positions its children based off of the boundaries given to it by its parent * @param minDistance * @param maxDistance * @param startingArc * @param defaultArc * @param levelBuffer * @return */ private float positionPartial(float minDistance, float maxDistance, float startingArc, float defaultArc, float levelBuffer) { this.setShape(minDistance, maxDistance, defaultArc, startingArc); smartRotateZ(startingArc); //System.out.println("Resizing this slice to min:"+minDistance+", max:"+maxDistance+", arc:"+defaultArc); //System.out.println("Positioning this slice to "+startingArc); return positionChildren(minDistance,maxDistance,startingArc+defaultArc,defaultArc,levelBuffer); } /** * In both of the above cases, the children are positioned around the object by this method * @param minDistance * @param maxDistance * @param startingArc * @param defaultArc * @param levelBuffer * @return */ public float positionChildren(float minDistance, float maxDistance, float startingArc, float defaultArc, float levelBuffer) { float currentArc = startingArc; if (children != null && this.isSelected()) { //System.out.println("PRinting children, all "+this.countVisibleChildren()+" of them"); this.setDefaultOrientation(startingArc); currentArc = defaultArc; //we reset the rotation because the children are relative to us for (MTCircularItem item : children) { item.setParent(this); currentArc = item.position(minDistance+levelBuffer, maxDistance+levelBuffer, currentArc, defaultArc); } currentArc += startingArc - defaultArc; //and we restore the rotation for other siblings } return currentArc; } /** * I had many issues with MT4J's rotation scheme; this fixed those. * @param arc */ private void smartRotateZ(float arc) { this.rotateZ(Vector3D.ZERO_VECTOR, -1*lastRotation); this.rotateZ(Vector3D.ZERO_VECTOR, arc); lastRotation=arc; } /** * A method I wanted to do so the current selection stays in the same place * @param degrees */ private void setDefaultOrientation(float degrees) { float rotationFix = 1f; if (this.children != null) rotationFix = ((float)this.children.length) / this.countVisibleItems(); if (parent != null) { parent.setDefaultOrientation(degrees*rotationFix + currentArc); return; } else { this.smartRotateZ(degrees*rotationFix); } } /** * An odd class to override... we know the center of the button is... not techincally. */ @Override public Vector3D getCenterPointLocal(){ return new Vector3D(0,0,0); } /** * The handler of gesture events. Its quite a nice handler. */ public boolean processGestureEvent(MTGestureEvent ge) { TapEvent te = (TapEvent)ge; switch (te.getId()) { case MTGestureEvent.GESTURE_DETECTED: if (this.children == null) { this.selected=true; this.setSelected(true, te); } swapHoverover(true); break; case MTGestureEvent.GESTURE_UPDATED: break; case MTGestureEvent.GESTURE_ENDED: if (this.children == null) { this.selected=false; this.setSelected(false, te); } else { this.selected = !selected; this.setSelected(selected, te); } break; } return false; } /** * Called when selected to swap the color of the background * @param selected */ public void swapHoverover(boolean selected) { if (selected) this.setFillColor(selectedColor); else this.setFillColor(normalColor); } /** * This is called when one of our children is clicked * The idea is once we reach the top, reposition everything. * @param child */ public void handleSelectEvent(MTCircularItem child, boolean selected) { if (parent != null) parent.handleSelectEvent(child , selected); else //if (selected) this.position(); } }