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