// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.gui.dialogs;
import java.awt.Dimension;
import java.util.ArrayList;
import java.util.List;
import javax.swing.BoxLayout;
import javax.swing.JPanel;
import javax.swing.JSplitPane;
import org.openstreetmap.josm.gui.widgets.MultiSplitLayout.Divider;
import org.openstreetmap.josm.gui.widgets.MultiSplitLayout.Leaf;
import org.openstreetmap.josm.gui.widgets.MultiSplitLayout.Node;
import org.openstreetmap.josm.gui.widgets.MultiSplitLayout.Split;
import org.openstreetmap.josm.gui.widgets.MultiSplitPane;
import org.openstreetmap.josm.tools.CheckParameterUtil;
import org.openstreetmap.josm.tools.Destroyable;
import org.openstreetmap.josm.tools.JosmRuntimeException;
import org.openstreetmap.josm.tools.bugreport.BugReport;
/**
* This is the panel displayed on the right side of JOSM. It displays a list of panels.
*/
public class DialogsPanel extends JPanel implements Destroyable {
private final List<ToggleDialog> allDialogs = new ArrayList<>();
private final MultiSplitPane mSpltPane = new MultiSplitPane();
private static final int DIVIDER_SIZE = 5;
/**
* Panels that are added to the multisplitpane.
*/
private final List<JPanel> panels = new ArrayList<>();
/**
* If {@link #initialize(List)} was called. read only from outside
*/
public boolean initialized;
private final JSplitPane parent;
/**
* Creates a new {@link DialogsPanel}.
* @param parent The parent split pane that allows this panel to change it's size.
*/
public DialogsPanel(JSplitPane parent) {
this.parent = parent;
}
/**
* Initializes this panel
* @param pAllDialogs The list of dialogs this panel should contain on start.
*/
public void initialize(List<ToggleDialog> pAllDialogs) {
if (initialized) {
throw new IllegalStateException("Panel can only be initialized once.");
}
initialized = true;
allDialogs.clear();
for (ToggleDialog dialog: pAllDialogs) {
add(dialog, false);
}
this.add(mSpltPane);
reconstruct(Action.ELEMENT_SHRINKS, null);
}
/**
* Add a new {@link ToggleDialog} to the list of known dialogs and trigger reconstruct.
* @param dlg The dialog to add
*/
public void add(ToggleDialog dlg) {
add(dlg, true);
}
/**
* Add a new {@link ToggleDialog} to the list of known dialogs.
* @param dlg The dialog to add
* @param doReconstruct <code>true</code> if reconstruction should be triggered.
*/
public void add(ToggleDialog dlg, boolean doReconstruct) {
allDialogs.add(dlg);
dlg.setDialogsPanel(this);
dlg.setVisible(false);
final JPanel p = new MinSizePanel();
p.setLayout(new BoxLayout(p, BoxLayout.Y_AXIS));
p.setVisible(false);
int dialogIndex = allDialogs.size() - 1;
mSpltPane.add(p, 'L'+Integer.toString(dialogIndex));
panels.add(p);
if (dlg.isDialogShowing()) {
dlg.showDialog();
if (dlg.isDialogInCollapsedView()) {
dlg.isCollapsed = false; // pretend to be in Default view, this will be set back by collapse()
dlg.collapse();
}
if (doReconstruct) {
reconstruct(Action.INVISIBLE_TO_DEFAULT, dlg);
}
dlg.showNotify();
} else {
dlg.hideDialog();
}
}
static final class MinSizePanel extends JPanel {
@Override
public Dimension getMinimumSize() {
// Honoured by the MultiSplitPaneLayout when the entire Window is resized
return new Dimension(0, 40);
}
}
/**
* What action was performed to trigger the reconstruction
*/
public enum Action {
/**
* The panel was invisible previously
*/
INVISIBLE_TO_DEFAULT,
/**
* The panel was collapsed by the user.
*/
COLLAPSED_TO_DEFAULT,
/* INVISIBLE_TO_COLLAPSED, does not happen */
/**
* else. (Remaining elements have more space.)
*/
ELEMENT_SHRINKS
}
/**
* Reconstruct the view, if the configurations of dialogs has changed.
* @param action what happened, so the reconstruction is necessary
* @param triggeredBy the dialog that caused the reconstruction
*/
public void reconstruct(Action action, ToggleDialog triggeredBy) {
final int n = allDialogs.size();
/**
* reset the panels
*/
for (JPanel p: panels) {
p.removeAll();
p.setVisible(false);
}
/**
* Add the elements to their respective panel.
*
* Each panel contains one dialog in default view and zero or more
* collapsed dialogs on top of it. The last panel is an exception
* as it can have collapsed dialogs at the bottom as well.
* If there are no dialogs in default view, show the collapsed ones
* in the last panel anyway.
*/
JPanel p = panels.get(n-1); // current Panel (start with last one)
int k = -1; // indicates that current Panel index is N-1, but no default-view-Dialog has been added to this Panel yet.
for (int i = n-1; i >= 0; --i) {
final ToggleDialog dlg = allDialogs.get(i);
if (dlg.isDialogInDefaultView()) {
if (k == -1) {
k = n-1;
} else {
--k;
p = panels.get(k);
}
p.add(dlg, 0);
p.setVisible(true);
} else if (dlg.isDialogInCollapsedView()) {
p.add(dlg, 0);
p.setVisible(true);
}
}
if (k == -1) {
k = n-1;
}
final int numPanels = n - k;
/**
* Determine the panel geometry
*/
if (action == Action.ELEMENT_SHRINKS) {
for (int i = 0; i < n; ++i) {
final ToggleDialog dlg = allDialogs.get(i);
if (dlg.isDialogInDefaultView()) {
final int ph = dlg.getPreferredHeight();
final int ah = dlg.getSize().height;
dlg.setPreferredSize(new Dimension(Integer.MAX_VALUE, ah < 20 ? ph : ah));
}
}
} else {
CheckParameterUtil.ensureParameterNotNull(triggeredBy, "triggeredBy");
int sumP = 0; // sum of preferred heights of dialogs in default view (without the triggering dialog)
int sumA = 0; // sum of actual heights of dialogs in default view (without the triggering dialog)
int sumC = 0; // sum of heights of all collapsed dialogs (triggering dialog is never collapsed)
for (ToggleDialog dlg: allDialogs) {
if (dlg.isDialogInDefaultView()) {
if (dlg != triggeredBy) {
sumP += dlg.getPreferredHeight();
sumA += dlg.getHeight();
}
} else if (dlg.isDialogInCollapsedView()) {
sumC += dlg.getHeight();
}
}
/**
* If we add additional dialogs on startup (e.g. geoimage), they may
* not have an actual height yet.
* In this case we simply reset everything to it's preferred size.
*/
if (sumA == 0) {
reconstruct(Action.ELEMENT_SHRINKS, null);
return;
}
/** total Height */
final int h = mSpltPane.getMultiSplitLayout().getModel().getBounds().getSize().height;
/** space, that is available for dialogs in default view (after the reconfiguration) */
final int s2 = h - (numPanels - 1) * DIVIDER_SIZE - sumC;
final int hpTrig = triggeredBy.getPreferredHeight();
if (hpTrig <= 0) throw new IllegalStateException(); // Must be positive
/** The new dialog gets a fair share */
final int hnTrig = hpTrig * s2 / (hpTrig + sumP);
triggeredBy.setPreferredSize(new Dimension(Integer.MAX_VALUE, hnTrig));
/** This is remainig for the other default view dialogs */
final int r = s2 - hnTrig;
/**
* Take space only from dialogs that are relatively large
*/
int dm = 0; // additional space needed by the small dialogs
int dp = 0; // available space from the large dialogs
for (int i = 0; i < n; ++i) {
final ToggleDialog dlg = allDialogs.get(i);
if (dlg != triggeredBy && dlg.isDialogInDefaultView()) {
final int ha = dlg.getSize().height; // current
final int h0 = ha * r / sumA; // proportional shrinking
final int he = dlg.getPreferredHeight() * s2 / (sumP + hpTrig); // fair share
if (h0 < he) { // dialog is relatively small
int hn = Math.min(ha, he); // shrink less, but do not grow
dm += hn - h0;
} else { // dialog is relatively large
dp += h0 - he;
}
}
}
/** adjust, without changing the sum */
for (int i = 0; i < n; ++i) {
final ToggleDialog dlg = allDialogs.get(i);
if (dlg != triggeredBy && dlg.isDialogInDefaultView()) {
final int ha = dlg.getHeight();
final int h0 = ha * r / sumA;
final int he = dlg.getPreferredHeight() * s2 / (sumP + hpTrig);
if (h0 < he) {
int hn = Math.min(ha, he);
dlg.setPreferredSize(new Dimension(Integer.MAX_VALUE, hn));
} else {
int d = dp == 0 ? 0 : ((h0-he) * dm / dp);
dlg.setPreferredSize(new Dimension(Integer.MAX_VALUE, h0 - d));
}
}
}
}
/**
* create Layout
*/
final List<Node> ch = new ArrayList<>();
for (int i = k; i <= n-1; ++i) {
if (i != k) {
ch.add(new Divider());
}
Leaf l = new Leaf('L'+Integer.toString(i));
l.setWeight(1.0 / numPanels);
ch.add(l);
}
if (numPanels == 1) {
Node model = ch.get(0);
mSpltPane.getMultiSplitLayout().setModel(model);
} else {
Split model = new Split();
model.setRowLayout(false);
model.setChildren(ch);
mSpltPane.getMultiSplitLayout().setModel(model);
}
mSpltPane.getMultiSplitLayout().setDividerSize(DIVIDER_SIZE);
mSpltPane.getMultiSplitLayout().setFloatingDividers(true);
mSpltPane.revalidate();
/**
* Hide the Panel, if there is nothing to show
*/
if (numPanels == 1 && panels.get(n-1).getComponents().length == 0) {
parent.setDividerSize(0);
this.setVisible(false);
} else {
if (this.getWidth() != 0) { // only if josm started with hidden panel
this.setPreferredSize(new Dimension(this.getWidth(), 0));
}
this.setVisible(true);
parent.setDividerSize(5);
parent.resetToPreferredSizes();
}
}
@Override
public void destroy() {
for (ToggleDialog t : allDialogs) {
try {
t.destroy();
} catch (JosmRuntimeException | IllegalArgumentException | IllegalStateException e) {
throw BugReport.intercept(e).put("dialog", t).put("dialog-class", t.getClass());
}
}
}
/**
* Replies the instance of a toggle dialog of type <code>type</code> managed by this
* map frame
*
* @param <T> toggle dialog type
* @param type the class of the toggle dialog, i.e. UserListDialog.class
* @return the instance of a toggle dialog of type <code>type</code> managed by this
* map frame; null, if no such dialog exists
*
*/
public <T> T getToggleDialog(Class<T> type) {
for (ToggleDialog td : allDialogs) {
if (type.isInstance(td))
return type.cast(td);
}
return null;
}
}