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-2006 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.core;
043:
044: import java.awt.BorderLayout;
045: import java.beans.PropertyChangeEvent;
046: import java.beans.PropertyChangeListener;
047: import java.io.IOException;
048: import java.io.ObjectInput;
049: import java.io.ObjectOutput;
050: import java.io.ObjectStreamException;
051: import java.io.Serializable;
052: import java.text.MessageFormat;
053: import java.util.ArrayList;
054: import java.util.Arrays;
055: import java.util.HashMap;
056: import java.util.Iterator;
057: import java.util.List;
058: import java.util.Map;
059: import java.util.logging.Level;
060: import java.util.logging.Logger;
061: import javax.swing.SwingUtilities;
062: import org.openide.explorer.ExplorerManager;
063: import org.openide.explorer.propertysheet.PropertySheet;
064: import org.openide.nodes.Node;
065: import org.openide.nodes.NodeAdapter;
066: import org.openide.nodes.NodeEvent;
067: import org.openide.nodes.NodeListener;
068: import org.openide.nodes.NodeOp;
069: import org.openide.util.Exceptions;
070: import org.openide.util.HelpCtx;
071: import org.openide.util.NbBundle;
072: import org.openide.util.Mutex;
073: import org.openide.util.Utilities;
074: import org.openide.util.io.NbMarshalledObject;
075: import org.openide.util.io.SafeException;
076: import org.openide.windows.Mode;
077: import org.openide.windows.TopComponent;
078: import org.openide.windows.Workspace;
079: import org.openide.windows.WindowManager;
080:
081: /**
082: * Default view for properties.
083: */
084: public final class NbSheet extends TopComponent {
085:
086: /**
087: * Name of a property that can be passed in a Node instance. The value
088: * of the property must be String and can be an alternative to displayName.
089: */
090: private static final String PROP_LONGER_DISPLAY_NAME = "longerDisplayName"; // NOI18N
091:
092: /** generated Serialized Version UID */
093: static final long serialVersionUID = 7807519514644165460L;
094:
095: /** shared sheet */
096: private static NbSheet sharedSheet;
097: /** listener to the property changes */
098: transient private final Listener listener;
099: /** listener to the node changes, especially their destruction */
100: transient private final SheetNodesListener snListener;
101: /** Should property sheet listen to the global changes ? */
102: boolean global;
103: /** the property sheet that is used to display nodes */
104: private PropertySheet propertySheet;
105: /** the nodes that are displayed in the property sheet */
106: private Node[] nodes = new Node[0];
107:
108: /** Constructor for new sheet.
109: * The sheet does not listen to global changes */
110: public NbSheet() {
111: this (false);
112: }
113:
114: /** @param global should the content change when global properties changes?
115: */
116: public NbSheet(boolean global) {
117: this .global = global;
118: this .propertySheet = new PropertySheet();
119:
120: // Instructs winsys to name this mode as single if only property sheet
121: // is docked in this mode
122: // it's workaround, should be solved throgh some Mode API in future
123: // # 16888. Properties sheet is in single mode in SDI only.
124: // putClientProperty(ModeImpl.NAMING_TYPE, ModeImpl.SDI_ONLY_COMP_NAME); // TEMP
125: //Bugfix #36087: Fix naming type
126: putClientProperty("NamingType", "BothOnlyCompName"); // NOI18N
127:
128: setLayout(new BorderLayout());
129: add(propertySheet, BorderLayout.CENTER);
130:
131: setIcon(Utilities.loadImage(
132: "org/netbeans/core/resources/frames/properties.gif",
133: true)); // NOI18N
134:
135: // #36738 Component has to have a name from begining.
136: updateTitle();
137: // XXX - please rewrite to regular API when available - see issue #55955
138: putClientProperty("SlidingName", NbBundle.getMessage(
139: NbSheet.class, "CTL_PropertiesWindow")); //NOI18N
140:
141: // name listener and node listener
142: listener = new Listener();
143:
144: snListener = new SheetNodesListener();
145:
146: // set accessiblle description
147: getAccessibleContext().setAccessibleName(
148: NbBundle.getBundle(NbSheet.class).getString(
149: "ACSN_PropertiesSheet"));
150: getAccessibleContext().setAccessibleDescription(
151: NbBundle.getBundle(NbSheet.class).getString(
152: "ACSD_PropertiesSheet"));
153: setActivatedNodes(null);
154: }
155:
156: /* Singleton accessor. As NbSheet is persistent singleton this
157: * accessor makes sure that NbSheet is deserialized by window system.
158: * Uses known unique TopComponent ID "properties" to get NbSheet instance
159: * from window system. "properties" is name of settings file defined in module layer.
160: */
161: public static NbSheet findDefault() {
162: if (sharedSheet == null) {
163: TopComponent tc = WindowManager.getDefault()
164: .findTopComponent("properties"); // NOI18N
165: if (tc != null) {
166: if (tc instanceof NbSheet) {
167: sharedSheet = (NbSheet) tc;
168: } else {
169: //Incorrect settings file?
170: IllegalStateException exc = new IllegalStateException(
171: "Incorrect settings file. Unexpected class returned." // NOI18N
172: + " Expected:"
173: + NbSheet.class.getName() // NOI18N
174: + " Returned:"
175: + tc.getClass().getName()); // NOI18N
176: Logger.getLogger(NbSheet.class.getName()).log(
177: Level.WARNING, null, exc);
178: //Fallback to accessor reserved for window system.
179: NbSheet.getDefault();
180: }
181: } else {
182: //NbSheet cannot be deserialized
183: //Fallback to accessor reserved for window system.
184: NbSheet.getDefault();
185: }
186: }
187: return sharedSheet;
188: }
189:
190: protected String preferredID() {
191: return "properties"; //NOI18N
192: }
193:
194: /* Singleton accessor reserved for window system ONLY. Used by window system to create
195: * NbSheet instance from settings file when method is given. Use <code>findDefault</code>
196: * to get correctly deserialized instance of NbSheet. */
197: public static NbSheet getDefault() {
198: if (sharedSheet == null) {
199: sharedSheet = new NbSheet(true);
200: }
201: return sharedSheet;
202: }
203:
204: /** Overriden to explicitely set persistence type of NbSheet
205: * to PERSISTENCE_ALWAYS */
206: public int getPersistenceType() {
207: return TopComponent.PERSISTENCE_ALWAYS;
208: }
209:
210: public HelpCtx getHelpCtx() {
211: // #40372 fix - for non-global properties display (assumed to be in a dialog), don't show the help button
212: return (global ? org.openide.explorer.ExplorerUtils.getHelpCtx(
213: nodes, new HelpCtx(NbSheet.class)) : null);
214: }
215:
216: /** Transfer the focus to the property sheet.
217: */
218: @SuppressWarnings("deprecation")
219: public void requestFocus() {
220: super .requestFocus();
221: propertySheet.requestFocus();
222: }
223:
224: /** Transfer the focus to the property sheet.
225: */
226: @SuppressWarnings("deprecation")
227: public boolean requestFocusInWindow() {
228: super .requestFocusInWindow();
229: return propertySheet.requestFocusInWindow();
230: }
231:
232: /** always open global property sheet in its special mode */
233: @SuppressWarnings("deprecation")
234: public void open(Workspace workspace) {
235: if (global) {
236: Workspace realWorkspace = (workspace == null) ? WindowManager
237: .getDefault().getCurrentWorkspace()
238: : workspace;
239: Mode tcMode = realWorkspace.findMode(this );
240: if (tcMode == null) {
241: // dock into our mode if not docked yet
242: Mode mode = realWorkspace.findMode("properties"); // NOI18N
243: if (mode == null) {
244: mode = realWorkspace.createMode(
245: "properties", // NOI18N
246: NbBundle.getBundle(NbSheet.class)
247: .getString("CTL_PropertiesWindow"),
248: null);
249: }
250: mode.dockInto(this );
251: }
252: }
253: // behave like superclass
254: super .open(workspace);
255:
256: if (global) {
257: // Set the nodes when opening.
258: SwingUtilities.invokeLater(listener);
259: }
260: }
261:
262: /** cache the title formatters, they are used frequently and are slow to construct */
263: private static MessageFormat globalPropertiesFormat = null;
264: private static MessageFormat localPropertiesFormat = null;
265:
266: /** Changes name of the component to reflect currently displayed nodes.
267: * Called when set of displayed nodes has changed.
268: */
269: protected void updateTitle() {
270: // different naming for global and local sheets
271: Mode ourMode = WindowManager.getDefault().findMode(this );
272: String nodeTitle = null;
273:
274: // Fix a bug #12890, copy the nodes to prevent race condition.
275: List<Node> copyNodes = new ArrayList<Node>(Arrays.asList(nodes));
276:
277: Node node = null;
278:
279: if (!copyNodes.isEmpty()) {
280: node = copyNodes.get(0);
281: }
282:
283: if (node == null) {
284: nodeTitle = ""; // NOI18N
285: } else {
286: nodeTitle = node.getDisplayName();
287: Object alternativeDisplayName = node
288: .getValue(PROP_LONGER_DISPLAY_NAME);
289: if (alternativeDisplayName instanceof String) {
290: nodeTitle = (String) alternativeDisplayName;
291: }
292: }
293: Object[] titleParams = new Object[] {
294: Integer.valueOf(copyNodes.size()), nodeTitle };
295: // different naming if docked in properties mode
296: if ((ourMode != null)
297: && ("properties".equals(ourMode.getName()))) { // NOI18N
298: if (globalPropertiesFormat == null) {
299: globalPropertiesFormat = new MessageFormat(NbBundle
300: .getMessage(NbSheet.class,
301: "CTL_FMT_GlobalProperties"));
302: }
303: setName(globalPropertiesFormat.format(titleParams));
304: } else {
305: if (localPropertiesFormat == null) {
306: localPropertiesFormat = new MessageFormat(NbBundle
307: .getMessage(NbSheet.class,
308: "CTL_FMT_LocalProperties"));
309: }
310: setName(localPropertiesFormat.format(titleParams));
311: }
312: setToolTipText(getName());
313: }
314:
315: /** Nodes to display.
316: */
317: public void setNodes(Node[] nodes) {
318: setNodesWithoutReattaching(nodes);
319: // re-attach to listen to new nodes
320: snListener.detach();
321: snListener.attach(nodes);
322: }
323:
324: final Node[] getNodes() {
325: return nodes;
326: }
327:
328: /** Helper method, called from SheetNodesListener inner class */
329: private void setNodesWithoutReattaching(Node[] nodes) {
330: this .nodes = nodes;
331: propertySheet.setNodes(nodes);
332: SwingUtilities.invokeLater(new Runnable() {
333: public void run() {
334: updateTitle();
335: }
336: });
337: }
338:
339: /*
340: public Dimension getPreferredSize () {
341: return propertySheet.getPreferredSize();
342: }
343: */
344:
345: /** Serialize this property sheet */
346: public void writeExternal(ObjectOutput out) throws IOException {
347: super .writeExternal(out);
348:
349: if (global) {
350: // write dummy array
351: out.writeObject(null);
352: } else {
353: Node.Handle[] arr = NodeOp.toHandles(nodes);
354: out.writeObject(arr);
355: }
356:
357: out.writeBoolean(global);
358: }
359:
360: /** Deserialize this property sheet. */
361: public void readExternal(ObjectInput in) throws IOException,
362: ClassNotFoundException {
363: try {
364: super .readExternal(in);
365: } catch (SafeException se) {
366: // ignore--we really do not care about the explorer manager that much
367: //System.err.println("ignoring a SafeException: " + se.getLocalizedMessage ());
368: }
369: Object obj = in.readObject();
370:
371: if (obj instanceof NbMarshalledObject
372: || obj instanceof ExplorerManager) {
373: // old version read the Boolean
374: global = ((Boolean) in.readObject()).booleanValue();
375: } else {
376: Node[] nodes;
377:
378: if (obj == null) {
379: // handles can also be null for global
380: // property sheet
381: nodes = TopComponent.getRegistry().getActivatedNodes();
382: } else {
383: // new version, first read the nodes and then the global boolean
384: Node.Handle[] arr = (Node.Handle[]) obj;
385:
386: try {
387: nodes = NodeOp.fromHandles(arr);
388: } catch (IOException ex) {
389: Exceptions.attachLocalizedMessage(ex, NbBundle
390: .getBundle(NbSheet.class).getString(
391: "EXC_CannotLoadNodes"));
392: Logger.getLogger(NbSheet.class.getName()).log(
393: Level.WARNING, null, ex);
394: nodes = new Node[0];
395: }
396: }
397:
398: global = in.readBoolean();
399:
400: setNodes(nodes);
401: }
402:
403: /*
404: if (obj instanceof Boolean) {
405: global = (Boolean)in.readObject ()
406:
407: global = ((Boolean)in.readObject()).booleanValue();
408: /*
409: // start global listening if needed, but wait until
410: // deserialization is done (ExplorerManager is uses
411: // post-deserialization validating too, so we are forced
412: // to use it)
413: ((ObjectInputStream)in).registerValidation(
414: new ObjectInputValidation () {
415: public void validateObject () {
416: updateGlobalListening(false);
417: }
418: }, 0
419: );
420: */
421: // JST: I guess we are not and moreover the type casting is really ugly
422: // updateGlobalListening (global);
423: }
424:
425: /** Resolve to singleton instance, if needed. */
426: public Object readResolve() throws ObjectStreamException {
427: if (global) {
428: return getDefault();
429: } else {
430: if ((nodes == null) || (nodes.length <= 0)) {
431: return null;
432: }
433: }
434: return this ;
435: }
436:
437: protected Object writeReplace() throws ObjectStreamException {
438: if (global) {
439: return new Replacer();
440: } else {
441: return super .writeReplace();
442: }
443: }
444:
445: private static final class Replacer implements Serializable {
446: static final long serialVersionUID = -7897067133215740572L;
447:
448: Replacer() {
449: }
450:
451: private Object readResolve() throws ObjectStreamException {
452: return NbSheet.getDefault();
453: }
454: }
455:
456: /** Helper, listener variable must be initialized before
457: * calling this */
458: private void updateGlobalListening(boolean listen) {
459: if (global) {
460: if (listen) {
461: TopComponent.getRegistry().addPropertyChangeListener(
462: listener);
463: } else {
464: TopComponent.getRegistry()
465: .removePropertyChangeListener(listener);
466: }
467: }
468: }
469:
470: protected void componentOpened() {
471: updateGlobalListening(true);
472: }
473:
474: protected void componentClosed() {
475: updateGlobalListening(false);
476: setNodes(new Node[0]);
477: }
478:
479: protected void componentDeactivated() {
480: super .componentDeactivated();
481: if (Utilities.isMac()) {
482: propertySheet.firePropertyChange("MACOSX", true, false);
483: }
484: }
485:
486: /** Change listener to changes in selected nodes. And also
487: * nodes listener to listen to global changes of the nodes.
488: */
489: private class Listener extends Object implements Runnable,
490: PropertyChangeListener {
491: Listener() {
492: }
493:
494: public void propertyChange(PropertyChangeEvent ev) {
495: if (TopComponent.Registry.PROP_ACTIVATED_NODES.equals(ev
496: .getPropertyName())) {
497: activate();
498: }
499: /*
500: if ((ev.getPropertyName().equals(TopComponent.Registry.PROP_ACTIVATED)) &&
501: (ev.getNewValue() == Sheet.this)) {
502: return; // we do not want to call setNodes if we are
503: // the activated window
504: }
505: activate ();
506: */
507: }
508:
509: public void run() {
510: activate();
511: }
512:
513: public void activate() {
514: Node[] arr = TopComponent.getRegistry().getActivatedNodes();
515: setNodes(arr);
516: }
517:
518: }
519:
520: /** Change listener to changes in selected nodes. And also
521: * nodes listener to listen to global changes of the nodes.
522: */
523: private class SheetNodesListener extends NodeAdapter implements
524: Runnable {
525:
526: /* maps nodes to their listeners (Node, WeakListener) */
527: private HashMap<Node, NodeListener> listenerMap;
528:
529: /* maps nodes to their proeprty change listeners (Node, WeakListener)*/
530: private HashMap<Node, PropertyChangeListener> pListenerMap;
531:
532: SheetNodesListener() {
533: }
534:
535: /** Fired when the node is deleted.
536: * @param ev event describing the node
537: */
538: public void nodeDestroyed(NodeEvent ev) {
539: Node destroyedNode = ev.getNode();
540: NodeListener listener = listenerMap.get(destroyedNode);
541: PropertyChangeListener pListener = pListenerMap
542: .get(destroyedNode);
543: // stop to listen to destroyed node
544: destroyedNode.removeNodeListener(listener);
545: destroyedNode.removePropertyChangeListener(pListener);
546: listenerMap.remove(destroyedNode);
547: pListenerMap.remove(destroyedNode);
548: // close top component (our outer class) if last node was destroyed
549: if (listenerMap.isEmpty() && !global) {
550: //fix #39251 start - posting the closing of TC to awtevent thread
551: Mutex.EVENT.readAccess(new Runnable() {
552: public void run() {
553: close();
554: }
555: });
556: //fix #39251 end
557: } else {
558: setNodesWithoutReattaching((listenerMap.keySet()
559: .toArray(new Node[listenerMap.size()])));
560: }
561: }
562:
563: public void attach(Node[] nodes) {
564: listenerMap = new HashMap<Node, NodeListener>(
565: nodes.length * 2);
566: pListenerMap = new HashMap<Node, PropertyChangeListener>(
567: nodes.length * 2);
568: NodeListener curListener = null;
569: PropertyChangeListener pListener = null;
570: // start to listen to all given nodes and map nodes to
571: // their listeners
572: for (int i = 0; i < nodes.length; i++) {
573: curListener = org.openide.nodes.NodeOp
574: .weakNodeListener(this , nodes[i]);
575: pListener = org.openide.util.WeakListeners
576: .propertyChange(this , nodes[i]);
577: listenerMap.put(nodes[i], curListener);
578: pListenerMap.put(nodes[i], pListener);
579: nodes[i].addNodeListener(curListener);
580: nodes[i].addPropertyChangeListener(pListener);
581: }
582: ;
583: }
584:
585: public void detach() {
586: if (listenerMap == null) {
587: return;
588: }
589: // stop to listen to all nodes
590: for (Iterator iter = listenerMap.entrySet().iterator(); iter
591: .hasNext();) {
592: Map.Entry curEntry = (Map.Entry) iter.next();
593: ((Node) curEntry.getKey())
594: .removeNodeListener((NodeListener) curEntry
595: .getValue());
596: }
597: for (Iterator iter = pListenerMap.entrySet().iterator(); iter
598: .hasNext();) {
599: Map.Entry curEntry = (Map.Entry) iter.next();
600: ((Node) curEntry.getKey())
601: .removePropertyChangeListener((PropertyChangeListener) curEntry
602: .getValue());
603: }
604: // destroy the map
605: listenerMap = null;
606: pListenerMap = null;
607: }
608:
609: public void propertyChange(PropertyChangeEvent pce) {
610: if (Node.PROP_DISPLAY_NAME.equals(pce.getPropertyName())) {
611: SwingUtilities.invokeLater(this );
612: }
613: }
614:
615: public void run() {
616: updateTitle();
617: }
618:
619: } // End of SheetNodesListener.
620:
621: }
|