Author: tchemit Date: 2010-09-04 23:14:18 +0200 (Sat, 04 Sep 2010) New Revision: 2061 Url: http://nuiton.org/repositories/revision/jaxx/2061 Log: Evolution #848: Introduce jaxx.runtime.swing.editor.bean package Evolution #849: Deprecates EntityComboBox, use now BeanComboBox Added: trunk/jaxx-widgets/src/main/java/jaxx/runtime/swing/editor/bean/BeanComboBox.jaxx trunk/jaxx-widgets/src/main/java/jaxx/runtime/swing/editor/bean/BeanComboBoxHandler.java Modified: trunk/jaxx-widgets/src/main/java/jaxx/runtime/swing/EntityComboBox.jaxx trunk/jaxx-widgets/src/main/java/jaxx/runtime/swing/EntityComboBoxHandler.java Modified: trunk/jaxx-widgets/src/main/java/jaxx/runtime/swing/EntityComboBox.jaxx =================================================================== --- trunk/jaxx-widgets/src/main/java/jaxx/runtime/swing/EntityComboBox.jaxx 2010-09-04 21:00:33 UTC (rev 2060) +++ trunk/jaxx-widgets/src/main/java/jaxx/runtime/swing/EntityComboBox.jaxx 2010-09-04 21:14:18 UTC (rev 2061) @@ -21,6 +21,8 @@ License along with this program. If not, see <http://www.gnu.org/licenses/lgpl-3.0.html>. #L% + + @deprecated since 2.2, use now {@link BeanComboBox} --> <Table fill='both' insets='0' genericType='O' Modified: trunk/jaxx-widgets/src/main/java/jaxx/runtime/swing/EntityComboBoxHandler.java =================================================================== --- trunk/jaxx-widgets/src/main/java/jaxx/runtime/swing/EntityComboBoxHandler.java 2010-09-04 21:00:33 UTC (rev 2060) +++ trunk/jaxx-widgets/src/main/java/jaxx/runtime/swing/EntityComboBoxHandler.java 2010-09-04 21:14:18 UTC (rev 2061) @@ -27,6 +27,7 @@ import java.awt.Component; import jaxx.runtime.SwingUtil; +import jaxx.runtime.swing.editor.bean.BeanComboBoxHandler; import org.apache.commons.beanutils.PropertyUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -68,7 +69,9 @@ * * @author tchemit <chemit@codelutin.com> * @see EntityComboBox + * @deprecated since 2.2, use now {@link BeanComboBoxHandler}. */ +@Deprecated public class EntityComboBoxHandler<O> implements PropertyChangeListener { public static final Log log = LogFactory.getLog(EntityComboBoxHandler.class); Added: trunk/jaxx-widgets/src/main/java/jaxx/runtime/swing/editor/bean/BeanComboBox.jaxx =================================================================== --- trunk/jaxx-widgets/src/main/java/jaxx/runtime/swing/editor/bean/BeanComboBox.jaxx (rev 0) +++ trunk/jaxx-widgets/src/main/java/jaxx/runtime/swing/editor/bean/BeanComboBox.jaxx 2010-09-04 21:14:18 UTC (rev 2061) @@ -0,0 +1,137 @@ +<!-- + #%L + JAXX :: Widgets + + $Id$ + $HeadURL$ + %% + Copyright (C) 2008 - 2010 CodeLutin + %% + This program 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 3 of the + License, or (at your option) any later version. + + This program 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 Lesser Public License for more details. + + You should have received a copy of the GNU General Lesser Public + License along with this program. If not, see + <http://www.gnu.org/licenses/lgpl-3.0.html>. + #L% + --> + +<Table fill='both' insets='0' genericType='O' + onFocusGained='combobox.requestFocus()' + onFocusLost='hidePopup()'> + + <!-- auto complete property --> + <Boolean id='autoComplete' javaBean='false'/> + + <!-- show reset property --> + <Boolean id='showReset' javaBean='false'/> + + <!-- show decorator property --> + <Boolean id='showDecorator' javaBean='true'/> + + <!-- editable combo property --> + <Boolean id='editable' javaBean='true'/> + + <!-- bean property linked state --> + <String id='property' javaBean='""'/> + + <!-- bean property --> + <Object id='bean' javaBean='null'/> + + <!-- selectedItem property --> + <Object id='selectedItem' javaBean='null'/> + + <!-- sort index property --> + <Integer id='index' javaBean='0'/> + + <!-- datas of the combo-box --> + <java.util.List id='data' genericType='O' javaBean='null'/> + + <!-- model of sorted property --> + <ButtonGroup id='indexes' useToolTipText='true' + onStateChanged='setIndex((Integer)indexes.getSelectedValue())'/> + + <!-- ui handler --> + <BeanComboBoxHandler id='handler' genericType='O' constructorParams='this'/> + + <String id='selectedToolTipText' javaBean='null'/> + + <String id='notSelectedToolTipText' javaBean='null'/> + + <String id='popupTitleText' javaBean='null'/> + + <String id='i18nPrefix' javaBean='"entitycombobox.common."'/> + + <!-- popup to change sorted property--> + <JPopupMenu id='popup' + border='{new TitledBorder(_("entitycombobox.popup.title"))}' + onPopupMenuWillBecomeInvisible='getChangeDecorator().setSelected(false)' + onPopupMenuCanceled='getChangeDecorator().setSelected(false)'> + <JLabel id='popupLabel'/> + <JSeparator/> + </JPopupMenu> + + <script><![CDATA[ +import static org.nuiton.i18n.I18n.n_; +import jaxx.runtime.decorator.JXPathDecorator; + +public static final String DEFAULT_POPUP_LABEL = n_("entitycombobox.popup.label"); + +public static final String DEFAULT_SELECTED_TOOLTIP = n_("entitycombobox.sort.on"); + +public static final String DEFAULT_NOT_SELECTED_TOOLTIP = n_("entitycombobox.sort.off"); + +public void init(JXPathDecorator<O> decorator, java.util.List<O> data) { + handler.init(decorator, data); +} + +protected void hidePopup() { + if (popup.isVisible()) { + popup.setVisible(false); + } +} +]]> + </script> + <row> + <cell anchor='west'> + <!-- le boutton pour reinitialiser la valeur sélectionnée --> + <JToolBar floatable='false' borderPainted='false' visible='{isShowReset()}'> + <JButton actionIcon='combobox-reset' + toolTipText='entitycombobox.action.reset.tip' + focusable='false' + focusPainted='false' + enabled='{isEditable() && isEnabled()}' + onActionPerformed='setSelectedItem(null)'/> + </JToolBar> + + </cell> + <cell weightx='1'> + <!-- la liste déroulante --> + <JComboBox id='combobox' + selectedItem='{getSelectedItem()}' + enabled='{isEnabled()}' + editable='{isEditable()}' + focusable='{isEnabled() && isEditable()}' + onFocusGained='hidePopup()' + onItemStateChanged='setSelectedItem(combobox.getSelectedItem())'/> + </cell> + <cell anchor='east' fill='both' insets='0'> + <!-- le boutton pour changer le tri --> + <JToolBar floatable='false' borderPainted='false' visible='{isShowDecorator()}'> + <JToggleButton id='changeDecorator' + actionIcon='combobox-sort' + toolTipText='entitycombobox.action.sort.tip' + focusable='false' + focusPainted='false' + onActionPerformed='getHandler().togglePopup()'/> + </JToolBar> + </cell> + </row> +</Table> Added: trunk/jaxx-widgets/src/main/java/jaxx/runtime/swing/editor/bean/BeanComboBoxHandler.java =================================================================== --- trunk/jaxx-widgets/src/main/java/jaxx/runtime/swing/editor/bean/BeanComboBoxHandler.java (rev 0) +++ trunk/jaxx-widgets/src/main/java/jaxx/runtime/swing/editor/bean/BeanComboBoxHandler.java 2010-09-04 21:14:18 UTC (rev 2061) @@ -0,0 +1,491 @@ +/* + * #%L + * JAXX :: Widgets + * + * $Id$ + * $HeadURL$ + * %% + * Copyright (C) 2008 - 2010 CodeLutin + * %% + * This program 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 3 of the + * License, or (at your option) any later version. + * + * This program 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 Lesser Public License for more details. + * + * You should have received a copy of the GNU General Lesser Public + * License along with this program. If not, see + * <http://www.gnu.org/licenses/lgpl-3.0.html>. + * #L% + */ + +package jaxx.runtime.swing.editor.bean; + +import jaxx.runtime.SwingUtil; +import jaxx.runtime.decorator.Decorator; +import jaxx.runtime.decorator.DecoratorUtils; +import jaxx.runtime.decorator.JXPathDecorator; +import jaxx.runtime.decorator.MultiJXPathDecorator; +import jaxx.runtime.swing.JAXXButtonGroup; +import jaxx.runtime.swing.renderer.DecoratorListCellRenderer; +import org.apache.commons.beanutils.PropertyUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.jdesktop.swingx.autocomplete.AutoCompleteDecorator; +import org.jdesktop.swingx.autocomplete.AutoCompletePropertyChangeListener; +import org.jdesktop.swingx.autocomplete.ObjectToStringConverter; + +import javax.swing.JComboBox; +import javax.swing.JPopupMenu; +import javax.swing.JRadioButtonMenuItem; +import javax.swing.JToggleButton; +import javax.swing.SwingUtilities; +import javax.swing.text.Document; +import javax.swing.text.JTextComponent; +import java.awt.Component; +import java.awt.Dimension; +import java.awt.event.FocusEvent; +import java.awt.event.FocusListener; +import java.beans.Introspector; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.beans.PropertyDescriptor; +import java.lang.reflect.Method; +import java.util.List; + +import static org.nuiton.i18n.I18n._; +import static org.nuiton.i18n.I18n.n_; + +/** + * Le handler d'un {@link BeanComboBox}. + * <p/> + * Note: ce handler n'est pas staeless et n'est donc pas partageable entre plusieurs ui. + * + * @author tchemit <chemit@codelutin.com> + * @param <O> le type des objet contenus dans le modèle du composant. + * @see BeanComboBox + */ +public class BeanComboBoxHandler<O> implements PropertyChangeListener { + + public static final Log log = LogFactory.getLog(BeanComboBoxHandler.class); + + public static final String SELECTED_ITEM_PROPERTY = "selectedItem"; + + public static final String INDEX_PROPERTY = "index"; + + public static final String AUTO_COMPLETE_PROPERTY = "autoComplete"; + + public static final String DATA_PROPERTY = "data"; + + /** ui if the handler */ + protected BeanComboBox<O> ui; + + /** the mutator method on the property of boxed bean in the ui */ + protected Method mutator; + + /** the original document of the combbo box editor (keep it to make possible undecorate) */ + protected Document originalDocument; + + /** the convertor used to auto-complete */ + protected ObjectToStringConverter convertor; + + /** the decorator of data */ + protected MultiJXPathDecorator<O> decorator; + + protected boolean init; + + public BeanComboBoxHandler(BeanComboBox<O> ui) { + this.ui = ui; + } + + protected final FocusListener EDITOR_TEXT_COMP0NENT_FOCUSLISTENER = new FocusListener() { + + @Override + public void focusGained(FocusEvent e) { + if (log.isDebugEnabled()) { + log.debug("close popup from " + e); + } + ui.getPopup().setVisible(false); + } + + @Override + public void focusLost(FocusEvent e) { + } + }; + + /** + * Initialise le handler de l'ui + * + * @param decorator le decorateur a utiliser + * @param data la liste des données a gérer + */ + public void init(JXPathDecorator<O> decorator, List<O> data) { + + if (init) { + throw new IllegalStateException("can not init the handler twice"); + } + init = true; + if (decorator == null) { + throw new NullPointerException("can not have a null decorator as parameter"); + } + + JAXXButtonGroup indexes = ui.getIndexes(); + + MultiJXPathDecorator<O> d; + if (decorator instanceof MultiJXPathDecorator<?>) { + // should clone decorator ? + d = (MultiJXPathDecorator<O>) decorator; + } else { + d = DecoratorUtils.newMultiJXPathDecorator(decorator.getInternalClass(), decorator.getInitialExpression(), " - "); + } + this.decorator = d; + + // init combobox renderer base on given decorator + ui.getCombobox().setRenderer(new DecoratorListCellRenderer(d)); + + convertor = newDecoratedObjectToStringConverter(d); + + // keep a trace of original document (to make possible reverse autom-complete) + JTextComponent editorComponent = (JTextComponent) ui.getCombobox().getEditor().getEditorComponent(); + originalDocument = editorComponent.getDocument(); + + // build popup + preparePopup(d); + + ui.autoComplete = true; + + ui.addPropertyChangeListener(this); + + // set datas + ui.setData(data); + + // select sort button + indexes.setSelectedButton(ui.getIndex()); + } + + /** Toggle the popup visible state. */ + public void togglePopup() { + boolean newValue = !ui.getPopup().isVisible(); + + if (log.isTraceEnabled()) { + log.trace(newValue); + } + + if (!newValue) { + if (ui.getPopup() != null) { + ui.getPopup().setVisible(false); + } + return; + } + SwingUtilities.invokeLater(new Runnable() { + + @Override + public void run() { + ui.getPopup().pack(); + Dimension dim = ui.getPopup().getPreferredSize(); + JToggleButton invoker = ui.getChangeDecorator(); + ui.getPopup().show(invoker, (int) (invoker.getPreferredSize().getWidth() - dim.getWidth()), invoker.getHeight()); + } + }); + } + + /** + * Modifie l'état autoComplete de l'ui. + * + * @param oldValue l'ancienne valeur + * @param newValue la nouvelle valeur + */ + protected void setAutoComplete(Boolean oldValue, Boolean newValue) { + oldValue = oldValue != null && oldValue; + newValue = newValue != null && newValue; + if (oldValue == newValue) { + return; + } + if (log.isDebugEnabled()) { + log.debug("autocomplete state : <" + oldValue + " to " + newValue + ">"); + } + if (!newValue) { + JTextComponent editorComponent = (JTextComponent) ui.getCombobox().getEditor().getEditorComponent(); + editorComponent.removeFocusListener(EDITOR_TEXT_COMP0NENT_FOCUSLISTENER); + undecorate(ui.getCombobox(), originalDocument); + } else { + decorate(ui.getCombobox(), convertor); + JTextComponent editorComponent = (JTextComponent) ui.getCombobox().getEditor().getEditorComponent(); + editorComponent.addFocusListener(EDITOR_TEXT_COMP0NENT_FOCUSLISTENER); + } + } + + /** + * Modifie l'index du décorateur + * + * @param oldValue l'ancienne valeur + * @param newValue la nouvelle valeur + */ + @SuppressWarnings({"unchecked"}) + protected void setIndex(Integer oldValue, Integer newValue) { + if (newValue.equals(oldValue)) { + return; + } + if (log.isDebugEnabled()) { + log.debug("check state : <" + oldValue + " to " + newValue + ">"); + } + + // change decorator context + decorator.setContextIndex(newValue); + + // keep selected item + Object previousSelectedItem = ui.getSelectedItem(); + Boolean wasAutoComplete = ui.isAutoComplete(); + + if (wasAutoComplete) { + ui.setAutoComplete(false); + } + + // remove autocomplete + if (previousSelectedItem != null) { + ui.getCombobox().setSelectedItem(null); + ui.selectedItem = null; + } + + + try { + // Sort data with the decorator jxpath tokens. + DecoratorUtils.sort(decorator, ui.getData(), newValue); + } catch (Exception e) { + log.warn(e.getMessage(), e); + //System.out.println("newValue :: "+decorator+" : "+newValue); + //System.out.println("datas :: "+ui.getData()); + } + + // reload the model + SwingUtil.fillComboBox(ui.getCombobox(), ui.getData(), null); + + if (wasAutoComplete) { + ui.setAutoComplete(true); + } + + if (previousSelectedItem != null) { + ui.setSelectedItem(previousSelectedItem); + } + + ui.getCombobox().requestFocus(); + } + + /** + * Modifie la valeur sélectionnée dans la liste déroulante. + * + * @param oldValue l'ancienne valeur + * @param newValue la nouvelle valeur + */ + protected void setSelectedItem(Object oldValue, Object newValue) { + if (ui.getBean() == null) { + return; + } + + if (newValue == null) { + if (ui.getCombobox().getSelectedItem() == null) { + return; + } + ui.getCombobox().setSelectedItem(null); + + if (ui.isAutoComplete()) { + ui.setAutoComplete(false); + ui.setAutoComplete(true); + } + + if (oldValue == null) { + return; + } + } + if (log.isDebugEnabled()) { + log.debug(ui.getProperty() + " on " + ui.getBean().getClass() + " :: " + oldValue + " to " + newValue); + } + + try { + Method mut = getMutator(); + if (mut != null) { + mut.invoke(ui.getBean(), newValue); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** @return le document de l'éditeur avant complétion. */ + public Document getOriginalDocument() { + return originalDocument; + } + + public MultiJXPathDecorator<O> getDecorator() { + return decorator; + } + + /** + * Creation de l'ui pour modifier le décorateur. + * + * @param decorator le decorateur a utiliser + */ + protected void preparePopup(MultiJXPathDecorator<?> decorator) { + String selectedTip = ui.getSelectedToolTipText(); + if (selectedTip == null) { + // use default selected tip text + selectedTip = BeanComboBox.DEFAULT_SELECTED_TOOLTIP; + } + String notSelectedTip = ui.getNotSelectedToolTipText(); + if (notSelectedTip == null) { + // use default selected tip text + notSelectedTip = BeanComboBox.DEFAULT_NOT_SELECTED_TOOLTIP; + } + JPopupMenu popup = ui.getPopup(); + + //Container container = ui.getIndexesContainer(); + for (int i = 0, max = decorator.getNbContext(); i < max; i++) { + String property = ui.getI18nPrefix() + decorator.getProperty(i); + String propertyI18n = _(property); + JRadioButtonMenuItem button = new JRadioButtonMenuItem(propertyI18n); + button.putClientProperty(JAXXButtonGroup.BUTTON8GROUP_CLIENT_PROPERTY, ui.getIndexes()); + button.putClientProperty(JAXXButtonGroup.VALUE_CLIENT_PROPERTY, i); + popup.add(button); + if (selectedTip != null) { + button.putClientProperty(JAXXButtonGroup.SELECTED_TIP_CLIENT_PROPERTY, _(selectedTip, propertyI18n)); + } + if (notSelectedTip != null) { + button.putClientProperty(JAXXButtonGroup.NOT_SELECTED_TIP_CLIENT_PROPERTY, _(notSelectedTip, propertyI18n)); + } + button.setSelected(false); + ui.getIndexes().add(button); + } + String title = ui.getPopupTitleText(); + if (title == null) { + // use default popup title + title = BeanComboBox.DEFAULT_POPUP_LABEL; + + Class<?> internalClass = decorator.getInternalClass(); + String beanI18nKey; + if (internalClass == null) { + beanI18nKey = n_("entitycombobox.unknown.type"); + } else { + beanI18nKey = ui.getI18nPrefix() + Introspector.decapitalize(internalClass.getSimpleName()); + } + String beanI18n = _(beanI18nKey); + title = _(title, beanI18n); + } else { + title = _(title); + } + ui.getPopupLabel().setText(title); + ui.getPopup().setLabel(title); + ui.getPopup().invalidate(); + } + + public Class<?> getTargetClass() { + Method m = getMutator(); + return m == null ? null : m.getParameterTypes()[0]; + } + + /** @return le mutateur a utiliser pour modifier le bean associé. */ + protected Method getMutator() { + if (mutator == null) { + Object bean = ui.getBean(); + if (bean == null) { + throw new NullPointerException("could not find bean in " + ui); + } + String property = ui.getProperty(); + if (property == null) { + throw new NullPointerException("could not find property in " + ui); + } + + try { + PropertyDescriptor descriptor = PropertyUtils.getPropertyDescriptor(bean, property); + if (descriptor != null) { + mutator = descriptor.getWriteMethod(); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + return mutator; + } + + /** + * Encapsule un {@link Decorator} dans un {@link ObjectToStringConverter}. + * + * @param decorator le decorateur a encapsuler. + * @return le converter encapsule dans un {@link ObjectToStringConverter} + */ + public static ObjectToStringConverter newDecoratedObjectToStringConverter(final Decorator<?> decorator) { + + return new ObjectToStringConverter() { + + @Override + public String getPreferredStringForItem(Object item) { + return item instanceof String ? (String) item : item == null ? "" : decorator.toString(item); + } + }; + } + + /** + * Ajout l'auto-complétion sur une liste déroulante, en utilisant le + * converteur donné pour afficher les données. + * + * @param combo la combo à décorer + * @param convertor le converter utilisé pour afficher les données. + */ + public static void decorate(JComboBox combo, ObjectToStringConverter convertor) { + + AutoCompleteDecorator.decorate(combo, convertor); + } + + /** + * Désactive l'aut-complétion sur une liste déroulante, en y repositionnant + * le modèle du document d'édition d'avant auto-complétion. + * + * @param combo la liste déroulante à décorer + * @param originalDocument le document original de l'édtieur de la + * liste déroulante. + */ + public static void undecorate(JComboBox combo, Document originalDocument) { + + // has not to be editable + combo.setEditable(false); + + // configure the text component=editor component + Component c = combo.getEditor().getEditorComponent(); + JTextComponent editorComponent = (JTextComponent) c; + editorComponent.setDocument(originalDocument); + editorComponent.setText(null); + //remove old property change listener + for (PropertyChangeListener l : c.getPropertyChangeListeners("editor")) { + if (l instanceof AutoCompletePropertyChangeListener) { + c.removePropertyChangeListener("editor", l); + } + } + } + + @Override + public void propertyChange(PropertyChangeEvent evt) { + String propertyName = evt.getPropertyName(); + + if (SELECTED_ITEM_PROPERTY.equals(propertyName)) { + setSelectedItem(evt.getOldValue(), evt.getNewValue()); + return; + } + + if (INDEX_PROPERTY.equals(propertyName)) { + setIndex((Integer) evt.getOldValue(), (Integer) evt.getNewValue()); + return; + } + if (AUTO_COMPLETE_PROPERTY.equals(propertyName)) { + setAutoComplete((Boolean) evt.getOldValue(), (Boolean) evt.getNewValue()); + return; + } + if (DATA_PROPERTY.equals(propertyName)) { + // list has changed, force reload of index + setIndex(-1, ui.getIndex()); + } + + } +}