package org.mt4jx.components.visibleComponents.widgets.menus; import java.util.ArrayList; import java.util.List; import org.mt4j.AbstractMTApplication; import org.mt4j.components.MTComponent; import org.mt4j.components.TransformSpace; import org.mt4j.components.clipping.Clip; import org.mt4j.components.css.style.CSSFont; import org.mt4j.components.css.style.CSSStyle; import org.mt4j.components.css.util.CSSFontManager; import org.mt4j.components.css.util.CSSKeywords.CSSFontWeight; import org.mt4j.components.visibleComponents.shapes.MTPolygon; import org.mt4j.components.visibleComponents.shapes.MTRectangle; import org.mt4j.components.visibleComponents.widgets.MTTextArea; import org.mt4j.input.inputProcessors.IGestureEventListener; import org.mt4j.input.inputProcessors.MTGestureEvent; 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.font.IFont; import org.mt4j.util.math.Tools3D; import org.mt4j.util.math.Vector3D; import org.mt4j.util.math.Vertex; import processing.core.PConstants; import processing.core.PImage; /** * The Class MTHexagonMenu. Menu that contains Hexagons as buttons containing * text or images */ public class MTHexagonMenu extends MTRectangle{ /** The app. */ private AbstractMTApplication app; /** The menu contents. */ private List<MTPolygon> menuContents = new ArrayList<MTPolygon>(); /** The layout. */ private List<ArrayList<MTPolygon>> layout = new ArrayList<ArrayList<MTPolygon>>(); /** The size. */ private float size; /** The current. */ private int current = 0; /** The max per line. */ private int maxPerLine = 0; /** The bezel. */ private float bezel = 4f; // List of the Child Polygons and their IGestureEventListeners /** The polygon listeners. */ private List<PolygonListeners> polygonListeners = new ArrayList<PolygonListeners>(); /** The menu items. */ private List<MenuItem> menuItems = new ArrayList<MenuItem>(); /** * Instantiates a new MTHexagonMenu. * * @param app * the app * @param position * the position of the Menu * @param menuItems * the menu items * @param size * the size (width of the Hexagon, the height is bigger) */ public MTHexagonMenu(AbstractMTApplication app, Vector3D position, List<MenuItem> menuItems, float size) { super(app, position.x, position.y, (float) (int) Math .sqrt(menuItems.size() + 1) * size, (float) (int) Math .sqrt(menuItems.size() + 1) * size); this.app = app; this.size = size; // Set the Rectangle to be invisible this.setCssForceDisable(true); this.setNoFill(true); this.setNoStroke(true); this.menuItems = menuItems; this.createMenuItems(); // Register the TapProcessor this.setGestureAllowance(TapProcessor.class, true); this.registerInputProcessor(new TapProcessor(app)); this.addGestureListener(TapProcessor.class, new TapListener( polygonListeners)); this.setCssForceDisable(true); } /** * Re-Creates the menu items. */ public void createMenuItems() { for (MTComponent c : this.getChildren()) { c.destroy(); } this.removeAllChildren(); menuContents.clear(); polygonListeners.clear(); for (MenuItem s : menuItems) { if (s != null && s.getType() == MenuItem.TEXT) { // Create Hexagon with Text Included MTPolygon container = getHexagon(size); this.addChild(container); // Add MTTextArea Children to take single lines of the Menu Text for (String t : s.getMenuText().split("\n")) { MTTextArea menuItem = new MTTextArea(app); menuItem.setText(t); menuItem.setCssForceDisable(true); menuItem.setFillColor(new MTColor(0, 0, 0, 0)); menuItem.setStrokeColor(new MTColor(0, 0, 0, 0)); menuItem.setPickable(false); container.addChild(menuItem); } container.setChildClip(new Clip(container)); container.setPickable(false); polygonListeners.add(new PolygonListeners(container, s .getGestureListener())); menuContents.add(container); } else if (s != null && s.getType() == MenuItem.PICTURE) { if (s.getMenuImage() != null) { // Create Polygon for holding an Image PImage texture = null; MTPolygon container = getHexagon(size); container.setCssForceDisable(true); int height = (int) container .getHeightXY(TransformSpace.LOCAL); // If Image doesn't fit, make it fit! if (s.getMenuImage().width != size || s.getMenuImage().height != height) { texture = cropImage(s.getMenuImage(), (int) size, height, true); } else { texture = s.getMenuImage(); } this.addChild(container); // Normalize Texture Coordinates for (Vertex v : container.getVerticesLocal()) { v.setTexCoordU(v.getX() / (float) texture.width); if (v.getTexCoordU() > 1) v.setTexCoordU(1); v.setTexCoordV(v.getY() / (float) texture.height); if (v.getTexCoordV() > 1) v.setTexCoordV(1); } // Set the Texture container.getGeometryInfo() .setTextureCoordsNormalized(true); container.setTexture(texture); menuContents.add(container); container.setPickable(false); polygonListeners.add(new PolygonListeners(container, s .getGestureListener())); } } } // Apply Style to Children this.styleChildren(getNecessaryFontSize(menuItems, size)); } /** * Gets the size. * * @return the size */ public float getSize() { return size; } /** * Sets the size. * * @param size the new size */ public void setSize(float size) { this.size = size; this.createMenuItems(); } /** * Calculates the total height of a number of MTTextAreas. * * @param components the components * @return the height */ private float calcTotalHeight(MTComponent[] components) { // Calculate the total height of several MTTextAreas float height = 0; for (MTComponent c : components) { if (c instanceof MTTextArea) height += ((MTTextArea) c).getHeightXY(TransformSpace.LOCAL); } return height; } /** * Crop image. * * @param image * the image * @param width * the width * @param height * the height * @param resize * resize the image? * @return the cropped image */ private PImage cropImage(PImage image, int width, int height, boolean resize) { PImage workingCopy; try { workingCopy= (PImage) image.clone(); } catch (CloneNotSupportedException e) { System.out.println("Cloning not supported!"); workingCopy = image; } // Crops an Image to fit to the Hexagon Size PImage returnImage = app.createImage(width, height, PConstants.RGB); // Resize Image to match size, but retain aspect ration if (resize || workingCopy.width < size || workingCopy.height < size) { if (((float) workingCopy.width / (float) width) < ((float) workingCopy.height / (float) height)) { workingCopy.resize( width, (int) ((float) workingCopy.height / ((float) workingCopy.width / (float) width))); } else { workingCopy.resize( (int) ((float) workingCopy.width / ((float) workingCopy.height / (float) height)), height); } } // Crop Starting Points int x = (workingCopy.width / 2) - (width / 2); int y = (workingCopy.height / 2) - (height / 2); // Bugfixing: Don't Allow Out-of-Bounds coordinates if (x + width > workingCopy.width) x = workingCopy.width - width; if (x < 0) x = 0; if (x + width > workingCopy.width) width = workingCopy.width - x; if (y + height > workingCopy.height) x = workingCopy.height - height; if (y < 0) y = 0; if (y + height > workingCopy.height) height = workingCopy.height - y; // Crop Image returnImage.copy(workingCopy, x, y, width, height, 0, 0, width, height); return returnImage; } /** * Creates a new Hexagon. * * @param size the width of the Hexagon * @return the hexagon */ private MTPolygon getHexagon(float size) { // Create a new Polygon float hypotenuse = (float) ((size / 2f) / Math.cos(Math.toRadians(30))); //float ankathete = (float) (Math.cos(Math.toRadians(30)) * hypotenuse); float gegenkathete = (float) (Math.sin(Math.toRadians(30)) * hypotenuse); Vertex v1 = new Vertex(0, gegenkathete); Vertex v2 = new Vertex(size / 2f, 0); Vertex v3 = new Vertex(size, gegenkathete); Vertex v4 = new Vertex(size, gegenkathete + hypotenuse); Vertex v5 = new Vertex(size / 2f, 2 * gegenkathete + hypotenuse); Vertex v6 = new Vertex(0, gegenkathete + hypotenuse); Vertex v7 = new Vertex(0, gegenkathete); MTPolygon hexagon = new MTPolygon(app, new Vertex[] { v1, v2, v3, v4, v5, v6, v7 }); return hexagon; } /** * Gets the maximum font size for a certain width. * * @param strings the strings * @param size the width * @return the maximum font size */ private int getNecessaryFontSize(List<MenuItem> strings, float size) { // Calculate the Necessary font size for a SansSerif Bold font int maxNumberCharacters = 0; for (MenuItem s : strings) { if (s.getType() == MenuItem.TEXT) { if (s.getMenuText().contains("\n")) { for (String t : s.getMenuText().split("\n")) { if (t.length() > maxNumberCharacters) maxNumberCharacters = t.length(); } } else { if (s.getMenuText().length() > maxNumberCharacters) maxNumberCharacters = s.getMenuText().length(); } } } float spc = size / (float) maxNumberCharacters; // Space Per Character int returnValue = (int) (-0.5 + 1.725 * spc); // Determined using Linear // Regression return returnValue; } /** * Returns the next n items. * * @param next the number of items to return * @return the list of n next items */ private List<MTPolygon> next(int next) { // Return the next n MTPolygons from the list of children List<MTPolygon> returnValues = new ArrayList<MTPolygon>(); for (int i = 0; i < next; i++) { returnValues.add(menuContents.get(current++)); } return returnValues; } /** * Distribute the contents of the menu to the rows. */ private void organizeHexagons() { // Distribute the items on the cell rows layout.clear(); layout.add(new ArrayList<MTPolygon>()); layout.add(new ArrayList<MTPolygon>()); layout.add(new ArrayList<MTPolygon>()); layout.add(new ArrayList<MTPolygon>()); current = 0; switch (menuContents.size()) { case 0: case -1: break; case 1: layout.get(0).addAll(next(1)); maxPerLine = 1; break; case 2: layout.get(0).addAll(next(2)); maxPerLine = 2; break; case 3: layout.get(0).addAll(next(1)); layout.get(1).addAll(next(2)); maxPerLine = 2; break; case 4: layout.get(0).addAll(next(1)); layout.get(1).addAll(next(2)); layout.get(2).addAll(next(1)); maxPerLine = 2; break; case 5: layout.get(0).addAll(next(2)); layout.get(1).addAll(next(3)); maxPerLine = 3; break; case 6: layout.get(0).addAll(next(1)); layout.get(1).addAll(next(2)); layout.get(1).addAll(next(3)); maxPerLine = 3; break; case 7: layout.get(0).addAll(next(2)); layout.get(1).addAll(next(3)); layout.get(2).addAll(next(2)); maxPerLine = 3; break; case 8: layout.get(0).addAll(next(3)); layout.get(1).addAll(next(2)); layout.get(2).addAll(next(3)); maxPerLine = 3; break; case 9: layout.get(0).addAll(next(2)); layout.get(1).addAll(next(3)); layout.get(2).addAll(next(4)); maxPerLine = 4; break; case 10: layout.get(0).addAll(next(3)); layout.get(1).addAll(next(4)); layout.get(2).addAll(next(3)); maxPerLine = 4; break; case 11: layout.get(0).addAll(next(4)); layout.get(1).addAll(next(3)); layout.get(2).addAll(next(4)); maxPerLine = 4; break; case 12: layout.get(0).addAll(next(3)); layout.get(1).addAll(next(4)); layout.get(2).addAll(next(5)); maxPerLine = 5; break; case 13: layout.get(0).addAll(next(4)); layout.get(1).addAll(next(5)); layout.get(2).addAll(next(4)); maxPerLine = 5; break; case 14: layout.get(0).addAll(next(3)); layout.get(1).addAll(next(4)); layout.get(2).addAll(next(3)); layout.get(3).addAll(next(4)); maxPerLine = 4; break; case 15: layout.get(0).addAll(next(4)); layout.get(1).addAll(next(5)); layout.get(2).addAll(next(6)); maxPerLine = 6; break; case 16: layout.get(1).addAll(next(5)); layout.get(2).addAll(next(6)); layout.get(3).addAll(next(5)); maxPerLine = 6; break; default:{ System.err.println("Unsupported number of menu items in: " + this); } } } /** * Style the cells of the menu. * * @param fontsize the font-size */ private void styleChildren(int fontsize) { organizeHexagons(); CSSStyle vss = this.getCssHelper().getVirtualStyleSheet(); CSSFont cf = this.getCssHelper().getVirtualStyleSheet().getCssfont().clone(); // Style Font: Bold + fitting fontsize cf.setFontsize(fontsize); cf.setWeight(CSSFontWeight.BOLD); // Load Font CSSFontManager cfm = new CSSFontManager(app); IFont font = cfm.selectFont(cf); for (MTPolygon c : menuContents) { MTPolygon rect = c; // Set Stroke/Border rect.setStrokeColor(vss.getBorderColor()); rect.setStrokeWeight(vss.getBorderWidth()); // Set Font and Position for the child MTTextAreas if (((MTPolygon) c).getTexture() == null) { rect.setFillColor(vss.getBackgroundColor()); for (MTComponent d : c.getChildren()) { if (d instanceof MTTextArea) { MTTextArea ta = (MTTextArea) d; ta.setFont(font); } } float height = calcTotalHeight(c.getChildren()); float ypos = rect.getHeightXY(TransformSpace.LOCAL) / 2f - height / 2f; for (MTComponent d : c.getChildren()) { if (d instanceof MTTextArea) { MTTextArea ta = (MTTextArea) d; ta.setPositionRelativeToParent(new Vector3D(size / 2f, ypos + ta.getHeightXY(TransformSpace.LOCAL) / 2f)); ypos += ta.getHeightXY(TransformSpace.LOCAL); } } } else { // Set Fill Color for Pictures MTColor fillColor = MTColor.WHITE; // fillColor.setAlpha(vss.getOpacity()); rect.setFillColor(fillColor); } } // Min/Max Values of the Children float minx = 16000, maxx = -16000, miny = 16000, maxy = -16000; float hypotenuse = (float) ((size / 2f) / Math.cos(Math.toRadians(30))); float gegenkathete = (float) (Math.sin(Math.toRadians(30)) * hypotenuse); int currentRow = 0; // Position the Polygons in the grid for (List<MTPolygon> lr : layout) { int currentColumn = 0; for (MTPolygon r : lr) { r.setPositionRelativeToParent((new Vector3D(this .getVerticesLocal()[0].x + (size / 2f) + (bezel / 2f) + currentColumn++ * (size + bezel) + (maxPerLine - lr.size()) * (size / 2f + bezel / 2f), this.getVerticesLocal()[0].y + (hypotenuse / 2 + bezel / 2f) + currentRow * (hypotenuse + gegenkathete + bezel)))); // Determine Min/Max-Positions // for (Vertex v : r.getVerticesGlobal()) { // if (v.x < minx) // minx = v.x; // if (v.x > maxx) // maxx = v.x; // if (v.y < miny) // miny = v.y; // if (v.y > maxy) // maxy = v.y; // } //Determine Min/Max-Positions //We have to use the childrens relative-to parent vertices for the calculation of this component's local vertices Vertex[] unTransformedCopy = Vertex.getDeepVertexArrayCopy(r.getGeometryInfo().getVertices()); //transform the copied vertices and save them in the vertices array Vertex[] verticesRelParent = Vertex.transFormArray(r.getLocalMatrix(), unTransformedCopy); for (Vertex v: verticesRelParent) { if (v.x < minx) minx = v.x; if (v.x > maxx) maxx = v.x; if (v.y < miny) miny = v.y; if (v.y > maxy) maxy = v.y; } } currentRow++; } // Set Vertices to include all children // this.setVertices(new Vertex[] { new Vertex(minx, miny), // new Vertex(maxx, miny), new Vertex(maxx, maxy), // new Vertex(minx, maxy), new Vertex(minx, miny) }); MTColor fill = this.getFillColor(); //Set Vertices to include all children this.setVertices(new Vertex[] {new Vertex(minx,miny, 0, fill.getR(), fill.getG(), fill.getB(), fill.getAlpha()), new Vertex(maxx,miny, 0, fill.getR(), fill.getG(), fill.getB(), fill.getAlpha()), new Vertex(maxx,maxy, 0, fill.getR(), fill.getG(), fill.getB(), fill.getAlpha()), new Vertex(minx,maxy, 0, fill.getR(), fill.getG(), fill.getB(), fill.getAlpha()),new Vertex(minx,miny, 0, fill.getR(), fill.getG(), fill.getB(), fill.getAlpha())}); } /** * The listener interface for receiving tap events. The class that is * interested in processing a tap event implements this interface, and the * object created with that class is registered with a component using the * component's <code>addTapListener<code> method. When * the tap event occurs, that object's appropriate * method is invoked. * * @see TapEvent */ public class TapListener implements IGestureEventListener { // Tap Listener to reach through TapListeners to children /** The children. */ List<PolygonListeners> children; /** * Instantiates a new tap listener. * * @param children * the children */ public TapListener(List<PolygonListeners> children) { this.children = children; } /* * (non-Javadoc) * * @see * org.mt4j.input.inputProcessors.IGestureEventListener#processGestureEvent * (org.mt4j.input.inputProcessors.MTGestureEvent) */ public boolean processGestureEvent(MTGestureEvent ge) { if (ge instanceof TapEvent) { TapEvent te = (TapEvent) ge; if (te.getTapID() == TapEvent.TAPPED) { for (PolygonListeners pl : children) { pl.component.setPickable(true); if (pl.component.getIntersectionGlobal(Tools3D .getCameraPickRay(app, pl.component, te .getCursor().getPosition().x, te .getCursor().getPosition().y)) != null) { pl.listener.processGestureEvent(ge); } else { } pl.component.setPickable(false); } } } return false; } } /** * The Class PolygonListeners. */ public class PolygonListeners { /** The component. */ public MTPolygon component; /** The listener. */ public IGestureEventListener listener; /** * Instantiates a new polygon listeners mapping. * * @param component the component * @param listener the listener */ public PolygonListeners(MTPolygon component, IGestureEventListener listener) { this.component = component; this.listener = listener; } } }