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
020/**
021 * This is the panel displayed on the right side of JOSM. It displays a list of panels.
022 */
023public class DialogsPanel extends JPanel implements Destroyable {
024    private final List<ToggleDialog> allDialogs = new ArrayList<>();
025    private final MultiSplitPane mSpltPane = new MultiSplitPane();
026    private static final int DIVIDER_SIZE = 5;
027
028    /**
029     * Panels that are added to the multisplitpane.
030     */
031    private final List<JPanel> panels = new ArrayList<>();
032
033    /**
034     * If {@link #initialize(List)} was called. read only from outside
035     */
036    public boolean initialized;
037
038    private final JSplitPane parent;
039
040    /**
041     * Creates a new {@link DialogsPanel}.
042     * @param parent The parent split pane that allows this panel to change it's size.
043     */
044    public DialogsPanel(JSplitPane parent) {
045        this.parent = parent;
046    }
047
048    /**
049     * Initializes this panel
050     * @param pAllDialogs The list of dialogs this panel should contain on start.
051     */
052    public void initialize(List<ToggleDialog> pAllDialogs) {
053        if (initialized) {
054            throw new IllegalStateException("Panel can only be initialized once.");
055        }
056        initialized = true;
057        allDialogs.clear();
058
059        for (ToggleDialog dialog: pAllDialogs) {
060            add(dialog, false);
061        }
062
063        this.add(mSpltPane);
064        reconstruct(Action.ELEMENT_SHRINKS, null);
065    }
066
067    /**
068     * Add a new {@link ToggleDialog} to the list of known dialogs and trigger reconstruct.
069     * @param dlg The dialog to add
070     */
071    public void add(ToggleDialog dlg) {
072        add(dlg, true);
073    }
074
075    /**
076     * Add a new {@link ToggleDialog} to the list of known dialogs.
077     * @param dlg The dialog to add
078     * @param doReconstruct <code>true</code> if reconstruction should be triggered.
079     */
080    public void add(ToggleDialog dlg, boolean doReconstruct) {
081        allDialogs.add(dlg);
082        dlg.setDialogsPanel(this);
083        dlg.setVisible(false);
084        final JPanel p = new JPanel() {
085            /**
086             * Honoured by the MultiSplitPaneLayout when the
087             * entire Window is resized.
088             */
089            @Override
090            public Dimension getMinimumSize() {
091                return new Dimension(0, 40);
092            }
093        };
094        p.setLayout(new BoxLayout(p, BoxLayout.Y_AXIS));
095        p.setVisible(false);
096
097        int dialogIndex = allDialogs.size() - 1;
098        mSpltPane.add(p, 'L'+Integer.toString(dialogIndex));
099        panels.add(p);
100
101        if (dlg.isDialogShowing()) {
102            dlg.showDialog();
103            if (dlg.isDialogInCollapsedView()) {
104                dlg.isCollapsed = false;    // pretend to be in Default view, this will be set back by collapse()
105                dlg.collapse();
106            }
107            if (doReconstruct) {
108                reconstruct(Action.INVISIBLE_TO_DEFAULT, dlg);
109            }
110            dlg.showNotify();
111        } else {
112            dlg.hideDialog();
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            t.destroy();
326        }
327    }
328
329    /**
330     * Replies the instance of a toggle dialog of type <code>type</code> managed by this
331     * map frame
332     *
333     * @param <T> toggle dialog type
334     * @param type the class of the toggle dialog, i.e. UserListDialog.class
335     * @return the instance of a toggle dialog of type <code>type</code> managed by this
336     * map frame; null, if no such dialog exists
337     *
338     */
339    public <T> T getToggleDialog(Class<T> type) {
340        for (ToggleDialog td : allDialogs) {
341            if (type.isInstance(td))
342                return type.cast(td);
343        }
344        return null;
345    }
346}