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