/* * File : Editor.java * Created : 04-jun-2002 16:30 * By : fbusquets * * JClic - Authoring and playing system for educational activities * * Copyright (C) 2000 - 2005 Francesc Busquets & Departament * d'Educacio de la Generalitat de Catalunya * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details (see the LICENSE file). */ package edu.xtec.jclic.edit; import edu.xtec.util.Options; import java.awt.AWTEvent; import java.awt.Component; import java.awt.SystemColor; import java.lang.reflect.Constructor; import java.util.Enumeration; import java.util.HashSet; import java.util.Iterator; import javax.swing.AbstractListModel; import javax.swing.Icon; import javax.swing.JTree; import javax.swing.ListSelectionModel; import javax.swing.tree.DefaultMutableTreeNode; import javax.swing.tree.DefaultTreeCellRenderer; import javax.swing.tree.DefaultTreeModel; import javax.swing.tree.MutableTreeNode; import javax.swing.tree.TreeNode; import javax.swing.tree.TreePath; /** * This generic class allows to modify the content and properties of an associated * {@link edu.xtec.jclic.edit.Editable} object. Editors provide methods to register * listeners that will be informed about changes occurred in its associated data * object. * The class extends {@link javax.swing.tree.DefaultMutableTreeNode} in order to * make easy to implement a tree of dependences between editor classes. * @author Francesc Busquets (fbusquets@xtec.cat) * @version 13.09.10 */ public abstract class Editor extends DefaultMutableTreeNode{ protected HashSet<EditorListener> listeners; protected DefaultTreeModel treeModel; protected JTree currentTree; protected ListSelectionModel listSelectionModel; protected LModel listModel; private boolean modified; public static EditorAction moveUpAction, moveDownAction, copyAction, cutAction, pasteAction, deleteAction; protected static Editor clip; protected static boolean clipCutted; protected Editor(Object data){ super(data); listeners=new HashSet<EditorListener>(); } public static Editor createEditor(String className, Object data, Editor parent){ Editor result=null; try{ Class cl=Class.forName(className); Constructor[] constructors=cl.getDeclaredConstructors(); Constructor cn=null; for(Constructor constr : constructors){ Class[] parameters=constr.getParameterTypes(); if(parameters!=null && parameters.length==1 && parameters[0].isInstance(data)){ cn=constr; break; } } if(cn==null) throw new Exception(); result=(Editor)cn.newInstance(new Object[]{data}); if(result==null) throw new Exception(); if(parent!=null) parent.add(result); result.createChildren(); } catch(Exception ex){ System.err.println("Unable to create "+className+" for "+data+"\n"+ex); } return result; } protected abstract void createChildren(); public String getTitleKey(){ return "edit_data"; } public boolean isModified(){ return modified; } public void setModified(boolean modified){ this.modified=modified; if(modified){ Editor ed=getEditorParent(); if(ed!=null) ed.setModified(true); } else{ Enumeration en=children(); while(en.hasMoreElements()) ((Editor)en.nextElement()).setModified(false); } } public DefaultTreeModel getTreeModel(){ if(isRoot()){ if(treeModel==null) setTreeModel(new DefaultTreeModel(this)); return treeModel; } return (treeModel!=null ? treeModel : getEditorParent().getTreeModel()); } public void setTreeModel(DefaultTreeModel treeModel){ this.treeModel=treeModel; } public void setCurrentTree(JTree currentTree){ this.currentTree=currentTree; } public JTree getCurrentTree(){ return currentTree!=null ? currentTree : isRoot() ? null : getEditorParent().getCurrentTree(); } public JTree createJTree(){ setCurrentTree(new JTree(getTreeModel())); currentTree.setCellRenderer(new DefaultTreeCellRenderer(){ @Override public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel, boolean expanded, boolean leaf, int row, boolean hasFocus){ super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus); if(value instanceof Editor){ Icon icon=((Editor)value).getIcon(leaf, expanded); if(icon!=null) setIcon(icon); if(clip==value && clipCutted){ setForeground(SystemColor.textInactiveText); } } return this; } }); return currentTree; } public AbstractListModel getListModel(){ if(listModel==null){ listModel=new LModel(); } return listModel; } protected class LModel extends AbstractListModel{ public Object getElementAt(int index){ return (index>=0 && index<getChildCount()) ? getChildAt(index) : null; } public int getSize(){ return getChildCount(); } @Override public void fireIntervalAdded(Object src, int index0, int index1){ super.fireIntervalAdded(src, index0, index1); } @Override public void fireIntervalRemoved(Object src, int index0, int index1){ super.fireIntervalRemoved(src, index0, index1); } @Override public void fireContentsChanged(Object src, int index0, int index1){ super.fireContentsChanged(src, index0, index1); } } @Override public void insert(MutableTreeNode newChild, int childIndex){ super.insert(newChild, childIndex); if(listModel!=null) listModel.fireIntervalAdded(listModel, childIndex, childIndex); } @Override public void remove(int childIndex){ super.remove(childIndex); if(listModel!=null) listModel.fireIntervalRemoved(listModel, childIndex, childIndex); } public void select(){ JTree tree=getCurrentTree(); if(tree!=null) tree.getSelectionModel().setSelectionPath(new TreePath(getPath())); if(getListSelectionModel()!=null){ Editor p=getEditorParent(); if(p!=null){ int index=p.getIndex(this); if(index>=0) getListSelectionModel().setSelectionInterval(index, index); } } } public void reselect(){ JTree tree=getCurrentTree(); if(tree!=null){ tree.getSelectionModel().clearSelection(); tree.getSelectionModel().setSelectionPath(new TreePath(getPath())); } if(getListSelectionModel()!=null){ Editor p=getEditorParent(); if(p!=null){ int index=p.getIndex(this); if(index>=0){ getListSelectionModel().removeIndexInterval(index, index); getListSelectionModel().setSelectionInterval(index, index); } } } } public int getNearestIndex(Editor fromChild, boolean down){ int result=down ? getChildCount() : 0; if(fromChild!=null){ int p=getIndex(fromChild); if(p>=0) result=p+(down ? 1 : 0); } return result; } protected boolean canClone(){ return false; } protected Editor getClone() throws Exception{ return null; } protected boolean delete(boolean changeSelection){ boolean result=false; Editor p=getEditorParent(); if(p!=null){ p.setModified(true); if(clip==this) setClip(null, false); int index=p.getIndex(this); if(index==p.getChildCount()-1) index--; getTreeModel().removeNodeFromParent(this); if(changeSelection){ Editor sel = index>=0 ? (Editor)p.getChildAt(index) : p; if(sel!=null){ sel.select(); } } // Added 03-Feb-2011 // Correct bug 172: when an editor loses its last element, // set their parent owner of the basic actions if(index<0) p.setActionsOwner(); result=true; } return result; } public boolean moveToIndex(int index, boolean updateSelection){ boolean result=false; DefaultTreeModel model=getTreeModel(); Editor p=getEditorParent(); if(p!=null && model!=null){ index=Math.min(Math.max(0, index), p.getChildCount()); if(index!=p.getIndex(this)){ p.setModified(true); model.removeNodeFromParent(this); model.insertNodeInto(this, p, index); result=true; if(updateSelection) select(); } } return result; } public boolean moveUp(boolean updateSelection){ boolean result=false; Editor p=getEditorParent(); if(p!=null){ int index=parent.getIndex(this); result=moveToIndex(index-1, updateSelection); } return result; } public boolean moveDown(boolean updateSelection){ boolean result=false; Editor p=getEditorParent(); if(p!=null){ int index=parent.getIndex(this); result=moveToIndex(index+1, updateSelection); } return result; } public boolean copy(){ boolean result=false; if(allowCopy && canClone()){ setClip(this, false); result=true; } return result; } protected static void setClip(Editor e, boolean cutted){ if(clip!=null){ Editor c=clip; clip=null; clipCutted=false; c.getTreeModel().nodeChanged(c); } clip=e; clipCutted=cutted; if(clip!=null) clip.getTreeModel().nodeChanged(clip); } public boolean cut(){ boolean result=false; if(allowCut){ setClip(this, true); result=true; } return result; } public boolean canBeParentOf(Editor e){ return !(getClass().isInstance(e)); } public boolean canBeSiblingOf(Editor e){ return true; } public boolean insertEditor(Editor e, boolean asChild, int index, boolean updateSelection){ boolean result=false; if(e!=null){ if(asChild){ setModified(true); if(index<0) index=getChildCount(); getTreeModel().insertNodeInto(e, this, index); result=true; if(updateSelection) e.select(); } else{ Editor p=getEditorParent(); if(p!=null){ p.setModified(true); if(index<0) index=p.getIndex(this); result=p.insertEditor(e, true, index, updateSelection); } } } return result; } protected boolean canPasteHere(){ return allowPaste && clip!=null && (!clipCutted || clip!=this) && (clipCutted || clip.canClone()) && (canBeParentOf(clip) || (canBeSiblingOf(clip) && getEditorParent()!=null)); } public boolean paste(boolean updateSelection){ boolean result=false; if(canPasteHere()){ Editor c=clip; if(clipCutted){ clip.delete(false); } else { try{ c=clip.getClone(); } catch(Exception ex){ System.err.println("Unable to clone "+clip+"\n"+ex); return false; } } result=insertEditor(c, canBeParentOf(c), -1, updateSelection); if(result){ setClip(clipCutted ? null : c, false); if(updateSelection){ c.select(); } } } return result; } public Icon getIcon(boolean leaf, boolean expanded){ return null; } public Editor getEditorParent(){ return (Editor)getParent(); } public Editor getFirstParent(Class cl){ Editor result=getEditorParent(); if(result!=null && !cl.isInstance(result)) result=result.getFirstParent(cl); return result; } public Editor getFirstChild(Class cl){ Editor result=null; Enumeration en=children(); while(en.hasMoreElements()){ Editor ed=(Editor)en.nextElement(); if(cl.isInstance(ed)){ result=ed; break; } } return result; } public Object getFirstObject(Class cl){ Object result=getUserObject(); if((result==null || !cl.isInstance(result)) && !isRoot()) result=getEditorParent().getFirstObject(cl); return result; } public abstract Class getEditorPanelClass(); public abstract EditorPanel createEditorPanel(Options options); @Override public String toString(){ return "generic Editor component"; } public Editor getChildByName(String name){ Editor result=null; if(name!=null){ Enumeration en=children(); while(result==null && en.hasMoreElements()){ Editor e=(Editor)en.nextElement(); if(name.equals(e.toString())) result=e; } } return result; } public static void createBasicActions(Options options){ if(!basicActionsCreated){ moveUpAction=new EditorAction("editor_moveUp", "icons/up.gif", "editor_moveUp_tooltip", options){ protected void doAction(Editor e){ e.moveUp(true); } }; moveDownAction=new EditorAction("editor_moveDown", "icons/down.gif", "editor_moveDown_tooltip", options){ protected void doAction(Editor e){ e.moveDown(true); } }; copyAction=new EditorAction("COPY", "icons/copy.gif", "COPY", options){ protected void doAction(Editor e){ e.copy(); } }; cutAction=new EditorAction("CUT", "icons/cut.gif", "CUT", options){ protected void doAction(Editor e){ e.cut(); } }; pasteAction=new EditorAction("PASTE", "icons/paste.gif", "PASTE", options){ protected void doAction(Editor e){ e.paste(true); } }; deleteAction=new EditorAction("DELETE", "icons/delete.gif", "DELETE", options){ protected void doAction(Editor e){ e.delete(true); } }; basicActionsCreated=true; } } protected static boolean basicActionsCreated=false; protected boolean restrictMoveToSameType=true; protected boolean allowCopy=false, allowCut=false, allowPaste=false, allowDelete=false; public static void clearBasicActionsOwner(){ if(basicActionsCreated){ moveUpAction.setActionOwner(null); moveDownAction.setActionOwner(null); copyAction.setActionOwner(null); cutAction.setActionOwner(null); pasteAction.setActionOwner(null); deleteAction.setActionOwner(null); } } public void setActionsOwner(){ if(basicActionsCreated){ boolean eUp=false, eDown=false; Editor p=getEditorParent(); if(p!=null){ int i=p.getIndex(this); eUp=i>0; eDown=i<p.getChildCount()-1; if(restrictMoveToSameType){ eUp = eUp && getClass().isInstance(p.getChildAt(i-1)); eDown = eDown && getClass().isInstance(p.getChildAt(i+1)); } } moveUpAction.setActionOwner(eUp ? this : null); moveDownAction.setActionOwner(eDown ? this : null); copyAction.setActionOwner(allowCopy && canClone() ? this : null); cutAction.setActionOwner(allowCut ? this : null); pasteAction.setActionOwner(canPasteHere() ? this : null); deleteAction.setActionOwner(allowDelete ? this : null); } } public void clearActionsOwner(){ clearBasicActionsOwner(); } protected static Component findParentForDlg(AWTEvent ev){ if(ev!=null && ev.getSource() instanceof Component) return (Component)ev.getSource(); else return null; } public interface EditorListener{ public void editorDataChanged(Editor e); } public void addEditorListener(EditorListener ls){ listeners.add(ls); } public void removeEditorListener(EditorListener ls){ listeners.remove(ls); } public void fireEditorDataChanged(EditorListener agent){ setModified(true); DefaultTreeModel tm=getTreeModel(); if(tm!=null) tm.nodeChanged(this); Iterator it=listeners.iterator(); while(it.hasNext()){ EditorListener ls=(EditorListener)it.next(); if(ls!=agent) ls.editorDataChanged(this); } } public void collectData(){ Iterator it=listeners.iterator(); while(it.hasNext()){ Object o=it.next(); if(o instanceof EditorPanel){ ((EditorPanel)o).save(); break; } } for(int i=0; i<getChildCount(); i++){ TreeNode tn=getChildAt(i); if(tn instanceof Editor){ ((Editor)tn).collectData(); } } } /** Getter for property listSelectionModel. * @return Value of property listSelectionModel. */ public ListSelectionModel getListSelectionModel() { ListSelectionModel result=listSelectionModel; if(result==null && getEditorParent()!=null) result=getEditorParent().getListSelectionModel(); return result; } /** Setter for property listSelectionModel. * @param listSelectionModel New value of property listSelectionModel. */ public void setListSelectionModel(ListSelectionModel listSelectionModel) { this.listSelectionModel = listSelectionModel; } public boolean editData(Component parent, Options options){ boolean result=false; EditorPanel panel=createEditorPanel(options); if(panel!=null){ panel.attachEditor(this, false); result=options.getMessages().showInputDlg(parent, panel, getTitleKey()); panel.attachEditor(null, result); if(result) setModified(true); } return result; } }