/*
Part of the GUI library for Processing
http://www.lagers.org.uk/g4p/index.html
http://sourceforge.net/projects/g4p/files/?source=navbar
Copyright (c) 2012 Peter Lager
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General
Public License along with this library; if not, write to the
Free Software Foundation, Inc., 59 Temple Place, Suite 330,
Boston, MA 02111-1307 USA
*/
package automenta.vivisect.gui;
import automenta.vivisect.gui.HotSpot.HSrect;
import java.awt.Graphics2D;
import java.awt.font.TextLayout;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import processing.core.PApplet;
import processing.event.MouseEvent;
/**
* A drop down list component. <br>
*
* This replaces the GCombo control in pre V3 editions of this library. <br>
*
* The number of items in the list is not restricted but the user can define
* the maximum number of items to be displayed in the drop list. If there are
* too many items to display a vertical scroll bar is provide to scroll through
* all the items.
*
* The vertical size of an individual item is calculated from the overall height
* specified when creating the control. <br>
*
* @author Peter Lager
*
*/
public class GDropList extends GTextBase {
static protected int LIST_SURFACE = 1;
static protected int CLOSED_SURFACE = 2;
protected static final int FORE_COLOR = 2;
protected static final int BACK_COLOR = 5;
protected static final int ITEM_FORE_COLOR = 3;
protected static final int ITEM_BACK_COLOR = 6;
protected static final int OVER_ITEM_FORE_COLOR = 15;
private GScrollbar vsb;
private GButton showList;
protected LinkedList<String> itemlist = new LinkedList<String>();
protected StyledString[] sitems;
protected StyledString selText;
protected int selItem = 0;
protected int startItem = 0;
protected int lastOverItem = -1;
protected int currOverItem = lastOverItem;
protected int dropListMaxSize = 4;
protected int dropListActualSize = 4;
protected float itemHeight, buttonWidth;
protected boolean expanded = false; // make false in release version
/**
* Create a drop down list component with a list size of 4.
*
* After creating the control use setItems to initialise the list. <br>
*
* @param theApplet the applet that will display this component.
* @param p0
* @param p1
* @param p2
* @param p3
*/
public GDropList(PApplet theApplet, float p0, float p1, float p2, float p3) {
this(theApplet, p0, p1, p2, p3, 4);
}
/**
* Create a drop down list component with a specified list size.
*
* After creating the control use setItems to initialise the list. <br>
*
* @param theApplet
* @param p0
* @param p1
* @param p2
* @param p3
* @param dropListMaxSize the maximum number of element to appear in the drop down list
*/
public GDropList(PApplet theApplet, float p0, float p1, float p2, float p3, int dropListMaxSize) {
super(theApplet, p0, p1, p2, p3);
children = new LinkedList<GControl>();
this.dropListMaxSize = Math.max(dropListMaxSize, 3);
itemHeight = height / (dropListMaxSize + 1); // make allowance for selected text at top
GUI.pushStyle();
GUI.showMessages = false;
vsb = new GScrollbar(theApplet, 0, 0, height - itemHeight, 10);
vsb.addEventHandler(this, "vsbEventHandler");
vsb.setAutoHide(true);
vsb.setVisible(false);
buttonWidth = 10;
showList = new GButton(theApplet, 0, 0, buttonWidth, itemHeight, ":");
showList.addEventHandler(this, "buttonShowListHandler");
// Do this before we add the button and scrollbar
z = Z_SLIPPY_EXPANDS;
// Add the button and scrollbar
GUI.control_mode = GControlMode.CORNER;
add(vsb, width, itemHeight + 1, PI/2);
add(showList, width - buttonWidth, 0, 0);
GUI.popStyle();
hotspots = new HotSpot[]{
new HSrect(LIST_SURFACE, 0, itemHeight+1, width - 11, height - itemHeight - 1), // text list area
new HSrect(CLOSED_SURFACE, 0, 0, width - buttonWidth, itemHeight) // selected text display area
};
createEventHandler(GUI.applet, "handleDropListEvents",
new Class<?>[]{ GDropList.class, GEvent.class },
new String[]{ "list", "event" }
);
registeredMethods = DRAW_METHOD | MOUSE_METHOD;
cursorOver = HAND;
GUI.addControl(this);
}
/**
* Use this to set or change the list of items to appear in the list. If
* you enter an invalid selection index then it is forced into
* the valid range. <br>
* Null and empty values in the list will be ignored. <br>
* If the list is null then or empty then then no changes are made. <br>
* @param array
* @param selected
*/
public void setItems(String[] array, int selected){
if(array == null)
return;
// Get rid of null or empty strings
ArrayList<String> strings = new ArrayList<String>();
for(String s : array)
strings.add(s);
if(cleanupList(strings))
setItemsImpl(selected);
}
public void setItems(List<String> list, int selected){
if(list == null)
return;
if(cleanupList(list))
setItemsImpl(selected);
}
private boolean cleanupList(List<String> list){
// Get rid of null or empty strings
Iterator<String> iter = list.iterator();
while(iter.hasNext()){
String s = iter.next();
if(s == null || s.length() == 0)
iter.remove();
}
if(list.size() > 0){
// We have at least one item for the droplist
itemlist.clear();
itemlist.addAll(list);
return true;
}
return false;
}
private void setItemsImpl(int selected){
sitems = new StyledString[itemlist.size()];
// Create styled strings for display
for(int i = 0; i < sitems.length; i++)
sitems[i] = new StyledString(itemlist.get(i));
// Force selected value into valid range
selItem = PApplet.constrain(selected, 0, sitems.length - 1);
startItem = (selItem >= dropListMaxSize) ? selItem - dropListMaxSize + 1 : 0;
// Make selected item bold
sitems[selItem].addAttribute(WEIGHT, WEIGHT_BOLD);
// Create separate styled string for display area
selText = new StyledString(sitems[selItem].getPlainText());
dropListActualSize = Math.min(sitems.length, dropListMaxSize);
if((sitems.length > dropListActualSize)){
float filler = ((float)dropListMaxSize)/sitems.length;
float value = ((float)startItem)/sitems.length;
vsb.setValue(value, filler);
vsb.setVisible(false); // make it false
}
bufferInvalid = true;
}
/**
* Remove an item from the list. <br>
* If idx is not a valid position in the list then the list is unchanged.
* If idx points to the selected item or an item below it then the
* selected index value is reduced by 1 but the selected text remains the
* same. <br>
* If idx points to an item above the selected item then the selected
* item is unchanged. <br>
* No event is fired even if the selected index changes. <br>
*
* @param idx index of the item to remove
* @return true if item is successfully removed else false
*/
public boolean removeItem(int idx){
if(itemlist.size() <= 1 || idx < 0 || idx >= itemlist.size() )
return false;
int sel = (idx > selItem) ? selItem : selItem - 1;
itemlist.remove(idx);
setItemsImpl(sel);
return true;
}
/**
* Insert an item at the specified position in the list. <br>
* If idx is <0 then the list is unchanged. <br>
* If idx is >= the number of items in the list, it is added to the end. <br>
* If idx points to the selected item or an item below it then the
* selected index value is incremented by 1 but the selected text remains
* the same. <br>
* If idx points to an item above the selected item then the selected
* item is unchanged. <br>
* No event is fired even if the selected index changes. <br>
*
* @param idx index of the item to remove
* @param name text of the item to insert (null values ignored)
* @return true if item is successfully inserted else false
*/
public boolean insertItem(int idx, String name){
if(idx < 0 || name == null || name.length() > 0)
return false;
if(idx >= itemlist.size()){
itemlist.addLast(name);
}
else {
itemlist.add(idx, name);
}
int sel = (idx <= selItem) ? selItem + 1: selItem;
setItemsImpl(sel);
return true;
}
/**
* Add an item to the end of the list. <br>
* No event is fired. <br>
*
* @param name text of the item to add (null values ignored)
* @return true if add is successfully added else false
*/
public boolean addItem(String name){
if(name == null)
return false;
itemlist.addLast(name);
return true;
}
/**
* Set the currently selected item from the droplist by index position. <br>
* Invalid values are ignored.
*
* @param selected
*/
public void setSelected(int selected){
if(selected >=0 && selected < sitems.length){
selItem = selected;
startItem = (selItem >= dropListMaxSize) ? selItem - dropListMaxSize + 1 : 0;
for(StyledString s : sitems)
s.clearAttributes();
sitems[selItem].addAttribute(WEIGHT, WEIGHT_BOLD);
selText = new StyledString(sitems[selItem].getPlainText());
bufferInvalid = true;
}
}
/**
* Get the index position of the selected item
*/
public int getSelectedIndex(){
return selItem;
}
/**
* Get the text for the selected item
*/
public String getSelectedText(){
return sitems[selItem].getPlainText();
}
/**
* Sets the local colour scheme for this control
*/
public void setLocalColorScheme(int cs){
super.setLocalColorScheme(cs);
if(showList != null)
showList.setLocalColorScheme(localColorScheme);
if(vsb != null)
vsb.setLocalColorScheme(localColorScheme);
}
/**
* Determines if a particular pixel position is over this control taking
* into account whether it is collapsed or not.
*/
public boolean isOver(float x, float y){
calcTransformedOrigin(winApp.getCursorX(), winApp.getCursorY());
currSpot = whichHotSpot(ox, oy);
return (!expanded)? currSpot == CLOSED_SURFACE : currSpot == CLOSED_SURFACE | currSpot == LIST_SURFACE;
}
public void mouseEvent(MouseEvent event){
if(!visible || !enabled || !available) return;
calcTransformedOrigin(winApp.getCursorX(), winApp.getCursorY());
currSpot = whichHotSpot(ox, oy);
if(currSpot >= 0 || focusIsWith == this)
cursorIsOver = this;
else if(cursorIsOver == this)
cursorIsOver = null;
switch(event.getAction()){
case MouseEvent.CLICK:
// No need to test for isOver() since if the component has focus
// and the mouse has not moved since MOUSE_PRESSED otherwise we
// would not get the Java MouseEvent.MOUSE_CLICKED event
if(focusIsWith == this ){
loseFocus(null);
vsb.setVisible(false);
expanded = false;
bufferInvalid = true;
// Make sure that we have selected a valid item and that
// it is not the same as before;
if(currOverItem >= 0 && currOverItem != selItem){
setSelected(currOverItem);
fireEvent(this, GEvent.SELECTED);
}
currOverItem = lastOverItem = -1;
}
break;
case MouseEvent.MOVE:
if(focusIsWith == this){
if(currSpot == LIST_SURFACE)
currOverItem = startItem + (int)(oy / itemHeight)-1;
//currOverItem = startItem + Math.round(oy / itemHeight) - 1;
else
currOverItem = -1;
// Only invalidate the buffer if the over item has changed
if(currOverItem != lastOverItem){
lastOverItem = currOverItem;
bufferInvalid = true;
}
}
break;
}
}
public void draw(){
if(!visible) return;
updateBuffer();
winApp.pushStyle();
winApp.pushMatrix();
applyTransform();
winApp.pushMatrix();
// Move matrix to line up with top-left corner
winApp.translate(-halfWidth, -halfHeight);
// Draw buffer
winApp.imageMode(PApplet.CORNER);
if(alphaLevel < 255)
winApp.tint(TINT_FOR_ALPHA, alphaLevel);
winApp.image(buffer, 0, 0);
winApp.popMatrix();
if(children != null){
for(GControl c : children)
c.draw();
}
winApp.popMatrix();
winApp.popStyle();
}
protected void updateBuffer(){
if(bufferInvalid) {
Graphics2D g2d = buffer.g2;
bufferInvalid = false;
buffer.beginDraw();
buffer.background(buffer.color(255,0));
buffer.noStroke();
buffer.fill(palette[BACK_COLOR]);
buffer.rect(0, 0, width, itemHeight);
if(expanded){
buffer.fill(palette[ITEM_BACK_COLOR]);
buffer.rect(0,itemHeight, width, itemHeight * dropListActualSize);
}
float px = TPAD2, py;
TextLayout line;
// Get selected text for display
line = selText.getLines(g2d).getFirst().layout;
py = (itemHeight + line.getAscent() - line.getDescent())/2;
g2d.setColor(jpalette[FORE_COLOR]);
line.draw(g2d, px, py);
if(expanded){
g2d.setColor(jpalette[ITEM_FORE_COLOR]);
for(int i = 0; i < dropListActualSize; i++){
py += itemHeight;
if(currOverItem == startItem + i)
g2d.setColor(jpalette[OVER_ITEM_FORE_COLOR]);
else
g2d.setColor(jpalette[ITEM_FORE_COLOR]);
line = sitems[startItem + i].getLines(g2d).getFirst().layout;
line.draw(g2d, px, py);
}
}
buffer.endDraw();
}
}
/**
* For most components there is nothing to do when they loose focus.
* Override this method in classes that need to do something when
* they loose focus eg TextField
*/
protected void loseFocus(GControl grabber){
if(grabber != vsb){
expanded = false;
vsb.setVisible(false);
bufferInvalid = true;
}
if(cursorIsOver == this)
cursorIsOver = null;
focusIsWith = grabber;
}
/**
* This method should <b>not</b> be called by the user. It
* is for internal library use only.
*/
public void vsbEventHandler(GScrollbar scrollbar, GEvent event){
int newStartItem = Math.round(vsb.getValue() * sitems.length);
startItem = newStartItem;
bufferInvalid = true;
}
/**
* This method should <b>not</b> be called by the user. It
* is for internal library use only.
*/
public void buttonShowListHandler(GButton button, GEvent event){
if(expanded){
loseFocus(null);
vsb.setVisible(false);
expanded = false;
}
else {
takeFocus();
vsb.setVisible(sitems.length > dropListActualSize);
expanded = true;
}
bufferInvalid = true;
}
}