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 MinSizePanel();
086        p.setLayout(new BoxLayout(p, BoxLayout.Y_AXIS));
087        p.setVisible(false);
088
089        int dialogIndex = allDialogs.size() - 1;
090        mSpltPane.add(p, 'L'+Integer.toString(dialogIndex));
091        panels.add(p);
092
093        if (dlg.isDialogShowing()) {
094            dlg.showDialog();
095            if (dlg.isDialogInCollapsedView()) {
096                dlg.isCollapsed = false;    // pretend to be in Default view, this will be set back by collapse()
097                dlg.collapse();
098            }
099            if (doReconstruct) {
100                reconstruct(Action.INVISIBLE_TO_DEFAULT, dlg);
101            }
102            dlg.showNotify();
103        } else {
104            dlg.hideDialog();
105        }
106    }
107
108    static final class MinSizePanel extends JPanel {
109        @Override
110        public Dimension getMinimumSize() {
111            // Honoured by the MultiSplitPaneLayout when the entire Window is resized
112            return new Dimension(0, 40);
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            try {
326                t.destroy();
327            } catch (RuntimeException e) {
328                throw BugReport.intercept(e).put("dialog", t).put("dialog-class", t.getClass());
329            }
330        }
331    }
332
333    /**
334     * Replies the instance of a toggle dialog of type <code>type</code> managed by this
335     * map frame
336     *
337     * @param <T> toggle dialog type
338     * @param type the class of the toggle dialog, i.e. UserListDialog.class
339     * @return the instance of a toggle dialog of type <code>type</code> managed by this
340     * map frame; null, if no such dialog exists
341     *
342     */
343    public <T> T getToggleDialog(Class<T> type) {
344        for (ToggleDialog td : allDialogs) {
345            if (type.isInstance(td))
346                return type.cast(td);
347        }
348        return null;
349    }
350}