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}