package org.openslx.dozmod.gui.control;
import java.awt.Component;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import javax.swing.Box;
import javax.swing.ButtonGroup;
import javax.swing.DefaultListCellRenderer;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JRadioButton;
import javax.swing.JScrollPane;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreeNode;
import javax.swing.tree.TreePath;
import org.apache.log4j.Logger;
import org.openslx.bwlp.thrift.iface.Location;
import org.openslx.dozmod.gui.Gui;
import org.openslx.dozmod.gui.changemonitor.AbstractControlWrapper;
import org.openslx.dozmod.gui.changemonitor.DialogChangeMonitor;
import org.openslx.dozmod.gui.control.JCheckBoxTree.CheckChangeEventListener;
import org.openslx.dozmod.gui.helper.GridManager;
import org.openslx.dozmod.thrift.Session;
import org.openslx.dozmod.thrift.cache.MetaDataCache;
import org.openslx.thrifthelper.Comparators;
import org.openslx.util.QuickTimer;
import org.openslx.util.QuickTimer.Task;
/**
*
* Widget to select the locations of a lecture. The UI is simply two JLists and
* a button panel to move the elements from one list to the other. Use the
* getters to read the values of the lists/checkbox: get
*
* @author Jonathan Bauer
*
*/
@SuppressWarnings("serial")
public class LocationSelector extends JPanel {
private final static Logger LOGGER = Logger.getLogger(LocationSelector.class);
private final JRadioButton btnLimitToLocations;
private final JRadioButton btnPrioritizeInLocations;
private final ButtonGroup grpLocationExclusive;
/**
* Flag for the initialization state
*/
private boolean initDone = false;
/**
* List of IDs of locations to set the selection to when we finished
* initializing
*/
private List<Integer> preselection = null;
private JCheckBoxTree locationTree = new JCheckBoxTree();
private HashMap<Integer, DefaultMutableTreeNode> locationNodesMap = new HashMap<Integer, DefaultMutableTreeNode>();
private AbstractControlWrapper<?> treeChangeHandler = null;
/**
* Constructor. Initializes the grid layout, the location lists and
* initializes the data
*/
public LocationSelector() {
btnLimitToLocations = new JRadioButton(
"Veranstaltung ausschließlich in den ausgewählten Räumen anzeigen");
btnPrioritizeInLocations = new JRadioButton(
"Veranstaltung mit höherer Priorität in den ausgewählten Räumen anzeigen");
btnPrioritizeInLocations.setSelected(true);
grpLocationExclusive = new ButtonGroup();
grpLocationExclusive.add(btnLimitToLocations);
grpLocationExclusive.add(btnPrioritizeInLocations);
// build the grid
GridManager grid = new GridManager(this, 1);
grid.add(new JScrollPane(locationTree)).fill(true, true).expand(true, true);
grid.nextRow();
grid.add(btnLimitToLocations);
grid.nextRow();
grid.add(btnPrioritizeInLocations);
grid.nextRow();
grid.add(Box.createVerticalGlue());
grid.nextRow();
grid.finish(false);
// initialise the data
init();
}
/**
* Async fetching of the list of the locations from the server
*/
private void init() {
QuickTimer.scheduleOnce(new Task() {
@Override
public void fire() {
final List<Location> locsList = MetaDataCache.getLocations();
if (locsList == null)
return;
Gui.asyncExec(new Runnable() {
@Override
public void run() {
fillLocationsList(locsList);
initDone = true;
// check if preselection was set before we were done
// initialising
if (preselection != null) {
setSelectionInternal(preselection);
preselection = null;
if (treeChangeHandler != null) {
treeChangeHandler.reset();
}
}
}
});
}
});
}
/**
* Sets the list of available locations. Should be called with the list of
* locations given by the server. This function will build the tree
* represented by the 'locationid'/'parentlocationid' fields of locations
* and set the generated tree as model for the JCheckboxTree used in the UI
* of this widget.
*
* @param locations list of locations to set the available list to
*/
private void fillLocationsList(final List<Location> locations) {
DefaultMutableTreeNode rootNode = new DefaultMutableTreeNode(Session.getSatelliteAddress());
DefaultTreeModel treeModel = new DefaultTreeModel(rootNode, true);
// build map containing the node object for each location
for (Location loc : locations) {
if (loc == null)
continue;
locationNodesMap.put(loc.getLocationId(), new DefaultMutableTreeNode(loc, true));
}
// now go over nodes and insert em
for (Integer id : locationNodesMap.keySet()) {
Location location = MetaDataCache.getLocationById(id);
if (location == null)
continue;
// determine which node this is a child of
DefaultMutableTreeNode parentNode = null;
if (location.getParentLocationId() == 0)
parentNode = rootNode;
else
parentNode = locationNodesMap.get(location.getParentLocationId());
// determine the right index to insert the child into
int childrenCount = parentNode.getChildCount();
int insertionIndex = childrenCount;
if (childrenCount != 0) {
// loop through kids
Enumeration<?> enumeration = parentNode.children();
while (enumeration.hasMoreElements()) {
DefaultMutableTreeNode currentChild = (DefaultMutableTreeNode) enumeration.nextElement();
if (currentChild == null)
continue;
Location childLocation = (Location) currentChild.getUserObject();
if (childLocation == null)
continue;
if (Comparators.location.compare(location, childLocation) <= 0) {
insertionIndex = parentNode.getIndex(currentChild);
break;
}
}
}
// insert the current node in the tree model
treeModel.insertNodeInto(locationNodesMap.get(location.getLocationId()), // what
parentNode, // parent?
insertionIndex); // index
}
locationTree.setModel(treeModel);
locationTree.updateUI();
locationTree.repaint();
}
/**
* Setter for the "show only in selection" checkbox
*
* @param limited true to enable the "limited" radio button, false to enable
* the other
*/
public void setOnlyInSelection(boolean limited) {
btnLimitToLocations.setSelected(limited);
btnPrioritizeInLocations.setSelected(!limited);
}
/**
* Getter for the "show only in selection" checkbox
*
* @return true if checked, false otherwise
*/
public boolean getOnlyInSelection() {
return btnLimitToLocations.isSelected();
}
/**
* Sets the selection to the locations corresponding to the given list of
* ids.
*
* @param list location ids to select
*/
public void setSelectedLocationsAsIds(List<Integer> list) {
if (list == null) {
LOGGER.error("No list given!");
return;
}
if (initDone) {
// if we are already initialised, we need to explicitly set the selection
setSelectionInternal(list);
} else {
if (treeChangeHandler != null) {
treeChangeHandler.mute();
}
preselection = list;
}
}
/**
* Gets the selected locations as a list of id's
*
* @return list of ids of the selection as Integer, empty list if the
* selection is empty
*/
public List<Integer> getSelectedLocationsAsIds() {
TreePath[] paths = locationTree.getCheckedPaths();
if (paths == null || paths.length == 0) {
return new ArrayList<Integer>();
}
// first try to reduce the amount of locations pushed to the server
// by removing those which are implicitly selected:
// all children of a node in the selection => keep only parent
// transform the array of paths to a list of leaf nodes
List<TreePath> leavesPathsList = new ArrayList<TreePath>();
for (TreePath path : paths) {
DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent();
if (node != null && node.isLeaf()) {
leavesPathsList.add(path);
}
}
List<TreePath> currentList = leavesPathsList;
List<TreePath> tmp;
do {
tmp = currentList;
currentList = minify(currentList);
} while (!tmp.equals(currentList));
// now get the locationId of the nodes in the TreePath list
List<Integer> idList = new ArrayList<Integer>(currentList.size());
// iterate over the paths
for (TreePath path : currentList) {
if (path == null)
continue;
DefaultMutableTreeNode currentNode = (DefaultMutableTreeNode) path.getLastPathComponent();
if (currentNode == null)
continue;
Object currentDataObject = currentNode.getUserObject();
if (currentDataObject instanceof Location) {
Location currentLocation = (Location) currentDataObject;
idList.add(currentLocation.getLocationId());
}
}
Collections.sort(idList);
return idList;
}
/**
* Minimize the given set of TreePath: if all children of an inner node are
* selected, remove all child from the result list and just keep the inner
* node
*
* @param leavesPathsList as a List of TreePath to minimize
* @return Resulting minimal list of TreePaths
*/
private List<TreePath> minify(final List<TreePath> leavesPathsList) {
List<TreePath> resultList = new ArrayList<TreePath>();
for (TreePath leaf : leavesPathsList) {
DefaultMutableTreeNode leafNode = (DefaultMutableTreeNode) leaf.getLastPathComponent();
if (leafNode.getParent() == null)
continue;
if (leafNode.getLevel() == 1) {
resultList.add(getPath(leafNode));
continue;
}
Enumeration<?> leafSiblings = leafNode.getParent().children();
List<TreePath> selectedSiblings = new ArrayList<TreePath>();
while (leafSiblings.hasMoreElements()) {
DefaultMutableTreeNode leafSibling = (DefaultMutableTreeNode) leafSiblings.nextElement();
if (leavesPathsList.contains(getPath(leafSibling))) {
selectedSiblings.add(getPath(leafSibling));
}
}
if (selectedSiblings.size() == leafNode.getParent().getChildCount()) {
// all selected, keep parent
if (!resultList.contains(getPath(leafNode.getParent())))
resultList.add(getPath(leafNode.getParent()));
} else {
for (TreePath sibling : selectedSiblings) {
if (!resultList.contains(sibling))
resultList.add(sibling);
}
}
}
return resultList;
}
/**
* Helper to get the TreePath of the given TreeNode
*
* @param treeNode node to get the TreePath of
* @return TreePath of the given TreeNode if it can be determined, null
* otherwise
*/
public static TreePath getPath(TreeNode treeNode) {
List<Object> nodes = new ArrayList<Object>();
if (treeNode != null) {
nodes.add(treeNode);
treeNode = treeNode.getParent();
while (treeNode != null) {
nodes.add(0, treeNode);
treeNode = treeNode.getParent();
}
}
return nodes.isEmpty() ? null : new TreePath(nodes.toArray());
}
/**
* Internal helper to set the selection of the list of location
* corresponding the the given list of ids
*
* @param list ids to set the selected locations to
*/
private void setSelectionInternal(List<Integer> list) {
if (list == null || list.isEmpty()) {
return;
}
// first, deselect everything
List<TreePath> selectedPathsList = new ArrayList<>(Math.max(MetaDataCache.getLocations().size(),
list.size()));
for (Location location : MetaDataCache.getLocations()) {
DefaultMutableTreeNode current = locationNodesMap.get(location.locationId);
if (current == null)
continue;
TreePath path = new TreePath(current.getPath());
selectedPathsList.add(path);
}
locationTree.setCheckedState(selectedPathsList, false);
// get the paths in the tree of the given id list of nodes
selectedPathsList.clear();
for (Integer id : list) {
DefaultMutableTreeNode current = locationNodesMap.get(id);
if (current == null)
continue;
TreePath path = new TreePath(current.getPath());
selectedPathsList.add(path);
}
locationTree.setCheckedState(selectedPathsList, true);
locationTree.repaint();
}
public void addToChangeMonitor(DialogChangeMonitor changeMonitor) {
treeChangeHandler = changeMonitor.add(locationTree);
changeMonitor.add(grpLocationExclusive);
}
public void addCheckChangeEventListener(CheckChangeEventListener checkChangeEventListener) {
locationTree.addCheckChangeEventListener(checkChangeEventListener);
}
}
@SuppressWarnings("serial")
class LocationRenderer extends DefaultListCellRenderer {
@Override
public Component getListCellRendererComponent(JList<? extends Object> list, Object value, int index,
boolean isSelected, boolean cellHasFocus) {
if (value instanceof Location) {
return super.getListCellRendererComponent(list, ((Location) value).getLocationName(), index,
isSelected, cellHasFocus);
}
return super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
}
}