001: /*
002: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
003: *
004: * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
005: *
006: * The contents of this file are subject to the terms of either the GNU
007: * General Public License Version 2 only ("GPL") or the Common
008: * Development and Distribution License("CDDL") (collectively, the
009: * "License"). You may not use this file except in compliance with the
010: * License. You can obtain a copy of the License at
011: * http://www.netbeans.org/cddl-gplv2.html
012: * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
013: * specific language governing permissions and limitations under the
014: * License. When distributing the software, include this License Header
015: * Notice in each file and include the License file at
016: * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this
017: * particular file as subject to the "Classpath" exception as provided
018: * by Sun in the GPL Version 2 section of the License file that
019: * accompanied this code. If applicable, add the following below the
020: * License Header, with the fields enclosed by brackets [] replaced by
021: * your own identifying information:
022: * "Portions Copyrighted [year] [name of copyright owner]"
023: *
024: * Contributor(s):
025: *
026: * The Original Software is NetBeans. The Initial Developer of the Original
027: * Software is Sun Microsystems, Inc. Portions Copyright 1997-2007 Sun
028: * Microsystems, Inc. All Rights Reserved.
029: *
030: * If you wish your version of this file to be governed by only the CDDL
031: * or only the GPL Version 2, indicate your decision by adding
032: * "[Contributor] elects to include this software in this distribution
033: * under the [CDDL or GPL Version 2] license." If you do not indicate a
034: * single choice of license, a recipient has the option to distribute
035: * your version of this file under either the CDDL, the GPL Version 2 or
036: * to extend the choice of license to its licensees as provided above.
037: * However, if you add GPL Version 2 code and therefore, elected the GPL
038: * Version 2 license, then the option applies only if the new code is
039: * made subject to such option by the copyright holder.
040: */
041:
042: package org.netbeans.modules.compapp.casaeditor;
043:
044: import java.awt.EventQueue;
045: import java.beans.PropertyVetoException;
046: import java.io.Serializable;
047: import java.util.ArrayList;
048: import java.util.Enumeration;
049: import java.util.HashMap;
050: import java.util.List;
051: import java.util.Map;
052: import java.util.Set;
053: import org.netbeans.core.api.multiview.MultiViewHandler;
054: import org.netbeans.core.api.multiview.MultiViews;
055: import org.netbeans.core.spi.multiview.CloseOperationHandler;
056: import org.netbeans.core.spi.multiview.CloseOperationState;
057: import org.netbeans.modules.compapp.casaeditor.multiview.CasaMultiViewFactory;
058: import org.openide.cookies.CloseCookie;
059: import org.openide.cookies.EditCookie;
060: import org.openide.cookies.EditorCookie;
061: import org.openide.cookies.LineCookie;
062: import org.openide.cookies.OpenCookie;
063: import org.openide.filesystems.FileLock;
064: import org.openide.filesystems.FileObject;
065: import org.openide.loaders.DataObject;
066: import org.openide.text.CloneableEditor;
067: import org.openide.text.CloneableEditorSupport;
068: import org.openide.text.CloneableEditorSupport.Pane;
069: import org.openide.text.DataEditorSupport;
070: import org.openide.util.Task;
071: import org.openide.windows.CloneableTopComponent;
072: import org.openide.windows.Mode;
073: import org.openide.windows.TopComponent;
074: import org.openide.windows.WindowManager;
075:
076: import java.io.IOException;
077: import javax.swing.JEditorPane;
078: import javax.swing.SwingUtilities;
079: import javax.swing.text.AbstractDocument;
080: import javax.swing.text.StyledDocument;
081: import org.netbeans.api.project.Project;
082: import org.netbeans.core.api.multiview.MultiViewPerspective;
083: import org.netbeans.modules.compapp.casaeditor.design.CasaModelGraphScene;
084: import org.netbeans.modules.compapp.casaeditor.model.casa.CasaComponent;
085: import org.netbeans.modules.compapp.casaeditor.model.casa.CasaModel;
086: import org.netbeans.modules.compapp.casaeditor.model.jbi.CasaModelFactory;
087: import org.netbeans.modules.compapp.casaeditor.model.casa.CasaWrapperModel;
088: import org.netbeans.modules.compapp.casaeditor.multiview.CasaGraphMultiViewElement;
089: import org.netbeans.modules.xml.retriever.catalog.Utilities; //import org.netbeans.modules.xml.validation.ShowCookie;
090: import org.netbeans.modules.xml.xam.ModelSource;
091: import org.netbeans.modules.xml.xam.spi.Validator.ResultItem;
092: import org.netbeans.modules.xml.xam.ui.undo.QuietUndoManager;
093: import org.openide.ErrorManager;
094: import org.openide.awt.UndoRedo;
095: import org.openide.filesystems.FileChangeListener;
096: import org.openide.util.TaskListener;
097:
098: /**
099: *
100: * @author tli
101: */
102: public class CasaDataEditorSupport extends DataEditorSupport implements
103: /* CasaModelCookie, */
104: OpenCookie, EditCookie, EditorCookie.Observable, LineCookie,
105: CloseCookie
106: // ShowCookie
107: {
108:
109: /** Used for managing the prepareTask listener. */
110: private transient Task prepareTask2;
111:
112: private transient CasaModelGraphScene mScene;
113:
114: //private CasaDataObject mDataObject;
115:
116: /** Needed for casa file deletion. */
117: private static Map<DataObject, CasaWrapperModel> modelMap = new HashMap<DataObject, CasaWrapperModel>();
118:
119: public CasaDataEditorSupport(CasaDataObject sobj) {
120: super (sobj, new CasaEditorEnv(sobj));
121:
122: //mDataObject = sobj;
123:
124: setMIMEType("text/xml"); // NOI18N
125: //TODO: we need to use below one eventually
126: //setMIMEType(WSDLDataLoader.MIME_TYPE);
127: }
128:
129: public CasaEditorEnv getEnv() {
130: return (CasaEditorEnv) env;
131: }
132:
133: @Override
134: protected Pane createPane() {
135: TopComponent tc = CasaMultiViewFactory
136: .createMultiView((CasaDataObject) getDataObject());
137: Mode editorMode = WindowManager.getDefault().findMode(
138: CasaDataEditorSupport.EDITOR_MODE);
139: if (editorMode != null) {
140: editorMode.dockInto(tc);
141: }
142: return (Pane) tc;
143: }
144:
145: public static boolean isLastView(TopComponent tc) {
146:
147: if (!(tc instanceof CloneableTopComponent))
148: return false;
149:
150: boolean oneOrLess = true;
151: Enumeration en = ((CloneableTopComponent) tc).getReference()
152: .getComponents();
153: if (en.hasMoreElements()) {
154: en.nextElement();
155: if (en.hasMoreElements())
156: oneOrLess = false;
157: }
158:
159: return oneOrLess;
160: }
161:
162: // Change method access to public
163: @Override
164: public void initializeCloneableEditor(CloneableEditor editor) {
165: super .initializeCloneableEditor(editor);
166: // Force the title to update so the * left over from when the
167: // modified data object was discarded is removed from the title.
168: EventQueue.invokeLater(new Runnable() {
169: public void run() {
170: // Have to do this later to avoid infinite loop.
171: updateTitles();
172: }
173: });
174: }
175:
176: @Override
177: protected void updateTitles() {
178: // This method is invoked by DataEditorSupport.DataNodeListener
179: // whenever the DataNode displayName property is changed. It is
180: // also called when the CloneableEditorSupport is (un)modified.
181:
182: // Let the superclass handle the CloneableEditor instances.
183: super .updateTitles();
184:
185: // We need to get the title updated on the MultiViewTopComponent.
186: EventQueue.invokeLater(new Runnable() {
187: public void run() {
188: // Create a list of TopComponents associated with the
189: // editor's data object, starting with the active
190: // TopComponent. Add all open TopComponents in any
191: // mode that are associated with the DataObject.
192: // [Note that EDITOR_MODE does not contain editors in
193: // split mode.]
194: List<TopComponent> associatedTCs = new ArrayList<TopComponent>();
195: DataObject targetDO = getDataObject();
196: TopComponent activeTC = TopComponent.getRegistry()
197: .getActivated();
198: if (activeTC != null
199: && targetDO == (DataObject) activeTC
200: .getLookup().lookup(DataObject.class)) {
201: associatedTCs.add(activeTC);
202: }
203: Set openTCs = TopComponent.getRegistry().getOpened();
204: for (Object tc : openTCs) {
205: TopComponent tcc = (TopComponent) tc;
206: if (targetDO == (DataObject) tcc.getLookup()
207: .lookup(DataObject.class)) {
208: associatedTCs.add(tcc);
209: }
210: }
211: for (TopComponent tc : associatedTCs) {
212: // Make sure this is a multiview window, and not just some
213: // window that has our DataObject (e.g. Projects, Files).
214: MultiViewHandler mvh = MultiViews
215: .findMultiViewHandler(tc);
216: if (mvh != null) {
217: tc.setHtmlDisplayName(messageHtmlName());
218: String name = messageName();
219: tc.setDisplayName(name);
220: tc.setName(name);
221: tc.setToolTipText(messageToolTip());
222:
223: // Make the Source View multiview window un-editable.
224: JEditorPane[] editorPanes = getOpenedPanes();
225: if (editorPanes != null) {
226: for (JEditorPane editorPane : editorPanes) {
227: editorPane.setEditable(false);
228: }
229: }
230:
231: }
232: }
233: }
234: });
235: }
236:
237: @Override
238: protected UndoRedo.Manager createUndoRedoManager() {
239: // Override so the superclass will use our proxy undo manager
240: // instead of the default, then we can intercept edits.
241: return new QuietUndoManager(super .createUndoRedoManager());
242: // Note we cannot set the document on the undo manager right
243: // now, as CES is probably trying to open the document.
244: }
245:
246: /**
247: * Returns the UndoRedo.Manager instance managed by this editor support.
248: *
249: * @return UndoRedo.Manager instance.
250: */
251: public QuietUndoManager getUndoManager() {
252: return (QuietUndoManager) getUndoRedo();
253: }
254:
255: @Override
256: public Task prepareDocument() {
257: Task task = super .prepareDocument();
258: // Avoid listening to the same task more than once.
259: if (task != prepareTask2) {
260: prepareTask2 = task;
261: task.addTaskListener(new TaskListener() {
262: public void taskFinished(Task task) {
263: QuietUndoManager undo = getUndoManager();
264: StyledDocument doc = getDocument();
265: synchronized (undo) {
266: // Now that the document is ready, pass it to the manager.
267: undo.setDocument((AbstractDocument) doc);
268: if (!undo.isCompound()) {
269: // The superclass prepareDocument() adds the undo/redo
270: // manager as a listener -- we need to remove it since
271: // we will initially listen to the model instead.
272: doc.removeUndoableEditListener(undo);
273: // If not listening to document, then listen to model.
274: addUndoManagerToModel(undo);
275: }
276: }
277: prepareTask2 = null;
278: }
279: });
280: }
281: return task;
282: }
283:
284: public Task reloadDocument() {
285: Task task = super .reloadDocument();
286: task.addTaskListener(new TaskListener() {
287: public void taskFinished(Task task) {
288: EventQueue.invokeLater(new Runnable() {
289: public void run() {
290: QuietUndoManager undo = getUndoManager();
291: StyledDocument doc = getDocument();
292: // The superclass reloadDocument() adds the undo
293: // manager as an undoable edit listener.
294: synchronized (undo) {
295: if (!undo.isCompound()) {
296: doc.removeUndoableEditListener(undo);
297: }
298: }
299: }
300: });
301: }
302: });
303: return task;
304: }
305:
306: protected void notifyClosed() {
307: // Stop listening to the undoable edit sources when we are closed.
308: QuietUndoManager undo = getUndoManager();
309: StyledDocument doc = getDocument();
310: synchronized (undo) {
311: // May be null when closing the editor.
312: if (doc != null) {
313: doc.removeUndoableEditListener(undo);
314: undo.endCompound();
315: undo.setDocument(null);
316: }
317: try {
318: CasaWrapperModel model = getModel();
319: if (model != null) {
320: model.removeUndoableEditListener(undo);
321: }
322: // Must unset the model when no longer listening to it.
323: undo.setModel(null);
324: } catch (Exception ioe) {
325: // Model is gone, but just removing the listener is not
326: // going to matter anyway.
327: }
328: }
329:
330: super .notifyClosed();
331: }
332:
333: // When the scene is created, it is set here for access later.
334: public void setScene(CasaModelGraphScene scene) {
335: mScene = scene;
336: }
337:
338: // Provides access to the scene, for convenience.
339: public CasaModelGraphScene getScene() {
340: return mScene;
341: }
342:
343: public CasaWrapperModel getModel() {
344: CasaDataObject dobj = getEnv().getCasaDataObject();
345: return getModel(dobj);
346: }
347:
348: private static CasaWrapperModel getModel(DataObject dobj) {
349: CasaWrapperModel model = modelMap.get(dobj);
350: if (model == null) {
351: ModelSource modelSource = Utilities.getModelSource(dobj
352: .getPrimaryFile(), true);
353: if (modelSource != null) {
354: model = (CasaWrapperModel) CasaModelFactory
355: .getInstance().getModel(modelSource);
356: modelMap.put(dobj, model);
357: }
358: }
359: return model;
360: }
361:
362: /**
363: * Adds the undo/redo manager to the document as an undoable edit
364: * listener, so it receives the edits onto the queue. The manager
365: * will be removed from the model as an undoable edit listener.
366: *
367: * <p>This method may be called repeatedly.</p>
368: */
369: public void addUndoManagerToDocument() {
370: // This method may be called repeatedly.
371: // Stop the undo manager from listening to the model, as it will
372: // be listening to the document now.
373: QuietUndoManager undo = getUndoManager();
374: StyledDocument doc = getDocument();
375: synchronized (undo) {
376: try {
377: CasaWrapperModel model = getModel();
378: if (model != null) {
379: model.removeUndoableEditListener(undo);
380: }
381: // Must unset the model when no longer listening to it.
382: undo.setModel(null);
383: } catch (Exception ioe) {
384: // Model is gone, but just removing the listener is not
385: // going to matter anyway.
386: }
387: // Document may be null if the cloned views are not behaving correctly.
388: if (doc != null) {
389: // Ensure the listener is not added twice.
390: doc.removeUndoableEditListener(undo);
391: doc.addUndoableEditListener(undo);
392: // Start the compound mode of the undo manager, such that when
393: // we are hidden, we will treat all of the edits as a single
394: // compound edit. This avoids having the user invoke undo
395: // numerous times when in the model view.
396: undo.beginCompound();
397: }
398: }
399: }
400:
401: /**
402: * Removes the undo/redo manager undoable edit listener from the
403: * document, to stop receiving undoable edits. The manager will
404: * be added to the model as an undoable edit listener.
405: *
406: * <p>This method may be called repeatedly.</p>
407: */
408: public void removeUndoManagerFromDocument() {
409: // This method may be called repeatedly.
410: QuietUndoManager undo = getUndoManager();
411: StyledDocument doc = getDocument();
412: synchronized (undo) {
413: // May be null when closing the editor.
414: if (doc != null) {
415: doc.removeUndoableEditListener(undo);
416: undo.endCompound();
417: }
418: // Have the undo manager listen to the model when it is not
419: // listening to the document.
420: addUndoManagerToModel(undo);
421: }
422: }
423:
424: /**
425: * Add the undo/redo manager undoable edit listener to the model.
426: *
427: * <p>Caller should synchronize on the undo manager prior to calling
428: * this method, to avoid thread concurrency issues.</p>
429: *
430: * @param undo the undo manager.
431: */
432: private void addUndoManagerToModel(QuietUndoManager undo) {
433: // This method may be called repeatedly.
434: try {
435: CasaWrapperModel model = getModel();
436: if (model != null) {
437: // Ensure the listener is not added twice.
438: model.removeUndoableEditListener(undo);
439: model.addUndoableEditListener(undo);
440: // Ensure the model is sync'd when undo/redo is invoked,
441: // otherwise the edits are added to the queue and eventually
442: // cause exceptions.
443: undo.setModel(model);
444: }
445: } catch (Exception ioe) {
446: // Model is gone, nothing will work, return immediately.
447: }
448: }
449:
450: /**
451: * This method allows the close behavior of CloneableEditorSupport to be
452: * invoked from the SourceMultiViewElement. The close method of
453: * CloneableEditorSupport at least clears the undo queue and releases
454: * the swing document.
455: */
456: public boolean silentClose() {
457: return super .close(false);
458: }
459:
460: /**
461: * Implement ShowCookie.
462: */
463: public void show(final ResultItem resultItem) {
464: if (!(resultItem.getModel() instanceof CasaModel))
465: return;
466:
467: final CasaComponent casaComponent = (CasaComponent) resultItem
468: .getComponents();
469:
470: // Get the edit and line cookies.
471: DataObject d = getDataObject();
472: LineCookie lc = (LineCookie) d.getCookie(LineCookie.class);
473: final EditCookie ec = (EditCookie) d
474: .getCookie(EditCookie.class);
475: if (lc == null || ec == null) {
476: return;
477: }
478:
479: SwingUtilities.invokeLater(new Runnable() {
480: public void run() {
481: // Opens the editor or brings it into focus
482: // and makes it the activated topcomponent.
483: ec.edit();
484:
485: TopComponent tc = WindowManager.getDefault()
486: .getRegistry().getActivated();
487: MultiViewHandler mvh = MultiViews
488: .findMultiViewHandler(tc);
489:
490: if (mvh == null) {
491: return;
492: }
493:
494: // Set annotation or select element in the multiview.
495: MultiViewPerspective mvp = mvh.getSelectedPerspective();
496: if (mvp.preferredID().equals("casa-graphview")) { // NOI18N
497: List<TopComponent> list = getAssociatedTopComponents();
498: for (TopComponent topComponent : list) {
499: // Make sure this is a multiview window, and not just
500: // some window that has our DataObject (e.g. Projects,
501: // Files).
502: MultiViewHandler handler = MultiViews
503: .findMultiViewHandler(topComponent);
504: if (handler != null && topComponent != null) {
505: topComponent.getLookup().lookupAll(
506: Object.class);
507: CasaGraphMultiViewElement selector = topComponent
508: .getLookup()
509: .lookup(
510: CasaGraphMultiViewElement.class); // FIXME: add new interface
511: if (selector == null)
512: return;
513: selector.select(casaComponent);
514: }
515: }
516: }
517: }
518: });
519: }
520:
521: private List<TopComponent> getAssociatedTopComponents() {
522: // Create a list of TopComponents associated with the
523: // editor's schema data object, starting with the the
524: // active TopComponent. Add all open TopComponents in
525: // any mode that are associated with the DataObject.
526: // [Note that EDITOR_MODE does not contain editors in
527: // split mode.]
528: List<TopComponent> associatedTCs = new ArrayList<TopComponent>();
529: DataObject targetDO = getDataObject();
530: TopComponent activeTC = TopComponent.getRegistry()
531: .getActivated();
532: if (activeTC != null
533: && targetDO == activeTC.getLookup().lookup(
534: DataObject.class)) {
535: associatedTCs.add(activeTC);
536: }
537: Set openTCs = TopComponent.getRegistry().getOpened();
538: for (Object tc : openTCs) {
539: TopComponent tcc = (TopComponent) tc;
540: if (targetDO == tcc.getLookup().lookup(DataObject.class)) {
541: associatedTCs.add(tcc);
542: }
543: }
544: return associatedTCs;
545: }
546:
547: /** {@inheritDoc} */
548: public void saveDocument() throws IOException {
549: DataObject casaDO = getDataObject();
550: FileObject casaFO = casaDO.getPrimaryFile();
551: CasaWrapperModel model = getModel();
552: Project project = model.getJBIProject();
553: FileChangeListener listener = project.getLookup().lookup(
554: FileChangeListener.class);
555: if (listener != null) {
556: casaFO.removeFileChangeListener(listener);
557: casaFO.addFileChangeListener(listener);
558: }
559:
560: super .saveDocument();
561:
562: model.saveDocument();
563:
564: // syncModel();
565: casaDO.setModified(false);
566: }
567:
568: /**
569: * Have the Casa model sync with the document.
570: */
571: public void syncModel() {
572: // Only sync the document if the change relates to loss of focus,
573: // which indicates that we are switching from the source view.
574: // Update the tree with the modified text.
575: try {
576: if (getModel() != null) {
577: getModel().sync();
578: }
579: } catch (Throwable ioe) {
580: // initially, for debug purposes, just print out the exception
581: ioe.printStackTrace();
582: ErrorManager.getDefault().notify(ioe);
583:
584: // NotifyDescriptor nd = new NotifyDescriptor.Message(
585: // NbBundle.getMessage(
586: // CasaDataEditorSupport.class,
587: // "MSG_NotWellformedXml"),
588: // NotifyDescriptor.ERROR_MESSAGE);
589: // DialogDisplayer.getDefault().notify(nd);
590: }
591: }
592:
593: ////////////////////////////////////////////////////////////////////////////
594: // Inner class
595: ////////////////////////////////////////////////////////////////////////////
596:
597: /**
598: * Env class extends DataEditorSupport.Env.
599: */
600: protected static class CasaEditorEnv extends DataEditorSupport.Env {
601:
602: static final long serialVersionUID = -4541024350167923282L;
603:
604: public CasaEditorEnv(CasaDataObject obj) {
605: super (obj);
606: }
607:
608: public CloneableEditorSupport findTextEditorSupport() {
609: return getCasaDataObject().getEditorSupport();
610: }
611:
612: public CasaDataObject getCasaDataObject() {
613: return (CasaDataObject) getDataObject();
614: }
615:
616: @Override
617: protected FileObject getFile() {
618: return getDataObject().getPrimaryFile();
619: }
620:
621: @Override
622: protected FileLock takeLock() throws IOException {
623: return getDataObject().getPrimaryFile().lock();
624: }
625: }
626:
627: ////////////////////////////////////////////////////////////////////////////
628: // Inner class
629: ////////////////////////////////////////////////////////////////////////////
630:
631: /**
632: * Implementation of CloseOperationHandler for multiview. Ensures both
633: * column view and xml editor are correctly closed, data saved, etc. Holds
634: * a reference to DataObject only - to be serializable with the
635: * multiview TopComponent without problems.
636: */
637: public static class CloseHandler implements CloseOperationHandler,
638: Serializable {
639: static final long serialVersionUID = -3085696462320564244L;
640: private DataObject myDataObject;
641:
642: private CloseHandler() {
643: super ();
644: }
645:
646: public CloseHandler(DataObject dobj) {
647: myDataObject = dobj;
648: }
649:
650: private CasaDataEditorSupport getCasaDataEditorSupport() {
651: return myDataObject instanceof CasaDataObject ? ((CasaDataObject) myDataObject)
652: .getEditorSupport()
653: : null;
654: }
655:
656: public boolean resolveCloseOperation(
657: CloseOperationState[] elements) {
658: CasaDataEditorSupport editor = getCasaDataEditorSupport();
659: boolean canClose = editor != null ? editor.canClose()
660: : true;
661: // during the shutdown sequence this is called twice. The first time
662: // through the multi-view infrastructure. The second time is done through
663: // the TopComponent close. If the file is dirty and the user chooses
664: // to discard changes, the second time will also ask whether the
665: // to save or discard changes.
666: if (canClose) {
667: myDataObject.setModified(false);
668:
669: // discard changes to related models
670: CasaWrapperModel model = getModel(myDataObject);
671: if (model != null) {
672: // when deleting casa file, the model will be null at
673: // the second time. The related data objects should have
674: // been discarded at the first time.
675: model.discardRelatedDataObjects();
676: }
677:
678: // invalidate the in-memory copy of this data object
679: try {
680: myDataObject.setValid(false);
681: } catch (PropertyVetoException ex) {
682: ex.printStackTrace();
683: }
684:
685: modelMap.remove(myDataObject);
686: }
687: return canClose;
688: }
689: }
690: }
|