/* * Copyright (c) 2008, 2010, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code 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 General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores * CA 94065 USA or visit www.oracle.com if you need additional information or * have any questions. */ package com.sun.lwuit.list; import com.sun.lwuit.CheckBox; import com.sun.lwuit.Button; import com.sun.lwuit.Command; import com.sun.lwuit.Component; import com.sun.lwuit.Container; import com.sun.lwuit.Display; import com.sun.lwuit.Form; import com.sun.lwuit.Graphics; import com.sun.lwuit.Image; import com.sun.lwuit.Label; import com.sun.lwuit.List; import com.sun.lwuit.RadioButton; import com.sun.lwuit.TextArea; import com.sun.lwuit.animations.Animation; import com.sun.lwuit.events.ActionEvent; import com.sun.lwuit.events.ActionListener; import com.sun.lwuit.plaf.UIManager; import java.util.Hashtable; import java.util.Vector; /** * The generic list cell renderer can display containers or arbitrary LWUIT components * as items in a list. It generally relies on the source data being either a hashtable or * a list of Strings. It extracts values from the hashtable using the component name as * an indication to the hashtable key lookup. * This renderer supports label tickering, check boxes/radio buttons etc. seamlessly. * Please notice that you must use at least two distinguished instances of the component * to render, reusing the same instance WILL NOT WORK. * Also the renderer instance cannot be reused for multiple lists, each list will need * a new instance of this renderer! * * @author Shai Almog */ public class GenericListCellRenderer implements ListCellRenderer, CellRenderer { private Button lastClickedComponent; private Vector pendingAnimations; /** * If this flag exists in a hashtable of data the renderer will enable/disable * the entries, the flag assumes either Boolean.TRUE or Boolean.FALSE. * Notice that just setting it to false when necessary will not work, when its * used it must be applied to all entries otherwise the reuse of the renderer * component will break this feature. */ public static final String ENABLED = "$$ENABLED$$"; /** * Put this flag as a hashtable key to indicate that a checkbox entry rendered by * this renderer should act as a "select all" entry and toggle all other entries. * The value for this entry is ignored */ public static final String SELECT_ALL_FLAG = "$$SELECTALL$$"; private Label focusComponent = new Label(); private Component selected; private Component unselected; private Component[] selectedEntries; private Component[] unselectedEntries; private Component selectedEven; private Component unselectedEven; private Component[] selectedEntriesEven; private Component[] unselectedEntriesEven; private Monitor mon = new Monitor(); private Component parentList; private boolean selectionListener = true; private boolean firstCharacterRTL; private boolean fisheye; /** * Constructs a generic renderer with the given selected/unselected components * * @param selected indicates the selected value for the renderer * @param unselected indicates the unselected value for the renderer */ public GenericListCellRenderer(Component selected, Component unselected) { if(selected == unselected) { throw new IllegalArgumentException("Must use distinct instances for renderer!"); } this.selected = selected; this.unselected = unselected; focusComponent.setUIID(selected.getUIID() + "Focus"); focusComponent.setFocus(true); selectedEntries = initRenderer(selected); unselectedEntries = initRenderer(unselected); firstCharacterRTL = UIManager.getInstance().isThemeConstant("firstCharRTLBool", false); addSelectedEntriesListener(selectedEntries); } private void addSelectedEntriesListener(Component[] e) { for(int iter = 0 ; iter < e.length ; iter++) { if(e[iter] instanceof Button) { ((Button)e[iter]).addActionListener(mon); } } } private Component[] initRenderer(Component r) { r.setCellRenderer(true); if(r instanceof Container) { Vector selectedVector = new Vector(); findComponentsOfInterest(r, selectedVector); return vectorToComponentArray(selectedVector); } else { return new Component[] {r}; } } /** * Allows partitioning the renderer into "areas" that can be clicked. When * receiving an action event in the list this method allows a developer to * query the renderer to "see" whether a button within the component was "touched" * by the user on a touch screen device. * This method will reset the value to null after returning a none-null value! * * @return a button or null */ public Button extractLastClickedComponent() { Button c = lastClickedComponent; lastClickedComponent = null; return c; } /** * Constructs a generic renderer with the given selected/unselected components for * odd/even values allowing a "pinstripe" effect * * @param odd indicates the selected value for the renderer * @param oddUnselected indicates the unselected value for the renderer * @param even indicates the selected value for the renderer * @param evenUnselected indicates the unselected value for the renderer */ public GenericListCellRenderer(Component odd, Component oddUnselected, Component even, Component evenUnselected) { this(odd, oddUnselected); selectedEven = even; unselectedEven = evenUnselected; selectedEntriesEven = initRenderer(even); unselectedEntriesEven = initRenderer(evenUnselected); addSelectedEntriesListener(selectedEntriesEven); } private Component[] vectorToComponentArray(Vector v) { Component[] result = new Component[v.size()]; for(int iter = 0 ; iter < result.length ; iter++) { result[iter] = (Component)v.elementAt(iter); } return result; } private void findComponentsOfInterest(Component cmp, Vector dest) { if(cmp instanceof Container) { Container c = (Container)cmp; int count = c.getComponentCount(); for(int iter = 0 ; iter < count ; iter++) { findComponentsOfInterest(c.getComponentAt(iter), dest); } return; } if((cmp instanceof Label || cmp instanceof TextArea) && cmp.getName() != null) { dest.addElement(cmp); return; } } /** * @inheritDoc */ public Component getCellRendererComponent(Component list, Object model, Object value, int index, boolean isSelected) { Component cmp; Component[] entries; if(!fisheye && !Display.getInstance().shouldRenderSelection(list)) { isSelected = false; } if(isSelected && (fisheye || list.hasFocus())) { cmp = selected; entries = selectedEntries; if(selectedEven != null && index % 2 == 0) { cmp = selectedEven; entries = selectedEntriesEven; // prevent the list from over-optimizing the background painting if(list instanceof List) { ((List)list).setMutableRendererBackgrounds(true); } } if(value instanceof Hashtable) { Hashtable h = (Hashtable)value; Boolean enabled = (Boolean)h.get(ENABLED); if(enabled != null) { cmp.setEnabled(enabled.booleanValue()); } for(int iter = 0 ; iter < entries.length ; iter++) { String currentName = entries[iter].getName(); Object val; if(currentName.equals("$number")) { val = "" + (index + 1); } else { // a selected entry might differ in its value to allow for // behavior such as rollover images val = h.get("#" + currentName); if(val == null) { val = h.get(currentName); } } setComponentValueWithTickering(entries[iter], val, list); entries[iter].setFocus(entries[iter].isFocusable()); } } else { setComponentValueWithTickering(entries[0], value, list); entries[0].setFocus(entries[0].isFocusable()); } return cmp; } else { cmp = unselected; entries = unselectedEntries; if(unselectedEven != null && index % 2 == 0) { cmp = unselectedEven; entries = unselectedEntriesEven; // prevent the list from over-optimizing the background painting if(list instanceof List) { ((List)list).setMutableRendererBackgrounds(true); } } if(value instanceof Hashtable) { Hashtable h = (Hashtable)value; Boolean enabled = (Boolean)h.get(ENABLED); if(enabled != null) { cmp.setEnabled(enabled.booleanValue()); } for(int iter = 0 ; iter < entries.length ; iter++) { String currentName = entries[iter].getName(); if(currentName.equals("$number")) { setComponentValue(entries[iter], "" + (index + 1)); continue; } setComponentValue(entries[iter], h.get(currentName)); } } else { setComponentValue(entries[0], value); } return cmp; } } /** * @inheritDoc */ public Component getListCellRendererComponent(List list, Object value, int index, boolean isSelected) { return getCellRendererComponent(list, list.getModel(), value, index, isSelected); } private boolean isSelectedValue(Object v) { return v != null && "true".equalsIgnoreCase(v.toString()); } private void setComponentValueWithTickering(Component cmp, Object value, Component l) { setComponentValue(cmp, value); if(cmp instanceof Label) { if(selectionListener) { if(l instanceof List) { ((List)l).addActionListener(mon); } parentList = l; } Label label = (Label)cmp; if(label.shouldTickerStart() && Display.getInstance().shouldRenderSelection()) { if(!label.isTickerRunning()) { parentList = l; if(parentList != null) { Form f = parentList.getComponentForm(); if(f != null) { f.registerAnimated(mon); label.startTicker(UIManager.getInstance().getLookAndFeel().getTickerSpeed(), true); } } } } else { if(label.isTickerRunning()) { label.stopTicker(); } label.setTextPosition(0); } } } /** * Initializes the given component with the given value * * @param cmp one of the components that is or is a part of the renderer * @param value the value to install into the component */ protected void setComponentValue(Component cmp, Object value) { // fixed components shouldn't be modified by the renderer, this allows for // hardcoded properties in the renderer. We still want them to go through the // process so renderer selected/unselected styles are applied if(cmp.getName().toLowerCase().endsWith("fixed")) { return; } if(cmp instanceof Label) { if(value instanceof Image) { Image i = (Image)value; if(i.isAnimation()) { if(pendingAnimations == null) { pendingAnimations = new Vector(); } if(!pendingAnimations.contains(i)) { pendingAnimations.addElement(i); if(parentList != null) { Form f = parentList.getComponentForm(); if(f != null) { f.registerAnimated(mon); } } } } ((Label)cmp).setIcon(i); ((Label)cmp).setText(""); return; } else { ((Label)cmp).setIcon(null); } if(cmp instanceof CheckBox) { ((CheckBox)cmp).setSelected(isSelectedValue(value)); return; } if(cmp instanceof RadioButton) { ((RadioButton)cmp).setSelected(isSelectedValue(value)); return; } Label l = (Label)cmp; if(value == null) { l.setText(""); } else { if(value instanceof Label){ l.setText(((Label)value).getText()); l.setIcon(((Label)value).getIcon()); }else{ l.setText(value.toString()); } } if(firstCharacterRTL) { String t = l.getText(); if(t.length() > 0) { l.setRTL(Display.getInstance().isRTL(t.charAt(0))); } } return; } if(cmp instanceof TextArea) { if(value == null) { ((TextArea)cmp).setText(""); } else { ((TextArea)cmp).setText(value.toString()); } } } /** * @inheritDoc */ public Component getListFocusComponent(List list) { return focusComponent; } /** * @inheritDoc */ public Component getFocusComponent(Component list) { return focusComponent; } /** * @return the selectionListener */ public boolean isSelectionListener() { return selectionListener; } /** * @param selectionListener the selectionListener to set */ public void setSelectionListener(boolean selectionListener) { if(parentList != null) { if(parentList instanceof List) { ((List)parentList).addActionListener(mon); } } this.selectionListener = selectionListener; } /** * @return the selected */ public Component getSelected() { return selected; } /** * @return the unselected */ public Component getUnselected() { return unselected; } /** * @return the selectedEven */ public Component getSelectedEven() { return selectedEven; } /** * @return the unselectedEven */ public Component getUnselectedEven() { return unselectedEven; } /** * In fisheye rendering mode the renderer maintains selected component drawing * @return the fisheye */ public boolean isFisheye() { return fisheye; } /** * In fisheye rendering mode the renderer maintains selected component drawing * @param fisheye the fisheye to set */ public void setFisheye(boolean fisheye) { this.fisheye = fisheye; } class Monitor implements ActionListener, Animation { private boolean selectAllChecked; private int selectAllOffset; /** * @inheritDoc */ public boolean animate() { if(parentList != null) { boolean repaint = false; if(pendingAnimations != null) { int s = pendingAnimations.size(); for(int iter = 0 ; iter < s ; iter++) { Image i = (Image)pendingAnimations.elementAt(iter); repaint = repaint || i.animate(); } pendingAnimations.removeAllElements(); } Form f = parentList.getComponentForm(); if(f != null) { if(parentList.hasFocus() && Display.getInstance().shouldRenderSelection(parentList)) { for(int iter = 0 ; iter < selectedEntries.length ; iter++) { if(selectedEntries[iter] instanceof Label) { Label l = (Label)selectedEntries[iter]; if(l.isTickerRunning()) { repaint = true; l.animate(); } } } } else { for(int iter = 0 ; iter < selectedEntries.length ; iter++) { if(selectedEntries[iter] instanceof Label) { Label l = (Label)selectedEntries[iter]; if(l.isTickerRunning()) { l.stopTicker(); repaint = true; } } } } if(repaint) { parentList.repaint(); } else { f.deregisterAnimated(this); } return false; } if(repaint) { parentList.repaint(); } } return false; } /** * @inheritDoc */ public void paint(Graphics g) { } /** * @inheritDoc */ public void actionPerformed(ActionEvent evt) { if(evt.getComponent() instanceof Button) { lastClickedComponent = (Button)evt.getComponent(); return; } if(parentList instanceof List) { // prevent list from losing focus on action parentList.setHandlesInput(true); Object selection = ((List)parentList).getSelectedItem(); if(selection instanceof Hashtable) { Hashtable h = (Hashtable)selection; Command cmd = (Command)h.get("$navigation"); if(cmd != null) { parentList.getComponentForm().dispatchCommand(cmd, new ActionEvent(parentList)); return; } for(int iter = 0 ; iter < selectedEntries.length ; iter++) { if(selectedEntries[iter] instanceof CheckBox || selectedEntries[iter] instanceof RadioButton) { boolean sel = !isSelectedValue(h.get(selectedEntries[iter].getName())); if(h.get(SELECT_ALL_FLAG) != null) { selectAllChecked = sel; selectAllOffset = ((List)parentList).getSelectedIndex(); // we need to toggle all entries int count = ((List)parentList).getModel().getSize(); String selectionVal = "" + sel; for(int x = 0 ; x < count ; x++) { Object o = ((List)parentList).getModel().getItemAt(x); if(o instanceof Hashtable) { ((Hashtable)o).put(selectedEntries[iter].getName(), selectionVal); } } } else { if(selectAllChecked) { selectAllChecked = false; Hashtable selAll = (Hashtable)((List)parentList).getModel().getItemAt(selectAllOffset); selAll.put(selectedEntries[iter].getName(), "false"); } h.put(selectedEntries[iter].getName(), "" + sel); } return; } } } } } } }