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