/* * 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.codename1.ui.list; import com.codename1.cloud.CloudObject; import com.codename1.ui.CheckBox; import com.codename1.ui.Button; import com.codename1.ui.Command; import com.codename1.ui.Component; import com.codename1.ui.Container; import com.codename1.ui.Display; import com.codename1.ui.EncodedImage; import com.codename1.ui.Form; import com.codename1.ui.Graphics; import com.codename1.ui.Image; import com.codename1.ui.Label; import com.codename1.ui.List; import com.codename1.ui.RadioButton; import com.codename1.ui.Slider; import com.codename1.ui.TextArea; import com.codename1.ui.URLImage; import com.codename1.ui.animations.Animation; import com.codename1.ui.events.ActionEvent; import com.codename1.ui.events.ActionListener; import com.codename1.ui.plaf.UIManager; import java.util.Map; import java.util.ArrayList; import java.util.HashMap; /** * <p>The generic list cell renderer can display containers or arbitrary Codename One components * as items in a list, <b>notice</b> that * <a href="https://www.codenameone.com/blog/avoiding-lists.html">we strongly * discourage usage of lists</a>.. It relies on the source data being a {@code Map} object. It extracts values from * the {@code Map} using the component name as an indication to the Map key lookup.<br> * This renderer supports label tickering, check boxes/radio buttons etc. seamlessly.</p> * <p> * Please notice that you must use at least two distinct instances of the component * when passing them to the constructor, reusing the same instance <b>WILL NOT WORK!</b><br> * Furthermore, the renderer instance cannot be reused for multiple lists, each list will need * a new instance of this renderer!</p> * <p> * Sample usage for this renderer follows: * </p> * <script src="https://gist.github.com/codenameone/15a2370c500e07a8fcf8.js"></script> * <img src="https://www.codenameone.com/img/developer-guide/components-generic-list-cell-renderer.png" alt="Sample of using the generic list cell renderer" /> * * <script src="https://gist.github.com/codenameone/15a2370c500e07a8fcf8.js"></script> * * @author Shai Almog */ public class GenericListCellRenderer<T> implements ListCellRenderer<T>, CellRenderer<T> { /** * The default adapter to use for image URLs * @return the defaultAdapter */ public static URLImage.ImageAdapter getDefaultAdapter() { return defaultAdapter; } /** * The default adapter to use for image URLs * @param aDefaultAdapter the defaultAdapter to set */ public static void setDefaultAdapter(URLImage.ImageAdapter aDefaultAdapter) { defaultAdapter = aDefaultAdapter; } private Button lastClickedComponent; private ArrayList<Image> pendingAnimations; /** * If this flag exists in a Map 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 Map 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; private boolean waitingForRegisterAnimation; private HashMap<String, EncodedImage> placeholders = new HashMap<String, EncodedImage>(); private static URLImage.ImageAdapter defaultAdapter = URLImage.RESIZE_SCALE; private URLImage.ImageAdapter adapter = defaultAdapter; /** * 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 = selected.getUIManager().isThemeConstant("firstCharRTLBool", false); addSelectedEntriesListener(selectedEntries); addSelectedEntriesListener(unselectedEntries); } void deinitialize(List l) { removeSelectedEntriesListener(selectedEntries); removeSelectedEntriesListener(unselectedEntries); l.removeActionListener(mon); } /** * Updates the placeholder instances, this is useful for changing the URLImage placeholder in runtime as * might happen in the designer */ public void updateIconPlaceholders() { updateIconPlaceholders(selectedEntries); updateIconPlaceholders(unselectedEntries); } private void updateIconPlaceholders(Component[] e) { int elen = e.length; for(int iter = 0 ; iter < elen ; iter++) { String n = e[iter].getName(); if(n != null) { if(n.endsWith("_URLImage") && e[iter] instanceof Label) { placeholders.put(n, (EncodedImage)((Label)e[iter]).getIcon()); } } } } private void removeSelectedEntriesListener(Component[] e) { int elen = e.length; for(int iter = 0 ; iter < elen ; iter++) { if(e[iter] instanceof Button) { ((Button)e[iter]).removeActionListener(mon); } } } private void addSelectedEntriesListener(Component[] e) { int elen = e.length; for(int iter = 0 ; iter < elen ; iter++) { if(e[iter] instanceof Button) { ((Button)e[iter]).addActionListener(mon); } String n = e[iter].getName(); if(n != null) { if(n.endsWith("_URLImage") && e[iter] instanceof Label) { placeholders.put(n, (EncodedImage)((Label)e[iter]).getIcon()); } } } } private Component[] initRenderer(Component r) { r.setCellRenderer(true); if(r instanceof Container) { ArrayList selectedVector = new ArrayList(); 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); addSelectedEntriesListener(unselectedEntriesEven); } private Component[] vectorToComponentArray(ArrayList v) { Component[] result = new Component[v.size()]; int rlen = result.length; for(int iter = 0 ; iter < rlen ; iter++) { result[iter] = (Component)v.get(iter); } return result; } private void findComponentsOfInterest(Component cmp, ArrayList 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; } // performance optimization for fixed images in lists if(cmp.getName() != null) { if(cmp instanceof Label) { Label l = (Label)cmp; if(l.getName().toLowerCase().endsWith("fixed") && l.getIcon() != null) { l.getIcon().lock(); } dest.add(cmp); return; } if(cmp instanceof TextArea) { dest.add(cmp); return; } } } /** * {@inheritDoc} */ public Component getCellRendererComponent(Component list, Object model, T 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); } } cmp.setFocus(true); boolean lead = false; if(cmp instanceof Container) { lead = ((Container)cmp).getLeadComponent() != null; } if(value instanceof Map) { Map h = (Map)value; Boolean enabled = (Boolean)h.get(ENABLED); if(enabled != null) { cmp.setEnabled(enabled.booleanValue()); } int elen = entries.length; for(int iter = 0 ; iter < elen ; 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); } val = updateModelValues(h, currentName, entries, iter, val); } setComponentValueWithTickering(entries[iter], val, list, cmp); entries[iter].setFocus(lead || entries[iter].isFocusable()); } } else { if(value instanceof CloudObject) { CloudObject h = (CloudObject)value; Boolean enabled = (Boolean)h.getBoolean(ENABLED); if(enabled != null) { cmp.setEnabled(enabled.booleanValue()); } int elen = entries.length; for(int iter = 0 ; iter < elen ; 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.getObject("#" + currentName); if(val == null) { val = h.getObject(currentName); } } setComponentValueWithTickering(entries[iter], val, list, cmp); entries[iter].setFocus(entries[iter].isFocusable()); } } else { setComponentValueWithTickering(entries[0], value, list, cmp); 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); } } cmp.setFocus(false); if(value instanceof Map) { Map h = (Map)value; Boolean enabled = (Boolean)h.get(ENABLED); if(enabled != null) { cmp.setEnabled(enabled.booleanValue()); } int elen = entries.length; for(int iter = 0 ; iter < elen ; iter++) { String currentName = entries[iter].getName(); if(currentName.equals("$number")) { setComponentValue(entries[iter], "" + (index + 1), list, cmp); continue; } Object val = h.get(currentName); val = updateModelValues(h, currentName, entries, iter, val); setComponentValue(entries[iter], val, list, cmp); } } else { if(value instanceof CloudObject) { CloudObject h = (CloudObject)value; Boolean enabled = h.getBoolean(ENABLED); if(enabled != null) { cmp.setEnabled(enabled.booleanValue()); } int elen = entries.length; for(int iter = 0 ; iter < elen ; iter++) { String currentName = entries[iter].getName(); if(currentName.equals("$number")) { setComponentValue(entries[iter], "" + (index + 1), list, cmp); continue; } setComponentValue(entries[iter], h.getObject(currentName), list, cmp); } } else { if(entries.length > 0) { setComponentValue(entries[0], value, list, cmp); } } } return cmp; } } private Object updateModelValues(Map h, String currentName, Component[] entries, int iter, Object val) { String uiid = (String)h.get(currentName + "_uiid"); if(uiid != null) { entries[iter].setUIID(uiid); } if(currentName.endsWith("_URLImage")) { URLImage img = (URLImage)h.get(currentName + "Actual"); if(img != null) { val = img; } else { String name = (String)h.get(currentName + "Name"); if(name == null) { name = val.toString(); name = name.substring(name.lastIndexOf('/')); } val = URLImage.createToStorage(placeholders.get(currentName), name, val.toString(), adapter); h.put(currentName + "Actual", val); } } return val; } /** * {@inheritDoc} */ public Component getListCellRendererComponent(List list, T 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, Component rootRenderer) { setComponentValue(cmp, value, l, rootRenderer); 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(cmp.getUIManager().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 */ private void setComponentValue(Component cmp, Object value, Component parent, Component rootRenderer) { // 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 ArrayList<Image>(); } if(!pendingAnimations.contains(i)) { pendingAnimations.add(i); if(parentList == null) { parentList = parent; } if(parentList != null) { Form f = parentList.getComponentForm(); if(f != null) { f.registerAnimated(mon); waitingForRegisterAnimation = false; } else { waitingForRegisterAnimation = true; } } } else { if(waitingForRegisterAnimation) { if(parentList != null) { Form f = parentList.getComponentForm(); if(f != null) { f.registerAnimated(mon); waitingForRegisterAnimation = false; } } } } } Image oldImage = ((Label)cmp).getIcon(); ((Label)cmp).setIcon(i); ((Label)cmp).setText(""); if(oldImage == null || oldImage.getWidth() != i.getWidth() || oldImage.getHeight() != i.getHeight()) { ((Container)rootRenderer).revalidate(); } 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; } if(cmp instanceof Slider) { ((Slider)cmp).setProgress(((Integer)value).intValue()); 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; } /** * The adapter used when dealing with image URL's * @return the adapter */ public URLImage.ImageAdapter getAdapter() { return adapter; } /** * The adapter used when dealing with image URL's * @param adapter the adapter to set */ public void setAdapter(URLImage.ImageAdapter adapter) { this.adapter = adapter; } class Monitor implements ActionListener, Animation { private boolean selectAllChecked; private int selectAllOffset; /** * {@inheritDoc} */ public boolean animate() { boolean hasAnimations = false; if(parentList != null) { boolean repaint = false; if(pendingAnimations != null && pendingAnimations.size() > 0) { int s = pendingAnimations.size(); hasAnimations = true; for(int iter = 0 ; iter < s ; iter++) { Image i = (Image)pendingAnimations.get(iter); repaint = i.animate() || repaint; } if(repaint) { pendingAnimations.clear(); } else { // flush the queue if we have too many animations if(pendingAnimations.size() > 20) { repaint = true; } } } Form f = parentList.getComponentForm(); if(f != null) { if(parentList.hasFocus() && Display.getInstance().shouldRenderSelection(parentList)) { int slen = selectedEntries.length; for(int iter = 0 ; iter < slen ; iter++) { if(selectedEntries[iter] instanceof Label) { Label l = (Label)selectedEntries[iter]; if(l.isTickerRunning()) { repaint = true; l.animate(); } } } } else { int slen = selectedEntries.length; for(int iter = 0 ; iter < slen ; iter++) { if(selectedEntries[iter] instanceof Label) { Label l = (Label)selectedEntries[iter]; if(l.isTickerRunning()) { l.stopTicker(); repaint = true; } } } } if(repaint) { parentList.repaint(); } else { if(!hasAnimations) { 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 Map) { Map h = (Map)selection; Command cmd = (Command)h.get("$navigation"); if(cmd != null) { parentList.getComponentForm().dispatchCommand(cmd, new ActionEvent(cmd,ActionEvent.Type.Command)); return; } int slen = selectedEntries.length; for(int iter = 0 ; iter < slen ; 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 Map) { ((Map)o).put(selectedEntries[iter].getName(), selectionVal); } } } else { if(selectAllChecked) { selectAllChecked = false; Map selAll = (Map)((List)parentList).getModel().getItemAt(selectAllOffset); selAll.put(selectedEntries[iter].getName(), "false"); } h.put(selectedEntries[iter].getName(), "" + sel); } return; } } } } } } }