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}