| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to you under the Apache License, |
| * Version 2.0 (the "License"); you may not use this file except in |
| * compliance with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package org.apache.pivot.wtk; |
| |
| import java.util.Comparator; |
| |
| import org.apache.pivot.beans.DefaultProperty; |
| import org.apache.pivot.collections.ArrayList; |
| import org.apache.pivot.collections.List; |
| import org.apache.pivot.collections.ListListener; |
| import org.apache.pivot.collections.Sequence; |
| import org.apache.pivot.collections.immutable.ImmutableList; |
| import org.apache.pivot.json.JSON; |
| import org.apache.pivot.json.JSONSerializer; |
| import org.apache.pivot.serialization.SerializationException; |
| import org.apache.pivot.util.ListenerList; |
| import org.apache.pivot.wtk.content.SpinnerItemRenderer; |
| |
| /** |
| * Component that presents a means of cycling through a list of items. |
| */ |
| @DefaultProperty("spinnerData") |
| public class Spinner extends Container { |
| /** |
| * {@link Renderer} interface to customize the appearance of the data in a |
| * Spinner. |
| */ |
| public interface ItemRenderer extends Renderer { |
| /** |
| * Prepares the renderer for layout or paint. |
| * |
| * @param item The item to render, or <tt>null</tt> if called to |
| * calculate preferred height for skins that assume a fixed renderer |
| * height. |
| * @param spinner The host component. |
| */ |
| public void render(Object item, Spinner spinner); |
| |
| /** |
| * Converts a spinner item to a string representation. |
| * |
| * @param item The item from the spinner's data. |
| * @return The item's string representation, or <tt>null</tt> if the item |
| * does not have a string representation. <p> Note that this method may |
| * be called often during keyboard navigation, so implementations should |
| * avoid unnecessary string allocations. |
| */ |
| public String toString(Object item); |
| } |
| |
| /** |
| * Translates between spinner and bind context data during data binding. |
| */ |
| public interface SpinnerDataBindMapping { |
| /** |
| * Converts a context value to spinner data during a |
| * {@link Component#load(Object)} operation. |
| * |
| * @param value The value retrieved from the user context. |
| * @return That object converted to a list. |
| */ |
| public List<?> toSpinnerData(Object value); |
| |
| /** |
| * Converts spinner data to a context value during a |
| * {@link Component#store(Object)} operation. |
| * |
| * @param spinnerData The spinner data list. |
| * @return This list converted to data that can be stored in the user context. |
| */ |
| public Object valueOf(List<?> spinnerData); |
| } |
| |
| /** |
| * Spinner skin interface. Spinner skins must implement this interface to |
| * facilitate additional communication between the component and the skin. |
| */ |
| public interface Skin { |
| public Bounds getContentBounds(); |
| } |
| |
| /** |
| * Translates between spinner and bind context data during data binding. |
| */ |
| public interface ItemBindMapping { |
| /** |
| * Returns the index of the item in the source list during a |
| * {@link Component#load(Object)} operation. |
| * |
| * @param spinnerData The source spinner data. |
| * @param value The value to locate. |
| * @return The index of first occurrence of the value if it exists in the |
| * list; <tt>-1</tt>, otherwise. |
| */ |
| public int indexOf(List<?> spinnerData, Object value); |
| |
| /** |
| * Retrieves the item at the given index during a |
| * {@link Component#store(Object)} operation. |
| * |
| * @param spinnerData The source spinner data. |
| * @param index The index of the value to retrieve. |
| * @return The item at the given index. |
| */ |
| public Object get(List<?> spinnerData, int index); |
| } |
| |
| private static class SpinnerListenerList extends WTKListenerList<SpinnerListener> implements |
| SpinnerListener { |
| @Override |
| public void spinnerDataChanged(Spinner spinner, List<?> previousSpinnerData) { |
| for (SpinnerListener listener : this) { |
| listener.spinnerDataChanged(spinner, previousSpinnerData); |
| } |
| } |
| |
| @Override |
| public void itemRendererChanged(Spinner spinner, Spinner.ItemRenderer previousItemRenderer) { |
| for (SpinnerListener listener : this) { |
| listener.itemRendererChanged(spinner, previousItemRenderer); |
| } |
| } |
| |
| @Override |
| public void circularChanged(Spinner spinner) { |
| for (SpinnerListener listener : this) { |
| listener.circularChanged(spinner); |
| } |
| } |
| } |
| |
| private static class SpinnerItemListenerList extends WTKListenerList<SpinnerItemListener> |
| implements SpinnerItemListener { |
| @Override |
| public void itemInserted(Spinner spinner, int index) { |
| for (SpinnerItemListener listener : this) { |
| listener.itemInserted(spinner, index); |
| } |
| } |
| |
| @Override |
| public void itemsRemoved(Spinner spinner, int index, int count) { |
| for (SpinnerItemListener listener : this) { |
| listener.itemsRemoved(spinner, index, count); |
| } |
| } |
| |
| @Override |
| public void itemUpdated(Spinner spinner, int index) { |
| for (SpinnerItemListener listener : this) { |
| listener.itemUpdated(spinner, index); |
| } |
| } |
| |
| @Override |
| public void itemsCleared(Spinner spinner) { |
| for (SpinnerItemListener listener : this) { |
| listener.itemsCleared(spinner); |
| } |
| } |
| |
| @Override |
| public void itemsSorted(Spinner spinner) { |
| for (SpinnerItemListener listener : this) { |
| listener.itemsSorted(spinner); |
| } |
| } |
| } |
| |
| private static class SpinnerSelectionListenerList extends |
| WTKListenerList<SpinnerSelectionListener> implements SpinnerSelectionListener { |
| @Override |
| public void selectedIndexChanged(Spinner spinner, int previousSelectedIndex) { |
| for (SpinnerSelectionListener listener : this) { |
| listener.selectedIndexChanged(spinner, previousSelectedIndex); |
| } |
| } |
| |
| @Override |
| public void selectedItemChanged(Spinner spinner, Object previousSelectedItem) { |
| for (SpinnerSelectionListener listener : this) { |
| listener.selectedItemChanged(spinner, previousSelectedItem); |
| } |
| } |
| } |
| |
| private static class SpinnerBindingListenerList extends WTKListenerList<SpinnerBindingListener> |
| implements SpinnerBindingListener { |
| @Override |
| public void spinnerDataKeyChanged(Spinner spinner, String previousSpinnerDataKey) { |
| for (SpinnerBindingListener listener : this) { |
| listener.spinnerDataKeyChanged(spinner, previousSpinnerDataKey); |
| } |
| } |
| |
| @Override |
| public void spinnerDataBindTypeChanged(Spinner spinner, BindType previousSpinnerDataBindType) { |
| for (SpinnerBindingListener listener : this) { |
| listener.spinnerDataBindTypeChanged(spinner, previousSpinnerDataBindType); |
| } |
| } |
| |
| @Override |
| public void spinnerDataBindMappingChanged(Spinner spinner, |
| Spinner.SpinnerDataBindMapping previousSpinnerDataBindMapping) { |
| for (SpinnerBindingListener listener : this) { |
| listener.spinnerDataBindMappingChanged(spinner, previousSpinnerDataBindMapping); |
| } |
| } |
| |
| @Override |
| public void selectedItemKeyChanged(Spinner spinner, String previousSelectedItemKey) { |
| for (SpinnerBindingListener listener : this) { |
| listener.selectedItemKeyChanged(spinner, previousSelectedItemKey); |
| } |
| } |
| |
| @Override |
| public void selectedItemBindTypeChanged(Spinner spinner, |
| BindType previousSelectedItemBindType) { |
| for (SpinnerBindingListener listener : this) { |
| listener.selectedItemBindTypeChanged(spinner, previousSelectedItemBindType); |
| } |
| } |
| |
| @Override |
| public void selectedItemBindMappingChanged(Spinner spinner, |
| ItemBindMapping previousSelectedItemBindMapping) { |
| for (SpinnerBindingListener listener : this) { |
| listener.selectedItemBindMappingChanged(spinner, previousSelectedItemBindMapping); |
| } |
| } |
| } |
| |
| private List<?> spinnerData = null; |
| |
| private ItemRenderer itemRenderer = null; |
| |
| private boolean circular = false; |
| private int selectedIndex = -1; |
| |
| private String spinnerDataKey = null; |
| private BindType spinnerDataBindType = BindType.BOTH; |
| private SpinnerDataBindMapping spinnerDataBindMapping = null; |
| |
| private String selectedItemKey = null; |
| private BindType selectedItemBindType = BindType.BOTH; |
| private ItemBindMapping selectedItemBindMapping = null; |
| |
| private ListListener<Object> spinnerDataListener = new ListListener<Object>() { |
| @Override |
| public void itemInserted(List<Object> list, int index) { |
| int previousSelectedIndex = selectedIndex; |
| if (index <= selectedIndex) { |
| selectedIndex++; |
| } |
| |
| // Notify listeners that items were inserted |
| spinnerItemListeners.itemInserted(Spinner.this, index); |
| |
| if (selectedIndex != previousSelectedIndex) { |
| spinnerSelectionListeners.selectedIndexChanged(Spinner.this, selectedIndex); |
| } |
| } |
| |
| @Override |
| public void itemsRemoved(List<Object> list, int index, Sequence<Object> items) { |
| int count = items.getLength(); |
| |
| int previousSelectedIndex = selectedIndex; |
| |
| if (selectedIndex >= index) { |
| if (selectedIndex < index + count) { |
| selectedIndex = -1; |
| } else { |
| selectedIndex -= count; |
| } |
| } |
| |
| spinnerItemListeners.itemsRemoved(Spinner.this, index, count); |
| |
| if (selectedIndex != previousSelectedIndex) { |
| spinnerSelectionListeners.selectedIndexChanged(Spinner.this, selectedIndex); |
| |
| if (selectedIndex == -1) { |
| spinnerSelectionListeners.selectedItemChanged(Spinner.this, null); |
| } |
| } |
| } |
| |
| @Override |
| public void itemUpdated(List<Object> list, int index, Object previousItem) { |
| spinnerItemListeners.itemUpdated(Spinner.this, index); |
| } |
| |
| @Override |
| public void listCleared(List<Object> list) { |
| int previousSelectedIndex = selectedIndex; |
| selectedIndex = -1; |
| |
| spinnerItemListeners.itemsCleared(Spinner.this); |
| |
| if (previousSelectedIndex != selectedIndex) { |
| spinnerSelectionListeners.selectedIndexChanged(Spinner.this, selectedIndex); |
| spinnerSelectionListeners.selectedItemChanged(Spinner.this, getSelectedItem()); |
| } |
| } |
| |
| @Override |
| public void comparatorChanged(List<Object> list, Comparator<Object> previousComparator) { |
| if (list.getComparator() != null) { |
| int previousSelectedIndex = selectedIndex; |
| selectedIndex = -1; |
| |
| spinnerItemListeners.itemsSorted(Spinner.this); |
| |
| if (previousSelectedIndex != selectedIndex) { |
| spinnerSelectionListeners.selectedIndexChanged(Spinner.this, selectedIndex); |
| spinnerSelectionListeners.selectedItemChanged(Spinner.this, getSelectedItem()); |
| } |
| } |
| } |
| }; |
| |
| private SpinnerListenerList spinnerListeners = new SpinnerListenerList(); |
| private SpinnerItemListenerList spinnerItemListeners = new SpinnerItemListenerList(); |
| private SpinnerSelectionListenerList spinnerSelectionListeners = new SpinnerSelectionListenerList(); |
| private SpinnerBindingListenerList spinnerBindingListeners = new SpinnerBindingListenerList(); |
| |
| private static final ItemRenderer DEFAULT_ITEM_RENDERER = new SpinnerItemRenderer(); |
| |
| /** |
| * Creates a spinner populated with an empty array list. <p> The default |
| * contents is an {@link ImmutableList} so that if the default property |
| * (which is "spinnerData") is invoked in a BXML file, |
| * <code>BXMLSerializer</code> trying to add to this immutable sequence will |
| * catch an exception and will do a {@link #setSpinnerData |
| * setSpinnerData(List<?>)} instead. |
| */ |
| public Spinner() { |
| this(new ImmutableList<>(new ArrayList<>())); |
| } |
| |
| /** |
| * Creates a spinner populated with the given spinner data. <p> Note that |
| * the default renderer uses (as last option) the toString method on list |
| * elements, so override it to return whatever you want to display as text, |
| * or implement your own custom renderer. |
| * |
| * @param spinnerData The data to set. |
| * @see SpinnerItemRenderer |
| */ |
| public Spinner(List<?> spinnerData) { |
| setItemRenderer(DEFAULT_ITEM_RENDERER); |
| setSpinnerData(spinnerData); |
| |
| installSkin(Spinner.class); |
| } |
| |
| /** |
| * @return The data currently presented by the spinner. |
| */ |
| public List<?> getSpinnerData() { |
| return spinnerData; |
| } |
| |
| /** |
| * Sets the spinner data. Clears any existing selection state. |
| * |
| * @param spinnerData The data to be presented by the spinner. |
| */ |
| @SuppressWarnings("unchecked") |
| public void setSpinnerData(List<?> spinnerData) { |
| if (spinnerData == null) { |
| throw new IllegalArgumentException("spinnerData is null."); |
| } |
| |
| List<?> previousSpinnerData = this.spinnerData; |
| |
| if (previousSpinnerData != spinnerData) { |
| int previousSelectedIndex = selectedIndex; |
| |
| if (previousSpinnerData != null) { |
| // Clear any existing selection |
| selectedIndex = -1; |
| |
| ((List<Object>) previousSpinnerData).getListListeners().remove(spinnerDataListener); |
| } |
| |
| ((List<Object>) spinnerData).getListListeners().add(spinnerDataListener); |
| |
| // Update the spinner data and fire change event |
| this.spinnerData = spinnerData; |
| spinnerListeners.spinnerDataChanged(this, previousSpinnerData); |
| |
| if (selectedIndex != previousSelectedIndex) { |
| spinnerSelectionListeners.selectedIndexChanged(Spinner.this, selectedIndex); |
| spinnerSelectionListeners.selectedItemChanged(this, null); |
| } |
| } |
| } |
| |
| public final void setSpinnerData(String spinnerData) { |
| if (spinnerData == null) { |
| throw new IllegalArgumentException("spinnerData is null."); |
| } |
| |
| try { |
| setSpinnerData(JSONSerializer.parseList(spinnerData)); |
| } catch (SerializationException exception) { |
| throw new IllegalArgumentException(exception); |
| } |
| } |
| |
| @Override |
| protected void setSkin(org.apache.pivot.wtk.Skin skin) { |
| if (!(skin instanceof Spinner.Skin)) { |
| throw new IllegalArgumentException("Skin class must implement " |
| + Spinner.Skin.class.getName()); |
| } |
| |
| super.setSkin(skin); |
| } |
| |
| /** |
| * @return The item renderer used for items in this list. |
| */ |
| public ItemRenderer getItemRenderer() { |
| return itemRenderer; |
| } |
| |
| /** |
| * Sets the item renderer to be used for items in this list. |
| * |
| * @param itemRenderer The new item renderer for the list. |
| */ |
| public void setItemRenderer(ItemRenderer itemRenderer) { |
| if (itemRenderer == null) { |
| throw new IllegalArgumentException("itemRenderer is null."); |
| } |
| |
| ItemRenderer previousItemRenderer = this.itemRenderer; |
| |
| if (previousItemRenderer != itemRenderer) { |
| this.itemRenderer = itemRenderer; |
| spinnerListeners.itemRendererChanged(this, previousItemRenderer); |
| } |
| } |
| |
| /** |
| * @return Whether or not this spinner's values are circular. |
| */ |
| public boolean isCircular() { |
| return circular; |
| } |
| |
| /** |
| * Set whether the values in this spinner wrap around from the end |
| * back to the beginning (and vice-versa). |
| * |
| * @param circular The new setting for this spinner. |
| */ |
| public void setCircular(boolean circular) { |
| if (circular != this.circular) { |
| this.circular = circular; |
| spinnerListeners.circularChanged(this); |
| } |
| } |
| |
| /** |
| * @return The currently selected index. |
| */ |
| public int getSelectedIndex() { |
| return selectedIndex; |
| } |
| |
| /** |
| * Sets the selection to the specified index. |
| * |
| * @param selectedIndex The index to select, or <tt>-1</tt> to clear the |
| * selection. |
| */ |
| public void setSelectedIndex(int selectedIndex) { |
| indexBoundsCheck("selectedIndex", selectedIndex, -1, spinnerData.getLength() - 1); |
| |
| int previousSelectedIndex = this.selectedIndex; |
| |
| if (previousSelectedIndex != selectedIndex) { |
| this.selectedIndex = selectedIndex; |
| spinnerSelectionListeners.selectedIndexChanged(this, previousSelectedIndex); |
| spinnerSelectionListeners.selectedItemChanged(this, |
| (previousSelectedIndex == -1) ? null : spinnerData.get(previousSelectedIndex)); |
| } |
| } |
| |
| public Object getSelectedItem() { |
| int index = getSelectedIndex(); |
| Object item = null; |
| |
| if (index >= 0) { |
| item = spinnerData.get(index); |
| } |
| |
| return item; |
| } |
| |
| @SuppressWarnings("unchecked") |
| public void setSelectedItem(Object item) { |
| setSelectedIndex((item == null) ? -1 : ((List<Object>) spinnerData).indexOf(item)); |
| } |
| |
| public String getSpinnerDataKey() { |
| return spinnerDataKey; |
| } |
| |
| public void setSpinnerDataKey(String spinnerDataKey) { |
| String previousSpinnerDataKey = this.spinnerDataKey; |
| if (previousSpinnerDataKey != spinnerDataKey) { |
| this.spinnerDataKey = spinnerDataKey; |
| spinnerBindingListeners.spinnerDataKeyChanged(this, previousSpinnerDataKey); |
| } |
| } |
| |
| public BindType getSpinnerDataBindType() { |
| return spinnerDataBindType; |
| } |
| |
| public void setSpinnerDataBindType(BindType spinnerDataBindType) { |
| if (spinnerDataBindType == null) { |
| throw new IllegalArgumentException(); |
| } |
| |
| BindType previousSpinnerDataBindType = this.spinnerDataBindType; |
| |
| if (previousSpinnerDataBindType != spinnerDataBindType) { |
| this.spinnerDataBindType = spinnerDataBindType; |
| spinnerBindingListeners.spinnerDataBindTypeChanged(this, previousSpinnerDataBindType); |
| } |
| } |
| |
| public SpinnerDataBindMapping getSpinnerDataBindMapping() { |
| return spinnerDataBindMapping; |
| } |
| |
| public void setSpinnerDataBindMapping(SpinnerDataBindMapping spinnerDataBindMapping) { |
| SpinnerDataBindMapping previousSpinnerDataBindMapping = this.spinnerDataBindMapping; |
| |
| if (previousSpinnerDataBindMapping != spinnerDataBindMapping) { |
| this.spinnerDataBindMapping = spinnerDataBindMapping; |
| spinnerBindingListeners.spinnerDataBindMappingChanged(this, |
| previousSpinnerDataBindMapping); |
| } |
| } |
| |
| public String getSelectedItemKey() { |
| return selectedItemKey; |
| } |
| |
| public void setSelectedItemKey(String selectedItemKey) { |
| String previousSelectedItemKey = this.selectedItemKey; |
| |
| if (previousSelectedItemKey != selectedItemKey) { |
| this.selectedItemKey = selectedItemKey; |
| spinnerBindingListeners.selectedItemKeyChanged(this, previousSelectedItemKey); |
| } |
| } |
| |
| public BindType getSelectedItemBindType() { |
| return selectedItemBindType; |
| } |
| |
| public void setSelectedItemBindType(BindType selectedItemBindType) { |
| if (selectedItemBindType == null) { |
| throw new IllegalArgumentException(); |
| } |
| |
| BindType previousSelectedItemBindType = this.selectedItemBindType; |
| if (previousSelectedItemBindType != selectedItemBindType) { |
| this.selectedItemBindType = selectedItemBindType; |
| spinnerBindingListeners.selectedItemBindTypeChanged(this, previousSelectedItemBindType); |
| } |
| } |
| |
| public ItemBindMapping getSelectedItemBindMapping() { |
| return selectedItemBindMapping; |
| } |
| |
| public void setSelectedItemBindMapping(ItemBindMapping selectedItemBindMapping) { |
| ItemBindMapping previousSelectedItemBindMapping = this.selectedItemBindMapping; |
| |
| if (previousSelectedItemBindMapping != selectedItemBindMapping) { |
| this.selectedItemBindMapping = selectedItemBindMapping; |
| spinnerBindingListeners.selectedItemBindMappingChanged(this, |
| previousSelectedItemBindMapping); |
| } |
| } |
| |
| @Override |
| @SuppressWarnings("unchecked") |
| public void load(Object context) { |
| // Bind to spinner data |
| if (spinnerDataKey != null && spinnerDataBindType != BindType.STORE |
| && JSON.containsKey(context, spinnerDataKey)) { |
| Object value = JSON.get(context, spinnerDataKey); |
| |
| List<?> spinnerDataLocal; |
| if (spinnerDataBindMapping == null) { |
| spinnerDataLocal = (List<?>) value; |
| } else { |
| spinnerDataLocal = spinnerDataBindMapping.toSpinnerData(value); |
| } |
| |
| setSpinnerData(spinnerDataLocal); |
| } |
| |
| // Bind to selected item |
| if (selectedItemKey != null && JSON.containsKey(context, selectedItemKey) |
| && selectedItemBindType != BindType.STORE) { |
| Object item = JSON.get(context, selectedItemKey); |
| |
| int index; |
| if (selectedItemBindMapping == null) { |
| index = ((List<Object>) spinnerData).indexOf(item); |
| } else { |
| index = selectedItemBindMapping.indexOf(spinnerData, item); |
| } |
| |
| setSelectedIndex(index); |
| } |
| } |
| |
| @Override |
| public void store(Object context) { |
| // Bind to spinner data |
| if (spinnerDataKey != null && spinnerDataBindType != BindType.LOAD) { |
| Object value; |
| if (spinnerDataBindMapping == null) { |
| value = spinnerData; |
| } else { |
| value = spinnerDataBindMapping.valueOf(spinnerData); |
| } |
| |
| JSON.put(context, spinnerDataKey, value); |
| } |
| |
| // Bind to selected item |
| if (selectedItemKey != null && selectedItemBindType != BindType.LOAD) { |
| Object item; |
| if (selectedItemBindMapping == null) { |
| if (selectedIndex == -1) { |
| item = null; |
| } else { |
| item = spinnerData.get(selectedIndex); |
| } |
| } else { |
| item = selectedItemBindMapping.get(spinnerData, selectedIndex); |
| } |
| |
| JSON.put(context, selectedItemKey, item); |
| } |
| } |
| |
| @Override |
| public void clear() { |
| if (spinnerDataKey != null) { |
| setSpinnerData(new ArrayList<>()); |
| } |
| |
| if (selectedItemKey != null) { |
| setSelectedItem(null); |
| } |
| } |
| |
| /** |
| * Clears the selection. |
| */ |
| public void clearSelection() { |
| setSelectedItem(null); |
| } |
| |
| /** |
| * Gets the bounding area of the spinner content (the area in which the item |
| * renderer will render the content). |
| * |
| * @return The bounding area of the spinner content. |
| */ |
| public Bounds getContentBounds() { |
| Spinner.Skin spinnerSkin = (Spinner.Skin) getSkin(); |
| return spinnerSkin.getContentBounds(); |
| } |
| |
| /** |
| * @return The spinner listener list. |
| */ |
| public ListenerList<SpinnerListener> getSpinnerListeners() { |
| return spinnerListeners; |
| } |
| |
| /** |
| * @return The spinner item listener list. |
| */ |
| public ListenerList<SpinnerItemListener> getSpinnerItemListeners() { |
| return spinnerItemListeners; |
| } |
| |
| /** |
| * @return The spinner selection listener list. |
| */ |
| public ListenerList<SpinnerSelectionListener> getSpinnerSelectionListeners() { |
| return spinnerSelectionListeners; |
| } |
| |
| /** |
| * @return The spinner binding listener list. |
| */ |
| public ListenerList<SpinnerBindingListener> getSpinnerBindingListeners() { |
| return spinnerBindingListeners; |
| } |
| } |