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