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