// License: GPL. For details, see LICENSE file. package org.openstreetmap.josm.gui.util; import static org.openstreetmap.josm.tools.I18n.tr; import java.awt.Adjustable; import java.awt.event.AdjustmentEvent; import java.awt.event.AdjustmentListener; import java.awt.event.ItemEvent; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import javax.swing.JCheckBox; import org.openstreetmap.josm.tools.CheckParameterUtil; /** * Synchronizes scrollbar adjustments between a set of {@link Adjustable}s. * Whenever the adjustment of one of the registered Adjustables is updated * the adjustment of the other registered Adjustables is adjusted too. * @since 6147 */ public class AdjustmentSynchronizer implements AdjustmentListener { private final Set<Adjustable> synchronizedAdjustables; private final Map<Adjustable, Boolean> enabledMap; private final ChangeNotifier observable; /** * Constructs a new {@code AdjustmentSynchronizer} */ public AdjustmentSynchronizer() { synchronizedAdjustables = new HashSet<>(); enabledMap = new HashMap<>(); observable = new ChangeNotifier(); } /** * Registers an {@link Adjustable} for participation in synchronized scrolling. * * @param adjustable the adjustable */ public void participateInSynchronizedScrolling(Adjustable adjustable) { if (adjustable == null) return; if (synchronizedAdjustables.contains(adjustable)) return; synchronizedAdjustables.add(adjustable); setParticipatingInSynchronizedScrolling(adjustable, true); adjustable.addAdjustmentListener(this); } /** * Event handler for {@link AdjustmentEvent}s */ @Override public void adjustmentValueChanged(AdjustmentEvent e) { if (!enabledMap.get(e.getAdjustable())) return; for (Adjustable a : synchronizedAdjustables) { if (a != e.getAdjustable() && isParticipatingInSynchronizedScrolling(a)) { a.setValue(e.getValue()); } } } /** * Sets whether {@code adjustable} participates in adjustment synchronization or not * * @param adjustable the adjustable * @param isParticipating {@code true} if {@code adjustable} participates in adjustment synchronization */ protected void setParticipatingInSynchronizedScrolling(Adjustable adjustable, boolean isParticipating) { CheckParameterUtil.ensureParameterNotNull(adjustable, "adjustable"); if (!synchronizedAdjustables.contains(adjustable)) throw new IllegalStateException( tr("Adjustable {0} not registered yet. Cannot set participation in synchronized adjustment.", adjustable)); enabledMap.put(adjustable, isParticipating); observable.fireStateChanged(); } /** * Returns true if an adjustable is participating in synchronized scrolling * * @param adjustable the adjustable * @return true, if the adjustable is participating in synchronized scrolling, false otherwise * @throws IllegalStateException if adjustable is not registered for synchronized scrolling */ protected boolean isParticipatingInSynchronizedScrolling(Adjustable adjustable) { if (!synchronizedAdjustables.contains(adjustable)) throw new IllegalStateException(tr("Adjustable {0} not registered yet.", adjustable)); return enabledMap.get(adjustable); } /** * Wires a {@link JCheckBox} to the adjustment synchronizer, in such a way that: * <ol> * <li>state changes in the checkbox control whether the adjustable participates * in synchronized adjustment</li> * <li>state changes in this {@link AdjustmentSynchronizer} are reflected in the * {@link JCheckBox}</li> * </ol> * * @param view the checkbox to control whether an adjustable participates in synchronized adjustment * @param adjustable the adjustable * @throws IllegalArgumentException if view is null * @throws IllegalArgumentException if adjustable is null */ public void adapt(final JCheckBox view, final Adjustable adjustable) { CheckParameterUtil.ensureParameterNotNull(adjustable, "adjustable"); CheckParameterUtil.ensureParameterNotNull(view, "view"); if (!synchronizedAdjustables.contains(adjustable)) { participateInSynchronizedScrolling(adjustable); } // register an item lister with the check box // view.addItemListener(e -> { switch(e.getStateChange()) { case ItemEvent.SELECTED: if (!isParticipatingInSynchronizedScrolling(adjustable)) { setParticipatingInSynchronizedScrolling(adjustable, true); } break; case ItemEvent.DESELECTED: if (isParticipatingInSynchronizedScrolling(adjustable)) { setParticipatingInSynchronizedScrolling(adjustable, false); } break; default: // Do nothing } }); observable.addChangeListener(e -> { boolean sync = isParticipatingInSynchronizedScrolling(adjustable); if (view.isSelected() != sync) { view.setSelected(sync); } }); setParticipatingInSynchronizedScrolling(adjustable, true); view.setSelected(true); } }