package org.korsakow.ide.ui.controller;
import java.awt.Cursor;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import javax.swing.AbstractAction;
import javax.swing.JComponent;
import javax.swing.KeyStroke;
import javax.swing.event.MouseInputAdapter;
import javax.swing.event.MouseInputListener;
import javax.xml.parsers.ParserConfigurationException;
import org.korsakow.ide.resources.widget.WidgetComponent;
import org.korsakow.ide.resources.widget.WidgetModel;
import org.korsakow.ide.ui.interfacebuilder.DomChangeListener;
import org.korsakow.ide.ui.interfacebuilder.DomChangeNotifier;
import org.korsakow.ide.ui.interfacebuilder.WidgetCanvas;
import org.korsakow.ide.ui.interfacebuilder.WidgetCanvasModel;
import org.korsakow.ide.ui.interfacebuilder.WidgetCanvasModelAdapter;
import org.korsakow.ide.ui.interfacebuilder.WidgetCanvasModelListener;
import org.korsakow.ide.ui.interfacebuilder.WidgetResizer;
import org.korsakow.ide.ui.interfacebuilder.widget.SnuAutoLink;
import org.korsakow.ide.util.DomUtil;
import org.korsakow.ide.util.UIUtil;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.xml.sax.SAXException;
import com.jroller.santosh.border.DefaultResizableBorder;
public class WidgetCanvasController implements WidgetCanvasModelListener
{
private final WidgetCanvas widgetCanvas;
private final WidgetDomSynchronizer domSynchronizer;
private Document doc;
private final UndoDomListener undoDomListener;
private final DomChangeNotifier domNotifier;
private final WidgetDomParser widgetParser;
private final WidgetSelectionController selectionController;
private final MouseInputListener resizeController;
private boolean isUpdate = false;
public WidgetCanvasController(WidgetCanvas widgetCanvas)
{
this.widgetCanvas = widgetCanvas;
widgetCanvas.getModel().addListener(this);
resizeController = new WidgetResizer(widgetCanvas);
selectionController = new WidgetSelectionController(widgetCanvas, resizeController);
widgetCanvas.addKeyListener(new WidgetKeyboardMover(widgetCanvas));
widgetCanvas.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_Z, UIUtil.getPlatformCommandKeyMask()), "undo");
widgetCanvas.getActionMap().put("undo", new UndoAction());
widgetCanvas.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_BACK_SPACE, 0), "deleteSelected");
widgetCanvas.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0), "deleteSelected");
widgetCanvas.getActionMap().put("deleteSelected", new DeleteSelectedAction());
widgetCanvas.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_A, UIUtil.getPlatformCommandKeyMask()), "selectAll");
widgetCanvas.getActionMap().put("selectAll", new SelectAllAction());
try {
doc = DomUtil.createDocument();
} catch (ParserConfigurationException e) {
throw new RuntimeException(e);
} catch (SAXException e) {
throw new RuntimeException(e);
}
Element root = doc.createElement("Interface");
doc.appendChild(root);
Element widgetsRoot = doc.createElement("widgets");
root.appendChild(widgetsRoot);
domNotifier = new DomChangeNotifier();
undoDomListener = new UndoDomListener(doc, domNotifier);
widgetParser = new WidgetDomParser();
domSynchronizer = new WidgetDomSynchronizer(domNotifier, doc);
}
public void initialState()
{
undoDomListener.setInitialState();
}
public void gridSizeChanged(int oldWidth, int oldHeight, int newWidth, int newHeight)
{
widgetCanvas.setGridSize(newWidth, newHeight);
}
public void movieSizeChanged(int oldWidth, int oldHeight, int newWidth, int newHeight)
{
}
public void widgetAdded(WidgetModel widget) {
domSynchronizer.widgetAdded(widget);
widget.getComponent().removeMouseListener(selectionController);
widget.getComponent().addMouseListener(selectionController);
updateSnuAutoLinks();
}
public void widgetRemoved(WidgetModel widget) {
domSynchronizer.widgetRemoved(widget);
updateSnuAutoLinks();
}
public void widgetsDepthChanged() {
}
public void selectionChanged(Collection<WidgetModel> oldSelection, Collection<WidgetModel> newSelection)
{
for (WidgetComponent comp : widgetCanvas.getWidgetComponents()) {
if (widgetCanvas.getModel().isSelected(comp.getWidget())) {
comp.removeMouseListener(resizeController);
comp.removeMouseMotionListener(resizeController);
comp.addMouseListener(resizeController);
comp.addMouseMotionListener(resizeController);
comp.setBorder(new DefaultResizableBorder(6, comp.isResizable() && newSelection.size()==1));
comp.setCursor(new Cursor(Cursor.MOVE_CURSOR));
} else {
comp.setBorder(widgetCanvas.getDefaultBorder());
comp.removeMouseListener(resizeController);
comp.removeMouseMotionListener(resizeController);
}
}
widgetCanvas.repaint(); // this ensures the apparent z-order doesn't change when setBorder causes a repaint
}
private void updateSnuAutoLinks()
{
int snuAudoLinkIndex = 0;
Collection<WidgetModel> widgets = widgetCanvas.getModel().getWidgets();
for (WidgetModel widget : widgets) {
switch (widget.getWidgetType())
{
case SnuAutoLink:
SnuAutoLink snuAutoLink = (SnuAutoLink)widget;
snuAutoLink.setIndex(snuAudoLinkIndex);
++snuAudoLinkIndex;
break;
}
}
}
private class UndoAction extends AbstractAction
{
public void actionPerformed(ActionEvent e) {
undoDomListener.back();
}
}
private class SelectAllAction extends AbstractAction
{
public void actionPerformed(ActionEvent e) {
widgetCanvas.getSelectionModel().selectAll();
}
}
private class DeleteSelectedAction extends AbstractAction
{
public void actionPerformed(ActionEvent e) {
widgetCanvas.getModel().removeWidgets(widgetCanvas.getSelectionModel().getSelectedWidgets());
}
}
private class UndoDomListener implements DomChangeListener
{
private class HistoryEntry
{
public Node node;
public String changeName;
public boolean coalescable;
public HistoryEntry(Node node, String changeName, boolean coalescable)
{
this.node = node;
this.changeName = changeName;
this.coalescable = coalescable;
}
}
private final List<HistoryEntry> history = new ArrayList<HistoryEntry>();
private final Document doc;
private final DomChangeNotifier notifier;
public UndoDomListener(Document doc, DomChangeNotifier notifier)
{
this.doc = doc;
this.notifier = notifier;
notifier.add(this);
setInitialState();
}
public void setInitialState()
{
history.clear();
history.add(new HistoryEntry(doc.getDocumentElement().cloneNode(true), "initial state", false));
}
public void domDocumentChange(Document doc, String changeName, boolean coalescable)
{
if (isUpdate)
return;
if (changeName == null)
changeName = "[batch]"; // just for debugging clarity
Node copy = doc.getDocumentElement().cloneNode(true);
history.add(new HistoryEntry(copy, changeName, coalescable));
// Logger.getLogger(WidgetCanvasController.class).debug("UndoHistory: " + changeName + "; " + history.size());
// dump();
}
public void back()
{
if (history.isEmpty())
throw new IllegalStateException();
isUpdate = true;
widgetCanvas.getModel().clearWidgets();
doc.removeChild(doc.getDocumentElement());
HistoryEntry entry;
if (history.size() > 1) {
// latest entry always contains current state
history.remove(history.size()-1);
}
entry = history.get(history.size()-1);
doc.appendChild(entry.node.cloneNode(true));
// Logger.getLogger(WidgetCanvasController.class).debug("Undo to " + entry.changeName + "; " + (history.size()));
widgetParser.parseDom(doc);
for (WidgetModel widget : widgetParser.getWidgets()) {
widget.removePropertyChangeListener(domSynchronizer); // in case its already added
widget.addPropertyChangeListener(domSynchronizer);
widgetCanvas.getModel().addWidget(widget);
}
widgetCanvas.repaint();
isUpdate = false;
}
}
private class WidgetDomSynchronizer extends WidgetCanvasModelAdapter implements PropertyChangeListener
{
private final DomChangeNotifier notifier;
private final Document doc;
public WidgetDomSynchronizer(DomChangeNotifier notifier, Document dom)
{
this.notifier = notifier;
doc = dom;
}
public Element getRoot()
{
return DomUtil.findChildByTagName(doc.getDocumentElement(), "widgets");
}
@Override
public void widgetAdded(WidgetModel widget)
{
if (isUpdate)
return;
Element elm = DomUtil.getElementById(doc, ""+widget.getId());
if (elm != null)
throw new IllegalStateException("widget not found: " + widget.getId());
elm = doc.createElement("WidgetModel");
getRoot().appendChild(elm);
elm.setAttribute("id", ""+widget.getId());
elm.setIdAttribute("id", true);
assert DomUtil.getElementById(doc, ""+widget.getId()) != null;
widget.addPropertyChangeListener(this);
updateWidget(elm, widget); // dont update with an incomplete widget
notifier.notifyDomChanged(doc, "widget added", false);
}
@Override
public void widgetRemoved(WidgetModel widget)
{
if (isUpdate)
return;
Element elm = DomUtil.getElementById(doc, ""+widget.getId());
if (elm == null) {
new IllegalStateException().printStackTrace();
throw new IllegalStateException();
}
elm.getParentNode().removeChild(elm);
widget.removePropertyChangeListener(this);
notifier.notifyDomChanged(doc, "widget removed", false);
}
@Override
public void widgetsDepthChanged()
{
if (isUpdate)
return;
notifier.notifyDomChanged(doc, "widget removed", false);
}
public void propertyChange(PropertyChangeEvent event)
{
if (isUpdate)
return;
WidgetModel widget = (WidgetModel)event.getSource();
Element elm = DomUtil.getElementById(doc, ""+widget.getId());
if (elm == null)
throw new IllegalStateException("no element for #" + widget.getId() + " on propertychange for " + event.getPropertyName());
updateWidget(elm, widget);
notifier.notifyDomChanged(doc, event.getPropertyName(), true);
}
public void updateWidget(Element elm, WidgetModel widget)
{
setProperty(elm, "type", widget.getWidgetType().getId());
setProperty(elm, "x", widget.getX());
setProperty(elm, "y", widget.getY());
setProperty(elm, "width", widget.getWidth());
setProperty(elm, "height", widget.getHeight());
for (String propertyId : widget.getDynamicPropertyIds()) {
setProperty(elm, propertyId, widget.getDynamicProperty(propertyId));
}
}
private void setProperty(Element elm, String propertyName, Object propertyValue)
{
// null is represented as not being there. how else can this be done? attribute?
if (propertyValue == null)
return;
Element propertyElement = DomUtil.findChildByTagName(elm, propertyName);
if (propertyElement == null) {
propertyElement = doc.createElement(propertyName);
elm.appendChild(propertyElement);
}
while (propertyElement.hasChildNodes())
propertyElement.removeChild(propertyElement.getFirstChild());
propertyElement.appendChild(doc.createCDATASection(propertyValue!=null?propertyValue.toString():""));
}
}
private static class WidgetSelectionController extends MouseInputAdapter
{
private boolean didDrag = false;
private final WidgetCanvas canvas;
private final MouseInputListener resizeListener;
public WidgetSelectionController(WidgetCanvas canvas, MouseInputListener resizeListener)
{
this.canvas = canvas;
this.resizeListener = resizeListener;
}
@Override
public void mouseDragged(MouseEvent me)
{
if (!didDrag) {
didDrag = true;
canvas.requestFocus();
WidgetComponent widget = (WidgetComponent)me.getComponent();
boolean wasSelected = canvas.getSelectionModel().isSelected(widget.getWidget());
boolean replace = !UIUtil.isPlatformMultipleSectionKeyDown(me);
if (!wasSelected) {
if (replace) {
canvas.getSelectionModel().clearSelection();
canvas.getSelectionModel().addSelected(widget.getWidget());
} else {
canvas.getSelectionModel().addSelected(widget.getWidget());
}
}
if (!wasSelected) {
// TODO: insert comment as to why we do this
resizeListener.mousePressed(me);
}
}
}
@Override
public void mouseReleased(MouseEvent me)
{
me.getComponent().removeMouseMotionListener(this);
if (!didDrag) {
canvas.requestFocus();
WidgetComponent widget = (WidgetComponent)me.getComponent();
toggleSelected(widget, !UIUtil.isPlatformMultipleSectionKeyDown(me));
}
}
@Override
public void mousePressed(MouseEvent me)
{
WidgetComponent widget = (WidgetComponent)me.getComponent();
// if (!getSelectionModel().isSelected(widget.getWidget()))
me.getComponent().addMouseMotionListener(this);
didDrag = false;
}
private void toggleSelected(WidgetComponent widget, boolean replace)
{
if (canvas.getSelectionModel().isSelected(widget.getWidget())) {
if (replace) {
canvas.getSelectionModel().clearSelection();
} else
canvas.getSelectionModel().removeSelected(widget.getWidget());
} else {
if (replace) {
canvas.getSelectionModel().clearSelection();
canvas.getSelectionModel().addSelected(widget.getWidget());
} else {
canvas.getSelectionModel().addSelected(widget.getWidget());
}
}
}
}
private class WidgetKeyboardMover extends KeyAdapter
{
private final WidgetCanvas canvas;
private final WidgetCanvasModel model;
public WidgetKeyboardMover(WidgetCanvas canvas) {
this.canvas = canvas;
model = canvas.getModel();
}
@Override
public void keyPressed(KeyEvent e)
{
switch (e.getKeyCode())
{
case KeyEvent.VK_LEFT:
case KeyEvent.VK_RIGHT:
case KeyEvent.VK_UP:
case KeyEvent.VK_DOWN:
break;
default:
return;
}
Rectangle startBounds = new Rectangle(Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MIN_VALUE);
Collection<WidgetComponent> selected = canvas.getSelectedWidgetComponents();
for (WidgetComponent widget : selected) {
startBounds.x = Math.min(startBounds.x, widget.getX());
startBounds.y = Math.min(startBounds.y, widget.getY());
startBounds.width = Math.max(startBounds.width, widget.getWidth());
startBounds.height = Math.max(startBounds.height, widget.getHeight());
}
Point startPoint = new Point(startBounds.x, startBounds.y);
boolean maintainAspect = true;
int incrementX = e.isShiftDown()?1:10;
int incrementY = e.isShiftDown()?1:10;
boolean snapToGrid = !e.isAltDown();
if (snapToGrid) {
incrementX = model.getGridWidth();
incrementY = model.getGridHeight();
}
int moveX = 0;
int moveY = 0;
switch (e.getKeyCode())
{
case KeyEvent.VK_LEFT:
moveX = -incrementX;
break;
case KeyEvent.VK_RIGHT:
moveX = incrementX;
break;
case KeyEvent.VK_UP:
moveY = -incrementY;
break;
case KeyEvent.VK_DOWN:
moveY = incrementY;
break;
}
if (moveX == 0 && moveY ==0)
return;
Point movePoint = new Point(startBounds.x + moveX, startBounds.y + moveY);
WidgetResizer.Bounds newBounds = WidgetResizer.doResizeOrMove(model, Cursor.MOVE_CURSOR, startPoint, startBounds, movePoint, model.getGridWidth(), model.getGridHeight(), snapToGrid, maintainAspect);
int dx = newBounds.x1 - startBounds.x;
int dy = newBounds.y1 - startBounds.y;
Point p = new Point();
for (WidgetComponent widget : selected) {
p.x = widget.getX();
p.y = widget.getY();
p.x += dx;
p.y += dy;
widget.setLocation(widget.getX()+dx, widget.getY()+dy);
}
}
}
}