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.Set; 015 016import javax.swing.JCheckBox; 017import javax.swing.event.ChangeEvent; 018import javax.swing.event.ChangeListener; 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 ChangeNotifier observable; 034 035 /** 036 * Constructs a new {@code AdjustmentSynchronizer} 037 */ 038 public AdjustmentSynchronizer() { 039 synchronizedAdjustables = new HashSet<>(); 040 enabledMap = new HashMap<>(); 041 observable = new ChangeNotifier(); 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.fireStateChanged(); 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 default: // Do nothing 142 } 143 } 144 }); 145 146 observable.addChangeListener( 147 new ChangeListener() { 148 @Override 149 public void stateChanged(ChangeEvent e) { 150 boolean sync = isParticipatingInSynchronizedScrolling(adjustable); 151 if (view.isSelected() != sync) { 152 view.setSelected(sync); 153 } 154 } 155 } 156 ); 157 setParticipatingInSynchronizedScrolling(adjustable, true); 158 view.setSelected(true); 159 } 160}