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