001// License: GPL. See LICENSE file for details. 002package org.openstreetmap.josm.gui.dialogs; 003 004import java.awt.Dimension; 005import java.util.ArrayList; 006import java.util.List; 007 008import javax.swing.BoxLayout; 009import javax.swing.JPanel; 010import javax.swing.JSplitPane; 011 012import org.openstreetmap.josm.gui.widgets.MultiSplitLayout.Divider; 013import org.openstreetmap.josm.gui.widgets.MultiSplitLayout.Leaf; 014import org.openstreetmap.josm.gui.widgets.MultiSplitLayout.Node; 015import org.openstreetmap.josm.gui.widgets.MultiSplitLayout.Split; 016import org.openstreetmap.josm.gui.widgets.MultiSplitPane; 017import org.openstreetmap.josm.tools.CheckParameterUtil; 018import org.openstreetmap.josm.tools.Destroyable; 019 020public class DialogsPanel extends JPanel implements Destroyable { 021 protected List<ToggleDialog> allDialogs = new ArrayList<>(); 022 protected MultiSplitPane mSpltPane = new MultiSplitPane(); 023 protected static final int DIVIDER_SIZE = 5; 024 025 /** 026 * Panels that are added to the multisplitpane. 027 */ 028 private List<JPanel> panels = new ArrayList<>(); 029 030 private final JSplitPane parent; 031 public DialogsPanel(JSplitPane parent) { 032 this.parent = parent; 033 } 034 035 public boolean initialized = false; // read only from outside 036 037 public void initialize(List<ToggleDialog> pAllDialogs) { 038 if (initialized) 039 throw new IllegalStateException(); 040 initialized = true; 041 allDialogs = new ArrayList<>(); 042 043 for (ToggleDialog dialog: pAllDialogs) { 044 add(dialog, false); 045 } 046 047 this.add(mSpltPane); 048 reconstruct(Action.ELEMENT_SHRINKS, null); 049 } 050 051 public void add(ToggleDialog dlg) { 052 add(dlg, true); 053 } 054 055 public void add(ToggleDialog dlg, boolean doReconstruct) { 056 allDialogs.add(dlg); 057 int i = allDialogs.size() - 1; 058 dlg.setDialogsPanel(this); 059 dlg.setVisible(false); 060 final JPanel p = new JPanel() { 061 /** 062 * Honoured by the MultiSplitPaneLayout when the 063 * entire Window is resized. 064 */ 065 @Override 066 public Dimension getMinimumSize() { 067 return new Dimension(0, 40); 068 } 069 }; 070 p.setLayout(new BoxLayout(p, BoxLayout.Y_AXIS)); 071 p.setVisible(false); 072 073 mSpltPane.add(p, "L"+i); 074 panels.add(p); 075 076 if (dlg.isDialogShowing()) { 077 dlg.showDialog(); 078 if (dlg.isDialogInCollapsedView()) { 079 dlg.isCollapsed = false; // pretend to be in Default view, this will be set back by collapse() 080 dlg.collapse(); 081 } 082 if (doReconstruct) { 083 reconstruct(Action.INVISIBLE_TO_DEFAULT, dlg); 084 } 085 dlg.showNotify(); 086 } else { 087 dlg.hideDialog(); 088 } 089 } 090 091 /** 092 * What action was performed to trigger the reconstruction 093 */ 094 public enum Action { 095 INVISIBLE_TO_DEFAULT, 096 COLLAPSED_TO_DEFAULT, 097 /* INVISIBLE_TO_COLLAPSED, does not happen */ 098 ELEMENT_SHRINKS /* else. (Remaining elements have more space.) */ 099 } 100 /** 101 * Reconstruct the view, if the configurations of dialogs has changed. 102 * @param action what happened, so the reconstruction is necessary 103 * @param triggeredBy the dialog that caused the reconstruction 104 */ 105 public void reconstruct(Action action, ToggleDialog triggeredBy) { 106 107 final int N = allDialogs.size(); 108 109 /** 110 * reset the panels 111 */ 112 for (JPanel p: panels) { 113 p.removeAll(); 114 p.setVisible(false); 115 } 116 117 /** 118 * Add the elements to their respective panel. 119 * 120 * Each panel contains one dialog in default view and zero or more 121 * collapsed dialogs on top of it. The last panel is an exception 122 * as it can have collapsed dialogs at the bottom as well. 123 * If there are no dialogs in default view, show the collapsed ones 124 * in the last panel anyway. 125 */ 126 JPanel p = panels.get(N-1); // current Panel (start with last one) 127 int k = -1; // indicates that the current Panel index is N-1, but no default-view-Dialog has been added to this Panel yet. 128 for (int i=N-1; i >= 0 ; --i) { 129 final ToggleDialog dlg = allDialogs.get(i); 130 if (dlg.isDialogInDefaultView()) { 131 if (k == -1) { 132 k = N-1; 133 } else { 134 --k; 135 p = panels.get(k); 136 } 137 p.add(dlg, 0); 138 p.setVisible(true); 139 } 140 else if (dlg.isDialogInCollapsedView()) { 141 p.add(dlg, 0); 142 p.setVisible(true); 143 } 144 } 145 146 if (k == -1) { 147 k = N-1; 148 } 149 final int numPanels = N - k; 150 151 /** 152 * Determine the panel geometry 153 */ 154 if (action == Action.ELEMENT_SHRINKS) { 155 for (int i=0; i<N; ++i) { 156 final ToggleDialog dlg = allDialogs.get(i); 157 if (dlg.isDialogInDefaultView()) { 158 final int ph = dlg.getPreferredHeight(); 159 final int ah = dlg.getSize().height; 160 dlg.setPreferredSize(new Dimension(Integer.MAX_VALUE, (ah < 20 ? ph : ah))); 161 } 162 } 163 } else { 164 CheckParameterUtil.ensureParameterNotNull(triggeredBy, "triggeredBy"); 165 166 int sumP = 0; // sum of preferred heights of dialogs in default view (without the triggering dialog) 167 int sumA = 0; // sum of actual heights of dialogs in default view (without the triggering dialog) 168 int sumC = 0; // sum of heights of all collapsed dialogs (triggering dialog is never collapsed) 169 170 for (ToggleDialog dlg: allDialogs) { 171 if (dlg.isDialogInDefaultView()) { 172 if (dlg != triggeredBy) { 173 sumP += dlg.getPreferredHeight(); 174 sumA += dlg.getHeight(); 175 } 176 } else if (dlg.isDialogInCollapsedView()) { 177 sumC += dlg.getHeight(); 178 } 179 } 180 181 /** 182 * If we add additional dialogs on startup (e.g. geoimage), they may 183 * not have an actual height yet. 184 * In this case we simply reset everything to it's preferred size. 185 */ 186 if (sumA == 0) { 187 reconstruct(Action.ELEMENT_SHRINKS, null); 188 return; 189 } 190 191 /** total Height */ 192 final int H = mSpltPane.getMultiSplitLayout().getModel().getBounds().getSize().height; 193 194 /** space, that is available for dialogs in default view (after the reconfiguration) */ 195 final int s2 = H - (numPanels - 1) * DIVIDER_SIZE - sumC; 196 197 final int hp_trig = triggeredBy.getPreferredHeight(); 198 if (hp_trig <= 0) throw new IllegalStateException(); // Must be positive 199 200 /** The new dialog gets a fair share */ 201 final int hn_trig = hp_trig * s2 / (hp_trig + sumP); 202 triggeredBy.setPreferredSize(new Dimension(Integer.MAX_VALUE, hn_trig)); 203 204 /** This is remainig for the other default view dialogs */ 205 final int R = s2 - hn_trig; 206 207 /** 208 * Take space only from dialogs that are relatively large 209 */ 210 int D_m = 0; // additional space needed by the small dialogs 211 int D_p = 0; // available space from the large dialogs 212 for (int i=0; i<N; ++i) { 213 final ToggleDialog dlg = allDialogs.get(i); 214 if (dlg.isDialogInDefaultView() && dlg != triggeredBy) { 215 final int ha = dlg.getSize().height; // current 216 final int h0 = ha * R / sumA; // proportional shrinking 217 final int he = dlg.getPreferredHeight() * s2 / (sumP + hp_trig); // fair share 218 if (h0 < he) { // dialog is relatively small 219 int hn = Math.min(ha, he); // shrink less, but do not grow 220 D_m += hn - h0; 221 } else { // dialog is relatively large 222 D_p += h0 - he; 223 } 224 } 225 } 226 /** adjust, without changing the sum */ 227 for (int i=0; i<N; ++i) { 228 final ToggleDialog dlg = allDialogs.get(i); 229 if (dlg.isDialogInDefaultView() && dlg != triggeredBy) { 230 final int ha = dlg.getHeight(); 231 final int h0 = ha * R / sumA; 232 final int he = dlg.getPreferredHeight() * s2 / (sumP + hp_trig); 233 if (h0 < he) { 234 int hn = Math.min(ha, he); 235 dlg.setPreferredSize(new Dimension(Integer.MAX_VALUE, hn)); 236 } else { 237 int d; 238 try { 239 d = (h0-he) * D_m / D_p; 240 } catch (ArithmeticException e) { /* D_p may be zero - nothing wrong with that. */ 241 d = 0; 242 } 243 dlg.setPreferredSize(new Dimension(Integer.MAX_VALUE, h0 - d)); 244 } 245 } 246 } 247 } 248 249 /** 250 * create Layout 251 */ 252 final List<Node> ch = new ArrayList<>(); 253 254 for (int i = k; i <= N-1; ++i) { 255 if (i != k) { 256 ch.add(new Divider()); 257 } 258 Leaf l = new Leaf("L"+i); 259 l.setWeight(1.0 / numPanels); 260 ch.add(l); 261 } 262 263 if (numPanels == 1) { 264 Node model = ch.get(0); 265 mSpltPane.getMultiSplitLayout().setModel(model); 266 } else { 267 Split model = new Split(); 268 model.setRowLayout(false); 269 model.setChildren(ch); 270 mSpltPane.getMultiSplitLayout().setModel(model); 271 } 272 273 mSpltPane.getMultiSplitLayout().setDividerSize(DIVIDER_SIZE); 274 mSpltPane.getMultiSplitLayout().setFloatingDividers(true); 275 mSpltPane.revalidate(); 276 277 /** 278 * Hide the Panel, if there is nothing to show 279 */ 280 if (numPanels == 1 && panels.get(N-1).getComponents().length == 0) 281 { 282 parent.setDividerSize(0); 283 this.setVisible(false); 284 } else { 285 if (this.getWidth() != 0) { // only if josm started with hidden panel 286 this.setPreferredSize(new Dimension(this.getWidth(), 0)); 287 } 288 this.setVisible(true); 289 parent.setDividerSize(5); 290 parent.resetToPreferredSizes(); 291 } 292 } 293 294 @Override 295 public void destroy() { 296 for (ToggleDialog t : allDialogs) { 297 t.destroy(); 298 } 299 } 300 301 /** 302 * Replies the instance of a toggle dialog of type <code>type</code> managed by this 303 * map frame 304 * 305 * @param <T> 306 * @param type the class of the toggle dialog, i.e. UserListDialog.class 307 * @return the instance of a toggle dialog of type <code>type</code> managed by this 308 * map frame; null, if no such dialog exists 309 * 310 */ 311 public <T> T getToggleDialog(Class<T> type) { 312 for (ToggleDialog td : allDialogs) { 313 if (type.isInstance(td)) 314 return type.cast(td); 315 } 316 return null; 317 } 318}