/* * Copyright (C) 2007, 2008, 2010 IsmAvatar <IsmAvatar@gmail.com> * Copyright (C) 2007, 2008 Clam <clamisgood@gmail.com> * Copyright (C) 2007, 2008 Quadduc <quadduc@gmail.com> * * This file is part of LateralGM. * LateralGM is free software and comes with ABSOLUTELY NO WARRANTY. * See LICENSE for details. */ package org.lateralgm.components; import static org.lateralgm.main.Util.deRef; import java.awt.Component; import java.awt.datatransfer.DataFlavor; import java.awt.datatransfer.Transferable; import java.awt.datatransfer.UnsupportedFlavorException; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.beans.PropertyVetoException; import java.lang.ref.SoftReference; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.EmptyStackException; import java.util.List; import java.util.Stack; import java.util.WeakHashMap; import javax.swing.AbstractListModel; import javax.swing.BorderFactory; import javax.swing.DropMode; import javax.swing.ImageIcon; import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.JList; import javax.swing.ListCellRenderer; import javax.swing.ListModel; import javax.swing.TransferHandler; import javax.swing.border.EmptyBorder; import org.lateralgm.components.ActionListEditor.LibActionButton; import org.lateralgm.components.mdi.MDIFrame; import org.lateralgm.main.LGM; import org.lateralgm.main.Prefs; import org.lateralgm.main.UpdateSource.UpdateEvent; import org.lateralgm.main.UpdateSource.UpdateListener; import org.lateralgm.messages.Messages; import org.lateralgm.resources.GmObject; import org.lateralgm.resources.ResourceReference; import org.lateralgm.resources.library.LibAction; import org.lateralgm.resources.library.LibManager; import org.lateralgm.resources.sub.Action; import org.lateralgm.resources.sub.ActionContainer; import org.lateralgm.resources.sub.Argument; import org.lateralgm.subframes.ActionFrame; public class ActionList extends JList { private static final long serialVersionUID = 1L; private static final WeakHashMap<Action,WeakReference<ActionFrame>> FRAMES; private static final ActionListKeyListener ALKL = new ActionListKeyListener(); protected ActionContainer actionContainer; private ActionListModel model; private final ActionRenderer renderer = new ActionRenderer(); public final WeakReference<MDIFrame> parent; private final ActionListMouseListener alml; static { FRAMES = new WeakHashMap<Action,WeakReference<ActionFrame>>(); } public ActionList(MDIFrame parent) { this.parent = new WeakReference<MDIFrame>(parent); setActionContainer(null); setBorder(BorderFactory.createEmptyBorder(0,0,24,0)); if (LGM.javaVersion >= 10600) { setTransferHandler(new ActionTransferHandler(this.parent)); setDragEnabled(true); setDropMode(DropMode.ON_OR_INSERT); } alml = new ActionListMouseListener(this.parent); addMouseListener(alml); addKeyListener(ALKL); setCellRenderer(renderer); } public void setActionContainer(ActionContainer ac) { save(); actionContainer = ac; model = new ActionListModel(); model.renderer = renderer; setModel(model); if (ac == null) return; model.addAll(0,ac.actions); } public ActionContainer getActionContainer() { return actionContainer; } public void save() { if (actionContainer == null) return; for (WeakReference<ActionFrame> a : FRAMES.values()) if (a != null && a.get() != null) a.get().commitChanges(); actionContainer.actions = model.list; } /** * Opens an ActionFrame representing a given action. * Actions like "else" etc. will not have a frame opened. * @param a The action to open a frame for * @return The frame opened or <code>null</code> if no * frame was opened. */ public static MDIFrame openActionFrame(MDIFrame parent, Action a) { LibAction la = a.getLibAction(); if ((la.libArguments == null || la.libArguments.length == 0) && !la.canApplyTo && !la.allowRelative && !la.question) return null; WeakReference<ActionFrame> fr = FRAMES.get(a); ActionFrame af = fr == null ? null : fr.get(); if (af == null || af.isClosed()) { af = new ActionFrame(a); LGM.mdi.add(af); if (parent != null) LGM.mdi.addZChild(parent,af); FRAMES.put(a,new WeakReference<ActionFrame>(af)); } af.setVisible(true); //FIXME: Find out why parent is sent to back. This is a workaround. if (parent != null) parent.toFront(); af.toFront(); try { af.setIcon(false); af.setSelected(true); } catch (PropertyVetoException pve) { } return af; } private static class ActionListMouseListener extends MouseAdapter { public final WeakReference<MDIFrame> parent; public ActionListMouseListener(WeakReference<MDIFrame> parent) { super(); this.parent = parent; } public void mouseClicked(MouseEvent e) { if (e.getClickCount() != 2 || !(e.getSource() instanceof JList)) return; JList l = (JList) e.getSource(); Object o = l.getSelectedValue(); if (o == null && l.getModel().getSize() == 0) { o = new Action(LibManager.codeAction); ((ActionListModel) l.getModel()).add((Action) o); l.setSelectedValue(o,true); } if (o == null || !(o instanceof Action)) return; openActionFrame(parent.get(),(Action) o); } } private static class ActionListKeyListener extends KeyAdapter { public ActionListKeyListener() { super(); } @Override public void keyPressed(KeyEvent e) { JList l = (JList) e.getSource(); switch (e.getKeyCode()) { case KeyEvent.VK_DELETE: int[] indices = l.getSelectedIndices(); ActionListModel alm = (ActionListModel) l.getModel(); for (int i = indices.length - 1; i >= 0; i--) alm.remove(indices[i]); if (indices.length != 0) l.setSelectedIndex(Math.min(alm.getSize() - 1,indices[0])); e.consume(); break; } } } public static class ActionListModel extends AbstractListModel implements UpdateListener { private static final long serialVersionUID = 1L; protected ArrayList<Action> list; protected ArrayList<Integer> indents; private ActionRenderer renderer; public ActionListModel() { list = new ArrayList<Action>(); indents = new ArrayList<Integer>(); } public void add(Action a) { add(getSize(),a); } public void add(int index, Action a) { a.updateSource.addListener(this); list.add(index,a); updateIndentation(); fireIntervalAdded(this,index,index); } public void addAll(int index, Collection<? extends Action> c) { int s = c.size(); if (s <= 0) return; for (Action a : c) { a.updateSource.addListener(this); } list.addAll(index,c); updateIndentation(); fireIntervalAdded(this,index,index + s - 1); } public void remove(int index) { list.remove(index).updateSource.removeListener(this); updateIndentation(); fireIntervalRemoved(this,index,index); } public Object getElementAt(int index) { return list.get(index); } public int getSize() { return list.size(); } private void updateIndentation() { int lms = list.size(); indents.clear(); indents.ensureCapacity(lms); Stack<Integer> levelIndents = new Stack<Integer>(); Stack<Stack<Integer>> questions = new Stack<Stack<Integer>>(); levelIndents.push(0); questions.push(new Stack<Integer>()); int nextIndent = 0; for (int i = 0; i < lms; i++) { Action a = list.get(i); LibAction la = a.getLibAction(); int indent = nextIndent; switch (la.actionKind) { case Action.ACT_BEGIN: levelIndents.push(indent); questions.push(new Stack<Integer>()); break; case Action.ACT_END: indent = levelIndents.peek(); if (levelIndents.size() > 1) { levelIndents.pop(); questions.pop(); } nextIndent = levelIndents.peek(); break; case Action.ACT_ELSE: try { int j = questions.peek().pop(); if (j >= 0) indent = indents.get(j); } catch (EmptyStackException e) { } nextIndent = indent + 1; break; case Action.ACT_REPEAT: nextIndent++; break; case Action.ACT_EXIT: nextIndent = levelIndents.peek(); break; default: if (la.question) { questions.peek().push(i); nextIndent++; } else if (la.execType != Action.EXEC_NONE) nextIndent = levelIndents.peek(); } indents.add(indent); } } public void updated(UpdateEvent e) { if (renderer != null) renderer.clearCache(); } } public static final DataFlavor ACTION_FLAVOR = new DataFlavor(Action.class,"Action"); //$NON-NLS-1$ public static final DataFlavor ACTION_ARRAY_FLAVOR = new DataFlavor(List.class,"Action array"); //$NON-NLS-1$ public static final DataFlavor LIB_ACTION_FLAVOR = new DataFlavor(LibAction.class, "Library action"); //$NON-NLS-1$ public static class LibActionTransferable implements Transferable { private static final DataFlavor[] FLAVORS = { LIB_ACTION_FLAVOR }; private final LibAction libAction; public LibActionTransferable(LibAction la) { libAction = la; } public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException { if (flavor == LIB_ACTION_FLAVOR) { return libAction; } throw new UnsupportedFlavorException(flavor); } public DataFlavor[] getTransferDataFlavors() { return FLAVORS; } public boolean isDataFlavorSupported(DataFlavor flavor) { return flavor == LIB_ACTION_FLAVOR; } } public static class LibActionTransferHandler extends TransferHandler { private static final long serialVersionUID = 1L; public boolean canImport(TransferHandler.TransferSupport info) { return false; } public boolean importData(TransferHandler.TransferSupport info) { return false; } public int getSourceActions(JComponent c) { return COPY; } protected Transferable createTransferable(JComponent c) { LibActionButton lab = (LibActionButton) c; LibAction la = lab.getLibAction(); return new LibActionTransferable(la); } } public static class ActionTransferable implements Transferable { private final Action[] actions; private final DataFlavor[] flavors; public ActionTransferable(Action[] a) { actions = a; ArrayList<DataFlavor> fl = new ArrayList<DataFlavor>(2); fl.add(ACTION_ARRAY_FLAVOR); if (a.length == 1) fl.add(ACTION_FLAVOR); flavors = fl.toArray(new DataFlavor[2]); } public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException { if (flavor == ACTION_FLAVOR && actions.length == 1) { return actions[0]; } if (flavor == ACTION_ARRAY_FLAVOR) { List<Action> l = Arrays.asList(actions); return l; } throw new UnsupportedFlavorException(flavor); } public DataFlavor[] getTransferDataFlavors() { return flavors; } public boolean isDataFlavorSupported(DataFlavor flavor) { for (DataFlavor f : flavors) { if (f == flavor) return true; } return false; } } public static class ActionTransferHandler extends TransferHandler { private static final long serialVersionUID = 1L; private int[] indices = null; //Location of dragged items (to be deleted) private int addIndex = -1; //Location where items were added private int addCount = 0; //Number of items added. private final WeakReference<MDIFrame> parent; public ActionTransferHandler(WeakReference<MDIFrame> parent) { super(); this.parent = parent; } @Override protected void exportDone(JComponent source, Transferable data, int action) { if (action == MOVE && indices != null) { JList ls = (JList) source; ActionListModel model = (ActionListModel) ls.getModel(); if (addCount > 0) { for (int i = 0; i < indices.length; i++) { if (indices[i] > addIndex) { indices[i] += addCount; } } } for (int i = indices.length - 1; i >= 0; i--) { model.remove(indices[i]); } } indices = null; addCount = 0; addIndex = -1; } public boolean canImport(TransferHandler.TransferSupport info) { DataFlavor[] f = info.getDataFlavors(); boolean supported = false; for (DataFlavor flav : f) { if (flav == ACTION_FLAVOR || flav == ACTION_ARRAY_FLAVOR || flav == LIB_ACTION_FLAVOR) supported = true; } if (!supported) return false; ActionList list = (ActionList) info.getComponent(); if (list.actionContainer == null) return false; if (info.isDrop() && ((JList.DropLocation) info.getDropLocation()).getIndex() == -1) return false; return true; } public boolean importData(TransferHandler.TransferSupport info) { if (!canImport(info)) return false; ActionList list = (ActionList) info.getComponent(); ActionListModel alm = (ActionListModel) list.getModel(); Transferable t = info.getTransferable(); int index = alm.list.size(); if (info.isDrop()) index = ((JList.DropLocation) info.getDropLocation()).getIndex(); if (info.isDataFlavorSupported(ACTION_FLAVOR)) { Action a; try { a = (Action) t.getTransferData(ACTION_FLAVOR); } catch (Exception e) { return false; } //clone properly for drag-copy or clipboard paste if (!info.isDrop() || info.getDropAction() == COPY) a = a.copy(); //now add addIndex = index; addCount = 1; alm.add(index,a); list.setSelectedIndex(index); return true; } if (info.isDataFlavorSupported(ACTION_ARRAY_FLAVOR)) { Action[] a; try { a = ((List<?>) t.getTransferData(ACTION_ARRAY_FLAVOR)).toArray(new Action[0]); } catch (Exception e) { e.printStackTrace(); return false; } //clone properly for drag-copy or clipboard paste if (!info.isDrop() || info.getDropAction() == COPY) for (int i = 0; i < a.length; i++) a[i] = a[i].copy(); //now add addIndex = index; addCount = a.length; alm.addAll(index,Arrays.asList(a)); list.setSelectionInterval(index,index + a.length - 1); return true; } if (info.isDataFlavorSupported(LIB_ACTION_FLAVOR)) { LibAction la; Action a; try { la = (LibAction) t.getTransferData(LIB_ACTION_FLAVOR); a = new Action(la); ActionList.openActionFrame(parent.get(),a); } catch (Exception e) { return false; } addIndex = index; addCount = 1; alm.add(index,a); list.setSelectedIndex(index); return true; } return false; } public int getSourceActions(JComponent c) { return COPY_OR_MOVE; } protected Transferable createTransferable(JComponent c) { JList list = (JList) c; indices = list.getSelectedIndices(); Object[] o = list.getSelectedValues(); Action[] a = new Action[o.length]; a = Arrays.asList(o).toArray(a); return new ActionTransferable(a); } } private static class ActionRenderer implements ListCellRenderer { private final WeakHashMap<Action,SoftReference<ActionRendererComponent>> lcrMap; public ActionRenderer() { super(); lcrMap = new WeakHashMap<Action,SoftReference<ActionRendererComponent>>(); } public void clearCache() { lcrMap.clear(); } public static String parse(String s, Action a) { String escape = "FrNw01234567"; //$NON-NLS-1$ StringBuilder ret = new StringBuilder(); int k = 0; int p = s.indexOf('@'); while (p != -1) { ret.append(s.substring(k,p)); char c = s.charAt(p + 1); if (!escape.contains(String.valueOf(c))) { ret.append('@'); k = p + 1; p = s.indexOf('@',k); continue; } if (c == 'F') { if (s.charAt(p + 2) == 'B' || s.charAt(p + 2) == 'I') p += 2; else ret.append('@'); k = p + 1; p = s.indexOf('@',k); continue; } if (c == 'r' && a.isRelative()) ret.append(Messages.getString("Action.RELATIVE")); //$NON-NLS-1$ if (c == 'N' && a.isNot()) ret.append(Messages.getString("Action.NOT")); //$NON-NLS-1$ ResourceReference<GmObject> at = a.getAppliesTo(); if (c == 'w' && !at.equals(GmObject.OBJECT_SELF)) { if (at.equals(GmObject.OBJECT_OTHER)) ret.append(Messages.getString("Action.APPLIES_OTHER")); //$NON-NLS-1$ else { GmObject applies = deRef(at); ret.append(Messages.format("Action.APPLIES",applies == null ? at.toString() //$NON-NLS-1$ : applies.getName())); } } if (c >= '0' && c < '8') { int arg = c - '0'; List<Argument> args = a.getArguments(); if (arg >= args.size()) ret.append('0'); else { Argument aa = args.get(arg); ret.append(aa.toString(a.getLibAction().libArguments[arg])); } } k = p + 2; p = s.indexOf('@',k); } return ret + s.substring(k); } public static String escape(String s) { s = s.replaceAll("&","&"); //$NON-NLS-1$ //$NON-NLS-2$ s = s.replaceAll("<","<"); //$NON-NLS-1$ //$NON-NLS-2$ s = s.replaceAll(">",">"); //$NON-NLS-1$ //$NON-NLS-2$ s = s.replaceAll("\n","<br>"); //$NON-NLS-1$ //$NON-NLS-2$ s = s.replaceAll("\\\\#","\n"); //$NON-NLS-1$ //$NON-NLS-2$ s = s.replaceAll("#","<br>"); //$NON-NLS-1$ //$NON-NLS-2$ s = s.replaceAll("\n","#"); //$NON-NLS-1$ //$NON-NLS-2$ return s.replaceAll(" "," "); //$NON-NLS-1$ //$NON-NLS-2$ } private static class ActionRendererComponent extends JLabel { private static final long serialVersionUID = 1L; private int indent; private boolean selected; private final JList list; public ActionRendererComponent(Action a, JList list) { this.list = list; setOpaque(true); setBackground(selected ? list.getSelectionBackground() : list.getBackground()); setForeground(selected ? list.getSelectionForeground() : list.getForeground()); LibAction la = a.getLibAction(); if (la.actImage == null) setText(Messages.getString("Action.UNKNOWN")); //$NON-NLS-1$ else { StringBuilder sb = new StringBuilder("<html>"); if (la.listText.contains("@FI")) //$NON-NLS-1$ sb.append("<i>"); if (la.listText.contains("@FB")) //$NON-NLS-1$ sb.append("<b>"); sb.append(escape(parse(la.listText,a))); setText(sb.toString()); setIcon(new ImageIcon(la.actImage)); if (Prefs.actionToolTipLines > 0 && Prefs.actionToolTipColumns > 0) { sb = new StringBuilder(); String snip = parse(la.hintText.replaceAll("(?<!\\\\)#","\n"),a); int last, next = -1; for (int i = 0; i < Prefs.actionToolTipLines; i++) { last = next + 1; next = snip.indexOf('\n',last); if (next == -1) { sb.append(snip.substring(last)); break; } if (next > last + Prefs.actionToolTipColumns) { sb.append(snip.substring(last,last + Prefs.actionToolTipColumns)); sb.append("..."); } else sb.append(snip.substring(last,next)); sb.append("\n"); } if (next != -1) sb.append(Messages.getString("Action.HINT_MORE")); setToolTipText("<html><font face=\"Courier\">" + escape(sb.toString())); } } } // public JToolTip createToolTip() // { // JToolTip tip = new JToolTip(); // tip.setComponent(this); // return tip; // } /** * Overridden to address java bug 6700748 by returning false. * In WinXP, the cursor flickers between two states on drag & drop. */ public boolean isVisible() { return false; } public void setIndent(int indent) { if (this.indent == indent) return; this.indent = indent; setBorder(new EmptyBorder(1,2 + 8 * indent,1,2)); } public void setSelected(boolean selected) { if (this.selected == selected) return; this.selected = selected; if (selected) { setBackground(list.getSelectionBackground()); setForeground(list.getSelectionForeground()); } else { setBackground(list.getBackground()); setForeground(list.getForeground()); } } } public Component getListCellRendererComponent(JList list, Object cell, int index, boolean isSelected, boolean hasFocus) { final Action cellAction = (Action) cell; SoftReference<ActionRendererComponent> arcref = lcrMap.get(cellAction); ActionRendererComponent arc = null; if (arcref != null) arc = arcref.get(); if (arc == null) { arc = new ActionRendererComponent(cellAction,list); lcrMap.put(cellAction,new SoftReference<ActionRendererComponent>(arc)); } ListModel lm = list.getModel(); try { if (lm instanceof ActionListModel) arc.setIndent(((ActionListModel) lm).indents.get(index)); } catch (IndexOutOfBoundsException e) { } arc.setSelected(isSelected); return arc; } } }