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