/* * Sun Public License Notice * * The contents of this file are subject to the Sun Public License * Version 1.0 (the "License"). You may not use this file except in * compliance with the License. A copy of the License is available at * http://www.sun.com/ * * The Original Code is NetBeans. The Initial Developer of the Original * Code is Sun Microsystems, Inc. Portions Copyright 1997-2003 Sun * Microsystems, Inc. All Rights Reserved. */ package org.openide.text; import java.awt.BorderLayout; import java.io.*; import java.util.*; import java.awt.Dimension; import java.awt.Rectangle; import java.awt.Component; import javax.swing.Action; import javax.swing.JEditorPane; import javax.swing.JScrollPane; import javax.swing.SwingUtilities; import javax.swing.text.*; import org.openide.awt.UndoRedo; import org.openide.actions.FileSystemAction; import org.openide.ErrorManager; import org.openide.cookies.EditorCookie; import org.openide.loaders.*; import org.openide.windows.*; import org.openide.util.Task; import org.openide.util.TaskListener; import org.openide.util.actions.SystemAction; import org.openide.util.HelpCtx; import org.openide.util.NbBundle; /** Cloneable top component to hold the editor kit. */ public class CloneableEditor extends CloneableTopComponent { /** editor pane */ protected JEditorPane pane; /** Asociated editor support */ private CloneableEditorSupport support; /** Flag indicating it was initialized this <code>CloneableEditor</code> */ private boolean initialized; /** Position of cursor. Used to keep the value between deserialization * and initialization time. */ private int cursorPosition = -1; // #20647. More important custom component. /** Custom editor component, which is used if specified by document * which implements <code>NbDocument.CustomEditor</code> interface. * @see NbDocument.CustomEditor#createEditor */ private Component customComponent; private static final String HELP_ID = "editing.editorwindow"; // !!! NOI18N static final long serialVersionUID = -185739563792410059L; /** For externalization of subclasses only */ public CloneableEditor() { this(null); } /** Creates new editor component associated with * support object. * @param support support that holds the document and operations above it */ public CloneableEditor(CloneableEditorSupport support) { super(); this.support = support; // instruct winsys to save state of this top component only if opened putClientProperty("PersistenceType", "OnlyOpened"); updateName(); setCloseOperation(CLOSE_EACH); } /** Gives access to {@link CloneableEditorSupport} object under * this <code>CloneableEditor</code> component. * @return the {@link CloneableEditorSupport} object * that holds the document or <code>null</code>, what means * this component is not in valid state yet and can be discarded */ protected CloneableEditorSupport cloneableEditorSupport () { return support; } /** Get context help for this editor pane. * If the registered editor kit provides a help ID in bean info * according to the protocol described for {@link InstanceSupport#findHelp}, * then that it used, else general help on the editor is provided. * @return context help */ public HelpCtx getHelpCtx() { HelpCtx fromKit = InstanceSupport.findHelp (new InstanceSupport.Instance (support.kit ())); if (fromKit != null) return fromKit; else return new HelpCtx (HELP_ID); } /** Indicates whtehter this componetn can be closed in the workspace. * Overrides superclass method. Adds scheduling of "emptying" * <code>pane</code> field and removing all sub components. */ public boolean canClose(Workspace workspace,boolean last) { boolean result = super.canClose(workspace, last); int closeOp = getCloseOperation(); if(result && ((closeOp == CLOSE_EACH) || (closeOp == CLOSE_LAST && last))) { SwingUtilities.invokeLater( new Runnable() { public void run() { // #23486: pane could not be initialized yet. if(pane != null) { Document doc = support.createStyledDocument( pane.getEditorKit()); pane.setDocument (doc); pane.setEditorKit (null); } removeAll(); initialized = false; } } ); } return result; } /** Overrides superclass method. In case it is called first time, * initializes this <code>CloneableEditor</code>. */ protected void componentShowing() { super.componentShowing(); if(initialized || discard()) { return; } initialized = true; initialize(); } /** Performs needed initialization */ private void initialize() { final Task prepareTask = support.prepareDocument (); setLayout (new BorderLayout ()); final QuietEditorPane pane = new QuietEditorPane (); pane.getAccessibleContext().setAccessibleName( NbBundle.getMessage(CloneableEditor.class, "ACS_CloneableEditor_QuietEditorPane", this.getName())); pane.getAccessibleContext().setAccessibleDescription( NbBundle.getMessage(CloneableEditor.class, "ACSD_CloneableEditor_QuietEditorPane", this.getAccessibleContext().getAccessibleDescription())); this.pane = pane; pane.setEditorKit (support.kit ()); Document doc = support.getDocumentHack (); pane.setDocument (doc); if (doc instanceof NbDocument.CustomEditor) { NbDocument.CustomEditor ce = (NbDocument.CustomEditor)doc; customComponent = ce.createEditor(pane); if(customComponent == null) { throw new IllegalStateException("Document:" + doc // NOI18N + " implementing NbDocument.CustomEditor may not" // NOI18N + " return null component"); // NOI18N } add(customComponent, BorderLayout.CENTER); } else { // not custom editor add (new JScrollPane (pane), BorderLayout.CENTER); } pane.setWorking(QuietEditorPane.ALL); // Init action map: cut,copy,delete,paste actions. javax.swing.ActionMap am = getActionMap(); am.put(DefaultEditorKit.cutAction, getAction(DefaultEditorKit.cutAction)); am.put(DefaultEditorKit.copyAction, getAction(DefaultEditorKit.copyAction)); am.put("delete", getAction(DefaultEditorKit.deleteNextCharAction)); // NOI18N am.put(DefaultEditorKit.pasteAction, getAction(DefaultEditorKit.pasteAction)); // set the caret to right possition if this component was deserialized if (cursorPosition != -1) { prepareTask.addTaskListener(new TaskListener () { public void taskFinished(Task t) { Caret caret = pane.getCaret(); if(caret != null) { caret.setDot(cursorPosition); } prepareTask.removeTaskListener(this); } }); } } protected CloneableTopComponent createClonedObject() { return support.createCloneableTopComponent (); } /** Descendants overriding this method must either call * this implementation or fire the * {@link org.openide.cookies.EditorCookie.Observable#PROP_OPENED_PANES} * property change on their own. */ protected void componentOpened() { super.componentOpened(); CloneableEditorSupport ces = cloneableEditorSupport(); if (ces != null) { ces.firePropertyChange(EditorCookie.Observable.PROP_OPENED_PANES, null, null); } } /** Descendants overriding this method must either call * this implementation or fire the * {@link org.openide.cookies.EditorCookie.Observable#PROP_OPENED_PANES} * property change on their own. */ protected void componentClosed() { super.componentClosed(); CloneableEditorSupport ces = cloneableEditorSupport(); if (ces != null) { ces.firePropertyChange(EditorCookie.Observable.PROP_OPENED_PANES, null, null); } } /** Overrides superclass version. Opens top component only if * it is in valid state. * (Editor top component may become invalid after deserialization).<br> * Also tries to open all other top components which are docked * in editor mode on given workspace, but not visible.<br> */ public void open(Workspace workspace) { if(discard()) { ErrorManager.getDefault().log(ErrorManager.WARNING, "Can not open " + this + " component," // NOI18N + " its support environment is not valid" // NOI18N + " [support=" + support + ", env=" // NOI18N + (support == null ? null : support.env()) + "]"); // NOI18N } else { Workspace realWorkspace = (workspace == null) ? WindowManager.getDefault().getCurrentWorkspace() : workspace; dockIfNeeded(realWorkspace); boolean modeVisible = false; TopComponent[] tcArray = editorMode(realWorkspace).getTopComponents(); for (int i = 0; i < tcArray.length; i++) { if (tcArray[i].isOpened(realWorkspace)) { modeVisible = true; break; } } if (!modeVisible) { openOtherEditors(realWorkspace); } super.open(workspace); openOnOtherWorkspaces(realWorkspace); } } /** When closing last view, also close the document. * @return <code>true</code> if close succeeded */ protected boolean closeLast() { if (!support.canClose ()) { // if we cannot close the last window return false; } // close everything and do not ask support.notifyClosed (); if (support.lastSelected == this) { support.lastSelected = null; } return true; } /** The undo/redo manager of the support. * @return the undo/redo manager shared by all editors for this support */ public UndoRedo getUndoRedo() { return support.getUndoRedo (); } public SystemAction[] getSystemActions() { SystemAction[] sa = super.getSystemActions (); return SystemAction.linkActions (sa, new SystemAction[] { null, SystemAction.get(FileSystemAction.class) } ); } /** Transfer the focus to the editor pane. */ public void requestFocus() { super.requestFocus (); if(customComponent != null && !SwingUtilities.isDescendingFrom(pane, customComponent)) { customComponent.requestFocus(); } else if(pane != null) { pane.requestFocus(); } } /** Transfer default focus to editor pane. */ public boolean requestDefaultFocus() { if(customComponent != null && !SwingUtilities.isDescendingFrom(pane, customComponent)) { customComponent.requestFocus(); return true; } else if(pane != null) { pane.requestFocus(); return true; } return false; } /** @return Preferred size of editor top component */ public Dimension getPreferredSize() { Rectangle bounds = WindowManager.getDefault().getCurrentWorkspace().getBounds(); return new Dimension(bounds.width / 2, bounds.height / 2); } private Action getAction (String key) { if(key == null) { return null; } // Try to find the action from kit. EditorKit kit = support.kit (); if(kit == null) { // kit is cleared in closeDocument() return null; } Action[] actions = kit.getActions (); for(int i = 0; i < actions.length; i++) { if(key.equals(actions[i].getValue(Action.NAME))) { return actions[i]; } } return null; } /** Overrides superclass method. Remembers last selected component of * support belonging to this component. * @see #componentDeactivated */ protected void componentActivated() { support.lastSelected = this; } /** Updates the name and tooltip of this <code>CloneableEditor</code> * {@link org.openide.windows.TopComponent TopCompoenent} * according to the support retrieved from {@link #cloneableEditorSupport} * method. The name and tooltip are in case of support presence * updated thru its {@link CloneableEditorSupport#messageName} and * {@link CloneableEditorSupport#messageToolTip} methods. * @see #cloneableEditorSupport() */ protected void updateName() { CloneableEditorSupport ces = cloneableEditorSupport(); if(ces != null) { setName(ces.messageName()); setToolTipText(ces.messageToolTip()); } } public void writeExternal(ObjectOutput out) throws IOException { super.writeExternal(out); // Save environent if support is non-null. // XXX #13685: When support is null, the tc will be discarded // after deserialization. out.writeObject(support != null ? support.env () : null); // #16461 Caret could be null?!, // hot fix - making it robust for that case. int pos = 0; // 19559 Even pane could be null! Better solution would be put // writeReplace method in place also, but it is a API change. For // the time be just robust here. JEditorPane p = pane; if(p != null) { Caret caret = p.getCaret(); if(caret != null) { pos = caret.getDot(); } else { if(p instanceof QuietEditorPane) { int lastPos = ((QuietEditorPane)p).getLastPosition(); if(lastPos == -1) { ErrorManager.getDefault().notify( ErrorManager.INFORMATIONAL, new IllegalStateException( "Pane=" + p + "was not initialized yet!" // NOI18N ) ); } else { pos = lastPos; } } else { Document doc = (support != null ? support.getDocument() : null); // Relevant only if document is non-null?! if(doc != null) { ErrorManager.getDefault().notify( ErrorManager.INFORMATIONAL, new IllegalStateException( "Caret is null in editor pane=" + p // NOI18N + "\nsupport=" + support // NOI18N + "\ndoc=" + doc // NOI18N ) ); } } } } out.writeObject(new Integer(pos)); } public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { super.readExternal(in); int offset; Object firstObject = in.readObject (); if (firstObject instanceof DataObject) { // backward compatibility // read data object DataObject obj = (DataObject)firstObject; // load cursor position offset = ((Integer)in.readObject()).intValue(); // try to load or associate editor support Object maybe = in.readObject(); if (maybe instanceof CloneableEditorSupport) support = (CloneableEditorSupport)maybe; else { support = (CloneableEditorSupport)obj.getCookie(CloneableEditorSupport.class); if (support == null) { EditorSupport supp = (EditorSupport)obj.getCookie(EditorSupport.class); if (supp != null) support = supp.del; } } } else { // New deserialization that uses Env environment, // and which could be null(!) see writeExternal. if(firstObject instanceof CloneableOpenSupport.Env) { CloneableOpenSupport.Env env = (CloneableOpenSupport.Env)firstObject; CloneableOpenSupport os = env.findCloneableOpenSupport (); if (os instanceof EditorSupport) { support = ((EditorSupport)os).del; } else { support = (CloneableEditorSupport)os; } } // load cursor position offset = ((Integer)in.readObject()).intValue(); } if (!discard ()) { cursorPosition = offset; } updateName(); } /** * Replaces serializing object. Overrides superclass method. Adds checking * for object validity. In case this object is invalid * throws {@link java.io.NotSerializableException NotSerializableException}. * @throws ObjectStreamException When problem during serialization occures. * @throws NotSerializableException When this <code>CloneableEditor</code> * is invalid and doesn't want to be serialized. */ protected Object writeReplace() throws ObjectStreamException { if(discard()) { throw new NotSerializableException( "Serializing component is invalid: " + this); // NOI18N } return super.writeReplace(); } /** * Resolves deserialized object. Overrides superclass method. Adds checking * for object validity. In case this object is invalid * throws {@link java.io.InvalidObjectException InvalidObjectException}. * @throws ObjecStreamException When problem during serialization occures. * @throws InvalidObjectException When deserialized <code>CloneableEditor</code> * is invalid and shouldn't be used. */ protected Object readResolve() throws ObjectStreamException { if (discard ()) { throw new java.io.InvalidObjectException( "Deserialized component is invalid: " + this); // NOI18N } else { support.initializeCloneableEditor (this); return this; } } /** This component should be discarded if the associated environment * is not valid. */ private boolean discard () { return support == null || !support.env ().isValid (); } /** Dock this top component to editor mode if it is not docked * in some mode at this time */ private void dockIfNeeded(Workspace workspace) { // dock into editor mode if possible Mode ourMode = workspace.findMode(this); if (ourMode == null) { editorMode(workspace).dockInto(this); } } private Mode editorMode(Workspace workspace) { Mode ourMode = workspace.findMode(this); if (ourMode == null) { ourMode = workspace.createMode( CloneableEditorSupport.EDITOR_MODE, getName(), CloneableEditorSupport.class.getResource( "/org/openide/resources/editorMode.gif" // NOI18N ) ); } return ourMode; } /** Utility method, opens this top component on all workspaces * where editor mode is visible and which differs from given * workspace. */ private void openOnOtherWorkspaces(Workspace workspace) { Workspace[] workspaces = WindowManager.getDefault().getWorkspaces(); Mode curEditorMode = null; Mode tcMode = null; for (int i = 0; i < workspaces.length; i++) { // skip given workspace if (workspaces[i].equals(workspace)) { continue; } curEditorMode = workspaces[i].findMode(CloneableEditorSupport.EDITOR_MODE); tcMode = workspaces[i].findMode(this); if ( !isOpened(workspaces[i]) && curEditorMode != null && ( tcMode == null || tcMode.equals(curEditorMode) ) ) { // candidate for opening, but mode must be already visible // (= some opened top component in it) TopComponent[] tcArray = curEditorMode.getTopComponents(); for (int j = 0; j < tcArray.length; j++) { if (tcArray[j].isOpened(workspaces[i])) { // yep, open this top component on found workspace too pureOpen(this, workspaces[i]); break; } } } } } /** Utility method, opens top components which are opened * in editor mode on some other workspace. * This method should be called only if first top component is * being opened in editor mode on given workspace */ private void openOtherEditors(Workspace workspace) { // choose candidates for opening Set topComps = new HashSet(15); Workspace[] wsArray = WindowManager.getDefault().getWorkspaces(); Mode curEditorMode = null; TopComponent[] tcArray = null; for (int i = 0; i < wsArray.length; i++) { curEditorMode = wsArray[i].findMode(CloneableEditorSupport.EDITOR_MODE); if (curEditorMode != null) { tcArray = curEditorMode.getTopComponents(); for (int j = 0; j < tcArray.length; j++) { if (tcArray[j].isOpened(wsArray[i])) { topComps.add(tcArray[j]); } } } } // open choosed candidates for (Iterator iter = topComps.iterator(); iter.hasNext(); ) { pureOpen((TopComponent)iter.next(), workspace); } } /** Utility method, calls super version of open if given * top component is of Editor type, or calls regular open otherwise. * The goal is to prevent from cycle open call between * Editor top components */ private void pureOpen(TopComponent tc,Workspace workspace) { if (tc instanceof CloneableEditor) { ((CloneableEditor)tc).dockIfNeeded(workspace); ((CloneableEditor)tc).superOpen(workspace); } else { tc.open(workspace); } } private void superOpen(Workspace workspace) { super.open(workspace); } }