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.HashMap;
012import java.util.HashSet;
013import java.util.Map;
014import java.util.Observable;
015import java.util.Observer;
016import java.util.Set;
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 Set<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 HashSet<>();
040        enabledMap = new HashMap<>();
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 {@code adjustable} participates in adjustment synchronization or not
075     *
076     * @param adjustable the adjustable
077     * @param isParticipating {@code true} if {@code adjustable} participates in adjustment synchronization
078     */
079    protected void setParticipatingInSynchronizedScrolling(Adjustable adjustable, boolean isParticipating) {
080        CheckParameterUtil.ensureParameterNotNull(adjustable, "adjustable");
081        if (!synchronizedAdjustables.contains(adjustable))
082            throw new IllegalStateException(
083                    tr("Adjustable {0} not registered yet. Cannot set participation in synchronized adjustment.", adjustable));
084
085        enabledMap.put(adjustable, isParticipating);
086        observable.notifyObservers();
087    }
088
089    /**
090     * Returns true if an adjustable is participating in synchronized scrolling
091     *
092     * @param adjustable the adjustable
093     * @return true, if the adjustable is participating in synchronized scrolling, false otherwise
094     * @throws IllegalStateException if adjustable is not registered for synchronized scrolling
095     */
096    protected boolean isParticipatingInSynchronizedScrolling(Adjustable adjustable) {
097        if (!synchronizedAdjustables.contains(adjustable))
098            throw new IllegalStateException(tr("Adjustable {0} not registered yet.", adjustable));
099
100        return enabledMap.get(adjustable);
101    }
102
103    /**
104     * Wires a {@link JCheckBox} to  the adjustment synchronizer, in such a way that:
105     * <ol>
106     *   <li>state changes in the checkbox control whether the adjustable participates
107     *      in synchronized adjustment</li>
108     *   <li>state changes in this {@link AdjustmentSynchronizer} are reflected in the
109     *      {@link JCheckBox}</li>
110     * </ol>
111     *
112     * @param view  the checkbox to control whether an adjustable participates in synchronized adjustment
113     * @param adjustable the adjustable
114     * @throws IllegalArgumentException if view is null
115     * @throws IllegalArgumentException if adjustable is null
116     */
117    public void adapt(final JCheckBox view, final Adjustable adjustable)  {
118        CheckParameterUtil.ensureParameterNotNull(adjustable, "adjustable");
119        CheckParameterUtil.ensureParameterNotNull(view, "view");
120
121        if (!synchronizedAdjustables.contains(adjustable)) {
122            participateInSynchronizedScrolling(adjustable);
123        }
124
125        // register an item lister with the check box
126        //
127        view.addItemListener(new ItemListener() {
128            @Override
129            public void itemStateChanged(ItemEvent e) {
130                switch(e.getStateChange()) {
131                case ItemEvent.SELECTED:
132                    if (!isParticipatingInSynchronizedScrolling(adjustable)) {
133                        setParticipatingInSynchronizedScrolling(adjustable, true);
134                    }
135                    break;
136                case ItemEvent.DESELECTED:
137                    if (isParticipatingInSynchronizedScrolling(adjustable)) {
138                        setParticipatingInSynchronizedScrolling(adjustable, false);
139                    }
140                    break;
141                }
142            }
143        });
144
145        observable.addObserver(
146                new Observer() {
147                    @Override
148                    public void update(Observable o, Object arg) {
149                        boolean sync = isParticipatingInSynchronizedScrolling(adjustable);
150                        if (view.isSelected() != sync) {
151                            view.setSelected(sync);
152                        }
153                    }
154                }
155        );
156        setParticipatingInSynchronizedScrolling(adjustable, true);
157        view.setSelected(true);
158    }
159}