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}