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