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}