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