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