001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.util;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.Adjustable;
007import java.awt.event.AdjustmentEvent;
008import java.awt.event.AdjustmentListener;
009import java.awt.event.ItemEvent;
010import java.awt.event.ItemListener;
011import java.util.ArrayList;
012import java.util.HashMap;
013import java.util.List;
014import java.util.Map;
015import java.util.Observable;
016import java.util.Observer;
017
018import javax.swing.JCheckBox;
019
020import org.openstreetmap.josm.tools.CheckParameterUtil;
021
022/**
023 * Synchronizes scrollbar adjustments between a set of {@link Adjustable}s. 
024 * Whenever the adjustment of one of the registered Adjustables is updated 
025 * the adjustment of the other registered Adjustables is adjusted too.
026 * @since 6147
027 */
028public class AdjustmentSynchronizer implements AdjustmentListener {
029
030    private final List<Adjustable> synchronizedAdjustables;
031    private final Map<Adjustable, Boolean> enabledMap;
032
033    private final Observable observable;
034
035    /**
036     * Constructs a new {@code AdjustmentSynchronizer}
037     */
038    public AdjustmentSynchronizer() {
039        synchronizedAdjustables = new ArrayList<Adjustable>();
040        enabledMap = new HashMap<Adjustable, Boolean>();
041        observable = new Observable();
042    }
043
044    /**
045     * Registers an {@link Adjustable} for participation in synchronized scrolling.
046     *
047     * @param adjustable the adjustable
048     */
049    public void participateInSynchronizedScrolling(Adjustable adjustable) {
050        if (adjustable == null)
051            return;
052        if (synchronizedAdjustables.contains(adjustable))
053            return;
054        synchronizedAdjustables.add(adjustable);
055        setParticipatingInSynchronizedScrolling(adjustable, true);
056        adjustable.addAdjustmentListener(this);
057    }
058
059    /**
060     * Event handler for {@link AdjustmentEvent}s
061     */
062    @Override
063    public void adjustmentValueChanged(AdjustmentEvent e) {
064        if (! enabledMap.get(e.getAdjustable()))
065            return;
066        for (Adjustable a : synchronizedAdjustables) {
067            if (a != e.getAdjustable() && isParticipatingInSynchronizedScrolling(a)) {
068                a.setValue(e.getValue());
069            }
070        }
071    }
072
073    /**
074     * Sets whether adjustable participates in adjustment synchronization or not
075     *
076     * @param adjustable the adjustable
077     */
078    protected void setParticipatingInSynchronizedScrolling(Adjustable adjustable, boolean isParticipating) {
079        CheckParameterUtil.ensureParameterNotNull(adjustable, "adjustable");
080        if (! synchronizedAdjustables.contains(adjustable))
081            throw new IllegalStateException(tr("Adjustable {0} not registered yet. Cannot set participation in synchronized adjustment.", adjustable));
082
083        enabledMap.put(adjustable, isParticipating);
084        observable.notifyObservers();
085    }
086
087    /**
088     * Returns true if an adjustable is participating in synchronized scrolling
089     *
090     * @param adjustable the adjustable
091     * @return true, if the adjustable is participating in synchronized scrolling, false otherwise
092     * @throws IllegalStateException thrown, if adjustable is not registered for synchronized scrolling
093     */
094    protected boolean isParticipatingInSynchronizedScrolling(Adjustable adjustable) throws IllegalStateException {
095        if (! synchronizedAdjustables.contains(adjustable))
096            throw new IllegalStateException(tr("Adjustable {0} not registered yet.", adjustable));
097
098        return enabledMap.get(adjustable);
099    }
100
101    /**
102     * Wires a {@link JCheckBox} to  the adjustment synchronizer, in such a way that:
103     * <li>
104     *   <ol>state changes in the checkbox control whether the adjustable participates
105     *      in synchronized adjustment</ol>
106     *   <ol>state changes in this {@link AdjustmentSynchronizer} are reflected in the
107     *      {@link JCheckBox}</ol>
108     * </li>
109     *
110     * @param view  the checkbox to control whether an adjustable participates in synchronized adjustment
111     * @param adjustable the adjustable
112     * @exception IllegalArgumentException thrown, if view is null
113     * @exception IllegalArgumentException thrown, if adjustable is null
114     */
115    public void adapt(final JCheckBox view, final Adjustable adjustable)  {
116        CheckParameterUtil.ensureParameterNotNull(adjustable, "adjustable");
117        CheckParameterUtil.ensureParameterNotNull(view, "view");
118
119        if (! synchronizedAdjustables.contains(adjustable)) {
120            participateInSynchronizedScrolling(adjustable);
121        }
122
123        // register an item lister with the check box
124        //
125        view.addItemListener(new ItemListener() {
126            @Override
127            public void itemStateChanged(ItemEvent e) {
128                switch(e.getStateChange()) {
129                case ItemEvent.SELECTED:
130                    if (!isParticipatingInSynchronizedScrolling(adjustable)) {
131                        setParticipatingInSynchronizedScrolling(adjustable, true);
132                    }
133                    break;
134                case ItemEvent.DESELECTED:
135                    if (isParticipatingInSynchronizedScrolling(adjustable)) {
136                        setParticipatingInSynchronizedScrolling(adjustable, false);
137                    }
138                    break;
139                }
140            }
141        });
142
143        observable.addObserver(
144                new Observer() {
145                    @Override
146                    public void update(Observable o, Object arg) {
147                        boolean sync = isParticipatingInSynchronizedScrolling(adjustable);
148                        if (view.isSelected() != sync) {
149                            view.setSelected(sync);
150                        }
151                    }
152                }
153        );
154        setParticipatingInSynchronizedScrolling(adjustable, true);
155        view.setSelected(true);
156    }
157}