package gui;
import javax.swing.DefaultComboBoxModel;
import javax.swing.DefaultListModel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.BoxLayout;
import javax.swing.JScrollPane;
import javax.swing.JList;
import javax.swing.JLabel;
import javax.swing.JButton;
import java.awt.Component;
import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;
import javax.swing.ListSelectionModel;
import javax.swing.JTextField;
import java.awt.Dimension;
import javax.swing.JComboBox;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.Set;
import javax.swing.event.ListSelectionListener;
import javax.swing.event.ListSelectionEvent;
import com.mxgraph.view.mxGraph;
import puzzledice.AreaBlock;
import puzzledice.DoorUnlockBlock;
import puzzledice.PuzzleBlock;
public class AreaEditPanel extends JPanel {
/**
*
*/
private static final long serialVersionUID = 1L;
private final JTextField txtAreaName;
private final JList areaList;
private final static DefaultListModel areaListModel = new DefaultListModel();
private final JButton btnChangeArea;
private final JScrollPane scrollPane;
private final JComboBox addDoorSelect;
private final JComboBox removeDoorSelect;
private final JButton btnDeleteArea;
private static AreaBlock _startArea = null;
private int selectedAreaIndex;
private static int nextAreaIndex = 0;
public static void reset() {
nextAreaIndex = 0;
}
/**
* Create the panel.
*/
public AreaEditPanel() {
setLayout(new BoxLayout(this, BoxLayout.X_AXIS));
JPanel panel_1 = new JPanel();
add(panel_1);
panel_1.setLayout(new BoxLayout(panel_1, BoxLayout.Y_AXIS));
final JButton btnNewArea = new JButton("Add Area");
panel_1.add(btnNewArea);
btnNewArea.setAlignmentX(Component.CENTER_ALIGNMENT);
scrollPane = new JScrollPane();
panel_1.add(scrollPane);
areaList = new JList(areaListModel);
areaList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
scrollPane.setViewportView(areaList);
final JPanel panel = new JPanel();
add(panel);
panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
panel.setVisible(false);
btnDeleteArea = new JButton("Delete Area");
btnDeleteArea.setAlignmentX(Component.CENTER_ALIGNMENT);
panel.add(btnDeleteArea);
final JPanel namePanel = new JPanel();
panel.add(namePanel);
namePanel.setLayout(new BoxLayout(namePanel, BoxLayout.X_AXIS));
JLabel lblAreaName = new JLabel("Area Name:");
namePanel.add(lblAreaName);
txtAreaName = new JTextField();
txtAreaName.setMaximumSize(new Dimension(Integer.MAX_VALUE, txtAreaName.getPreferredSize().height));
namePanel.add(txtAreaName);
txtAreaName.setText("Area name");
txtAreaName.setColumns(10);
btnChangeArea = new JButton("Change");
namePanel.add(btnChangeArea);
btnChangeArea.setVisible(false);
JPanel addDoorPanel = new JPanel();
panel.add(addDoorPanel);
addDoorPanel.setLayout(new BoxLayout(addDoorPanel, BoxLayout.X_AXIS));
JButton btnRemoveDoor = new JButton("Remove Door From:");
JButton btnAddDoor = new JButton("Add Door to: ");
btnAddDoor.setPreferredSize(btnRemoveDoor.getPreferredSize());
addDoorPanel.add(btnAddDoor);
addDoorSelect = new JComboBox();
addDoorSelect.setMaximumSize(new Dimension(Integer.MAX_VALUE, addDoorSelect.getPreferredSize().height));
addDoorPanel.add(addDoorSelect);
JPanel removeDoorPanel = new JPanel();
panel.add(removeDoorPanel);
removeDoorPanel.setLayout(new BoxLayout(removeDoorPanel, BoxLayout.X_AXIS));
removeDoorPanel.add(btnRemoveDoor);
removeDoorSelect = new JComboBox();
removeDoorSelect.setMaximumSize(new Dimension(Integer.MAX_VALUE, removeDoorSelect.getPreferredSize().height));
removeDoorPanel.add(removeDoorSelect);
final JButton btnStartArea = new JButton("Make Start Area");
btnStartArea.setAlignmentX(CENTER_ALIGNMENT);
panel.add(btnStartArea);
// When we press enter in the text area, attempt to change the name of the selected area
txtAreaName.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent arg0) {
maybeChangeAreaName(txtAreaName.getText(), areaList.getSelectedIndex());
}
});
// When we leave the text area, attempt to change the name of the selected area
txtAreaName.addFocusListener(new FocusAdapter() {
@Override
public void focusLost(FocusEvent arg0) {
maybeChangeAreaName(txtAreaName.getText(), areaList.getSelectedIndex());
}
});
// Whenever the text of the area name is changed, make the areaNameChange button visible
txtAreaName.getDocument().addDocumentListener(new DocumentListener() {
@Override
public void changedUpdate(DocumentEvent arg0) {
btnChangeArea.setVisible(true);
}
@Override
public void insertUpdate(DocumentEvent arg0) {
btnChangeArea.setVisible(true);
}
@Override
public void removeUpdate(DocumentEvent arg0) {
btnChangeArea.setVisible(true);
}
});
// When the area name change button is pressed, attempt to change the name of the selected area
btnChangeArea.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent arg0) {
maybeChangeAreaName(txtAreaName.getText(), areaList.getSelectedIndex());
}
});
areaList.addListSelectionListener(new ListSelectionListener() {
public void valueChanged(ListSelectionEvent arg0) {
if(btnChangeArea.isVisible())
maybeChangeAreaName(txtAreaName.getText(), selectedAreaIndex);
selectedAreaIndex = areaList.getSelectedIndex();
AreaBlock currentArea = (AreaBlock)areaList.getSelectedValue();
if(currentArea != null) {
txtAreaName.setText(currentArea.getName());
panel.setVisible(true);
//Change the add door combobox to correspond to our current set of areas (with a few exceptions)
addDoorSelect.setModel(new DefaultComboBoxModel(possibleDoors(currentArea)));
//Change the remove door combobox to correspond to our current set of doors
removeDoorSelect.setModel(new DefaultComboBoxModel(currentArea.getDoorList()));
// set the change area button invisible until we actually change the text
btnChangeArea.setVisible(false);
// See if this is the start area (and whether we need the start area button
if (currentArea.isStartArea()) {
btnStartArea.setVisible(false);
}
else {
btnStartArea.setVisible(true);
}
}
else
panel.setVisible(false);
}
});
btnNewArea.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent arg0) {
if(btnChangeArea.isVisible())
maybeChangeAreaName(txtAreaName.getText(), selectedAreaIndex);
String newAreaName = "Area-" + ++nextAreaIndex;
AreaBlock areaToAdd = new AreaBlock(newAreaName);
if (_startArea == null) {
_startArea = areaToAdd;
areaToAdd.setStartArea(true);
}
// add this area to the graph layout
mxGraph areaGraph = WindowMain.getAreaGraph();
Object parent = areaGraph.getDefaultParent();
areaGraph.getModel().beginUpdate();
try
{
String style = (nextAreaIndex % 2 == 0) ? "fillColor=#8FFEDD" : "fillColor=#BAD0EF";
Object cell = areaGraph.insertVertex(parent, null, areaToAdd, 0, 0, 0, 0, style);
areaToAdd.setGraphCell(cell);
areaGraph.updateCellSize(cell);
}
finally
{
areaGraph.getModel().endUpdate();
// update with an animation
WindowMain.updateAreaGraph();
}
areaListModel.addElement(areaToAdd);
areaList.setSelectedIndex(areaListModel.getSize()-1);
txtAreaName.setText(newAreaName);
panel.setVisible(true);
txtAreaName.selectAll();
txtAreaName.requestFocusInWindow();
// set the change area button invisible until we actually change the text
btnChangeArea.setVisible(false);
// Update the block panel
PuzzleEditPanel.updateCurrentBlock();
buildAreaPuzzleGraph();
}
});
// Add the currently selected area (from the combobox) to the current area (from the list) as a door
btnAddDoor.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent arg0) {
AreaBlock currentArea = (AreaBlock)areaList.getSelectedValue();
AreaBlock doorToAdd = (AreaBlock)addDoorSelect.getSelectedItem();
if(doorToAdd == null)
return;
currentArea.addDoor(doorToAdd);
doorToAdd.addDoor(currentArea);
//add an edge to the graph layout
mxGraph areaGraph = WindowMain.getAreaGraph();
Object parent = areaGraph.getDefaultParent();
areaGraph.getModel().beginUpdate();
try
{
areaGraph.insertEdge(parent, null, null, currentArea.getGraphCell(), doorToAdd.getGraphCell());
areaGraph.insertEdge(parent, null, null, doorToAdd.getGraphCell(), currentArea.getGraphCell());
}
finally
{
areaGraph.getModel().endUpdate();
// update with an animation
WindowMain.updateAreaGraph();
}
//Change the remove door combobox to correspond to our current set of doors
removeDoorSelect.setModel(new DefaultComboBoxModel(currentArea.getDoorList()));
//Change the add door combobox to correspond to our current set of areas (with a few exceptions)
addDoorSelect.setModel(new DefaultComboBoxModel(possibleDoors(currentArea)));
buildAreaPuzzleGraph();
}
});
// Remove the currently selected area (from the combobox) as a door from the current area (from the list)
btnRemoveDoor.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent arg0) {
AreaBlock currentArea = (AreaBlock)areaList.getSelectedValue();
AreaBlock doorToRemove = (AreaBlock)removeDoorSelect.getSelectedItem();
if(doorToRemove == null)
return;
currentArea.removeDoor(doorToRemove);
doorToRemove.removeDoor(currentArea);
mxGraph areaGraph = WindowMain.getAreaGraph();
areaGraph.getModel().beginUpdate();
try
{
Object[] cells = areaGraph.getEdgesBetween(currentArea.getGraphCell(), doorToRemove.getGraphCell(), false);
areaGraph.removeCells(cells);
}
finally
{
areaGraph.getModel().endUpdate();
// update with an animation
WindowMain.updateAreaGraph();
}
//Change the remove door combobox to correspond to our current set of doors
removeDoorSelect.setModel(new DefaultComboBoxModel(currentArea.getDoorList()));
//Change the add door combobox to correspond to our current set of areas (with a few exceptions)
addDoorSelect.setModel(new DefaultComboBoxModel(possibleDoors(currentArea)));
buildAreaPuzzleGraph();
}
});
// The delete function. Slightly more complicated since we have to remove any doors the area might have
btnDeleteArea.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent arg0) {
AreaBlock areaToDelete = (AreaBlock)areaList.getSelectedValue();
// Only delete the area if the user confirms
if (JOptionPane.showConfirmDialog((Component)arg0.getSource(), "Really delete area: " + areaToDelete.getName() + "?", "Confirm Delete", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE) == JOptionPane.YES_OPTION) {
int newIndex = Math.max(0, areaList.getSelectedIndex()-1);
areaListModel.remove(areaList.getSelectedIndex());
// Now go through the areas and remove this any doors involving this area
for(Object o : areaListModel.toArray()) {
AreaBlock a = (AreaBlock)o;
a.removeDoor(areaToDelete);
}
// Remove the puzzle edges related to this area
if (_startArea == areaToDelete) {
_startArea = null;
areaToDelete.setStartArea(false);
}
areaToDelete.removePuzzleEdges();
areaToDelete.maybeDeletePuzzleCell();
// Delete this area from the graph
mxGraph areaGraph = WindowMain.getAreaGraph();
areaGraph.getModel().beginUpdate();
try
{
Object[] cells = new Object[]{areaToDelete.getGraphCell()};
areaGraph.removeCells(cells, true);
}
finally
{
areaGraph.getModel().endUpdate();
// update with an animation
WindowMain.updateAreaGraph();
}
// And go through the list of puzzle blocks and remove all references of this area
for(PuzzleBlock p : PuzzleEditPanel.getBlockList())
{
p.maybeRemoveRef(areaToDelete);
}
// If we can, want to select a new area in the list
if(areaListModel.size() >= 1)
areaList.setSelectedIndex(newIndex);
buildAreaPuzzleGraph();
}
}
});
btnStartArea.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent evt) {
if (_startArea != null) {
_startArea.setStartArea(false);
}
AreaBlock currentArea = (AreaBlock)areaList.getSelectedValue();
currentArea.setStartArea(true);
mxGraph areaGraph = WindowMain.getAreaGraph();
areaGraph.getModel().beginUpdate();
try {
areaGraph.updateCellSize(currentArea.getGraphCell());
if (_startArea != null)
areaGraph.updateCellSize(_startArea.getGraphCell());
}
finally {
areaGraph.getModel().endUpdate();
WindowMain.updateAreaGraph();
}
_startArea = currentArea;
areaList.repaint();
btnStartArea.setVisible(false);
buildAreaPuzzleGraph();
}
});
}
private void maybeChangeAreaName(String newAreaName, int selectedIndex)
{
AreaBlock currentArea = (AreaBlock)areaListModel.get(selectedIndex);
if(newAreaName.equals(currentArea.getName()))
return;
currentArea.setName(newAreaName);
areaListModel.set(selectedIndex, currentArea);
btnChangeArea.setVisible(false);
mxGraph areaGraph = WindowMain.getAreaGraph();
areaGraph.getModel().beginUpdate();
try
{
areaGraph.updateCellSize(currentArea.getGraphCell());
}
finally
{
areaGraph.getModel().endUpdate();
}
}
//Returns a list of the doors that can be placed in the addDoorSelect combobox when a new area is selected.
//This list should include all the current areas without the current area and without any of the current area's doors
private Object[] possibleDoors(AreaBlock currentBlock)
{
if(areaListModel.isEmpty())
return new Object[0];
//Start with a list and convert it to an array
List<AreaBlock> doors = new ArrayList<AreaBlock>();
for(Object o : areaListModel.toArray()) {
AreaBlock a = (AreaBlock)o;
if (!a.equals(currentBlock) && !currentBlock.hasDoor(a))
doors.add(a);
}
return doors.toArray();
}
// A Function to reset the visuals of the area edit panel on a load
public void justLoaded()
{
if (areaList.getModel().getSize() > 0)
areaList.setSelectedIndex(0);
WindowMain.updateAreaGraph();
}
// This function provides a way for other components to access the current list of areas
public static AreaBlock[] getAreaList()
{
AreaBlock[] retArray = new AreaBlock[areaListModel.size()];
areaListModel.copyInto(retArray);
return retArray;
}
public void clear()
{
areaListModel.clear();
areaList.setSelectedValue(null, true);
}
// A function to add areas to the list
public static void addArea(AreaBlock areaToAdd)
{
mxGraph areaGraph = WindowMain.getAreaGraph();
Object parent = areaGraph.getDefaultParent();
if (areaToAdd.isStartArea())
_startArea = areaToAdd;
areaGraph.getModel().beginUpdate();
try
{
String style = (nextAreaIndex % 2 == 0) ? "fillColor=#8FFEDD" : "fillColor=#BAD0EF";
Object cell = areaGraph.insertVertex(parent, null, areaToAdd, 0, 0, 0, 0, style);
areaToAdd.setGraphCell(cell);
areaGraph.updateCellSize(cell);
}
finally
{
areaGraph.getModel().endUpdate();
}
areaListModel.addElement(areaToAdd);
nextAreaIndex++;
}
public static void addDoors(AreaBlock area) {
mxGraph areaGraph = WindowMain.getAreaGraph();
Object parent = areaGraph.getDefaultParent();
areaGraph.getModel().beginUpdate();
try
{
for (AreaBlock door : area.getDoorList()) {
areaGraph.insertEdge(parent, null, null, area.getGraphCell(), door.getGraphCell());
}
}
finally
{
areaGraph.getModel().endUpdate();
}
}
// For creating the acyclic puzzle graph representation of areas
public static void buildAreaPuzzleGraph() {
AreaBlock[] areaList = getAreaList();
// First, remove any edge connections so we can start anew
for (AreaBlock area : areaList) {
area.removePuzzleEdges();
}
Set<AreaBlock> closedBlocks = new HashSet<AreaBlock>();
Queue<AreaBlock> agenda = new LinkedList<AreaBlock>();
if (_startArea != null) {
agenda.add(_startArea);
while (agenda.size() > 0) {
AreaBlock block = agenda.remove();
if (closedBlocks.contains(block))
continue;
closedBlocks.add(block);
// Create an edge to all of this blocks neighbors if they're not in the closed set
for (AreaBlock neighbor : block.getDoorList()) {
if (!closedBlocks.contains(neighbor) && !agenda.contains(neighbor)) {
block.addPuzzleEdge(neighbor);
agenda.add(neighbor);
}
}
// add any destination locked neighbors to the agenda
for (DoorUnlockBlock lockSource : block.getSourceLockList()) {
AreaBlock destBlock = lockSource.getDestBlock();
if (destBlock != null && !closedBlocks.contains(destBlock) && !agenda.contains(destBlock)) {
agenda.add(destBlock);
}
}
}
}
// Now treat each lock source that didn't make it as a potential new start area
for (AreaBlock area : areaList) {
if (!closedBlocks.contains(area) && area.getSourceLockList().length > 0) {
agenda.add(area);
while (agenda.size() > 0) {
AreaBlock block = agenda.remove();
if (closedBlocks.contains(block))
continue;
closedBlocks.add(block);
// Create an edge to all of this blocks neighbors if they're not in the closed set
for (AreaBlock neighbor : block.getDoorList()) {
if (!closedBlocks.contains(neighbor) && !agenda.contains(neighbor)) {
block.addPuzzleEdge(neighbor);
agenda.add(neighbor);
}
}
// add any destination locked neighbors to the agenda
for (DoorUnlockBlock lockSource : block.getSourceLockList()) {
AreaBlock destBlock = lockSource.getDestBlock();
if (destBlock != null && !closedBlocks.contains(destBlock) && !agenda.contains(destBlock)) {
agenda.add(destBlock);
}
}
}
}
}
// Now find all locked doors and check if they created a cycle in the graph.
for (PuzzleBlock block : PuzzleEditPanel.getBlockList()) {
if (block instanceof DoorUnlockBlock) {
DoorUnlockBlock lock = (DoorUnlockBlock)block;
AreaBlock source = lock.getSourceBlock();
AreaBlock dest = lock.getDestBlock();
if (source != null && dest != null && dest.nodeReachable(source)) {
lock.disconnectSource();
}
}
}
// Garbage collection on puzzle graph cells that are no longer needed
for (AreaBlock area : areaList) {
area.maybeDeletePuzzleCell();
}
// At the very end, update the puzzle graph finally
WindowMain.updatePuzzleGraph();
PuzzleEditPanel.resetTextualDescription();
}
}