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 preselection = null; private JCheckBoxTree locationTree = new JCheckBoxTree(); private HashMap locationNodesMap = new HashMap(); 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 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 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 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 getSelectedLocationsAsIds() { TreePath[] paths = locationTree.getCheckedPaths(); if (paths == null || paths.length == 0) { return new ArrayList(); } // 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 leavesPathsList = new ArrayList(); for (TreePath path : paths) { DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent(); if (node != null && node.isLeaf()) { leavesPathsList.add(path); } } List currentList = leavesPathsList; List tmp; do { tmp = currentList; currentList = minify(currentList); } while (!tmp.equals(currentList)); // now get the locationId of the nodes in the TreePath list List idList = new ArrayList(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 minify(final List leavesPathsList) { List resultList = new ArrayList(); 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 selectedSiblings = new ArrayList(); 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 nodes = new ArrayList(); 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 list) { if (list == null || list.isEmpty()) { return; } // first, deselect everything List 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 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); } }