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: package org.openide.explorer.view;
042:
043: import java.lang.ref.Reference;
044: import org.openide.explorer.propertysheet.*;
045: import org.openide.nodes.Node;
046:
047: import org.openide.nodes.Node.Property;
048: import org.openide.util.NbBundle;
049:
050: import java.awt.Color;
051: import java.awt.Component;
052: import java.awt.Container;
053: import java.awt.Font;
054: import java.awt.Graphics;
055: import java.awt.KeyboardFocusManager;
056:
057: import java.beans.*;
058:
059: import java.lang.ref.WeakReference;
060: import java.lang.reflect.InvocationTargetException;
061:
062: import java.text.MessageFormat;
063:
064: import java.util.EventObject;
065: import java.util.Map;
066: import java.util.StringTokenizer;
067: import java.util.WeakHashMap;
068: import java.util.logging.Level;
069: import java.util.logging.Logger;
070:
071: import javax.accessibility.AccessibleContext;
072: import javax.accessibility.AccessibleRole;
073:
074: import javax.swing.*;
075: import javax.swing.event.TableModelEvent;
076: import javax.swing.event.TableModelListener;
077: import javax.swing.table.*;
078:
079: import org.netbeans.modules.openide.explorer.TTVEnvBridge;
080:
081: /**
082: * TableCellEditor/Renderer implementation. Component returned is the PropertyPanel
083: *
084: * @author Jan Rojcek
085: */
086: class TableSheetCell extends AbstractCellEditor implements
087: TableModelListener, PropertyChangeListener, TableCellEditor,
088: TableCellRenderer {
089: /* Table sheet cell works only with NodeTableModel */
090: private NodeTableModel tableModel;
091:
092: /* Determines how to paint renderer */
093: private Boolean flat;
094:
095: //
096: // Editor
097: //
098:
099: /** Actually edited node (its property) */
100: private Node node;
101:
102: /** Edited property */
103: private Property prop;
104:
105: //
106: // Renderer
107: //
108:
109: /** Default header renderer */
110: private TableCellRenderer headerRenderer = new JTable()
111: .getTableHeader().getDefaultRenderer();
112:
113: /** Null panel is used if cell value is null */
114: private NullPanel nullPanel;
115:
116: /** Two-tier cache for property panels
117: * Map<TreeNode, WeakHashMap<Node.Property, Reference<FocusedPropertyPanel>> */
118: private Map panelCache = new WeakHashMap(); // weak! #31275
119: private FocusedPropertyPanel renderer = null;
120: private PropertyPanel editor = null;
121:
122: public TableSheetCell(NodeTableModel tableModel) {
123: this .tableModel = tableModel;
124: setFlat(false);
125: }
126:
127: /**
128: * Set how to paint renderer.
129: * @param f <code>true</code> means flat, <code>false</code> means with button border
130: */
131: public void setFlat(boolean f) {
132: Color controlDkShadow = Color.lightGray;
133:
134: if (UIManager.getColor("controlDkShadow") != null) {
135: controlDkShadow = UIManager.getColor("controlDkShadow"); // NOI18N
136: }
137:
138: Color controlLtHighlight = Color.black;
139:
140: if (UIManager.getColor("controlLtHighlight") != null) {
141: controlLtHighlight = UIManager
142: .getColor("controlLtHighlight"); // NOI18N
143: }
144:
145: Color buttonFocusColor = Color.blue;
146:
147: if (UIManager.getColor("Button.focus") != null) {
148: buttonFocusColor = UIManager.getColor("Button.focus"); // NOI18N
149: }
150:
151: flat = f ? Boolean.TRUE : Boolean.FALSE;
152: }
153:
154: /** Returns <code>null<code>.
155: * @return <code>null</code>
156: */
157: public Object getCellEditorValue() {
158: return null;
159: }
160:
161: /** Returns editor of property.
162: * @param table
163: * @param value
164: * @param isSelected
165: * @param r row
166: * @param c column
167: * @return <code>PropertyPanel</code>
168: */
169: public Component getTableCellEditorComponent(JTable table,
170: Object value, boolean isSelected, int r, int c) {
171: prop = (Property) value;
172: node = tableModel.nodeForRow(r);
173: node.addPropertyChangeListener(this );
174: tableModel.addTableModelListener(this );
175:
176: // create property panel
177: PropertyPanel propPanel = getEditor(prop, node);
178:
179: propPanel.setBackground(table.getSelectionBackground());
180: propPanel.setForeground(table.getSelectionForeground());
181:
182: //Fix for 35534, text shifts when editing. Maybe better fix possible
183: //in EditablePropertyDisplayer or InplaceEditorFactory.
184: propPanel.setBorder(BorderFactory.createMatteBorder(0, 1, 0, 0,
185: table.getSelectionBackground()));
186:
187: return propPanel;
188: }
189:
190: /** Cell should not be selected
191: * @param ev event
192: * @return <code>false</code>
193: */
194: public boolean shouldSelectCell(EventObject ev) {
195: return true;
196: }
197:
198: /** Return true.
199: * @param e event
200: * @return <code>true</code>
201: */
202: public boolean isCellEditable(EventObject e) {
203: return true;
204: }
205:
206: /** Forwards node property change to property model
207: * @param evt event
208: */
209: public void propertyChange(PropertyChangeEvent evt) {
210: // stopCellEditing(); //XXX ?
211: ((NodeTableModel) tableModel).fireTableDataChanged();
212: }
213:
214: /**
215: * Detaches listeners.
216: * Calls <code>fireEditingStopped</code> and returns true.
217: * @return true
218: */
219: public boolean stopCellEditing() {
220: if (prop != null) {
221: detachEditor();
222: }
223:
224: return super .stopCellEditing();
225: }
226:
227: /**
228: * Detaches listeners.
229: * Calls <code>fireEditingCanceled</code>.
230: */
231: public void cancelCellEditing() {
232: if (prop != null) {
233: detachEditor();
234: }
235:
236: super .cancelCellEditing();
237: }
238:
239: /** Table has changed. If underlied property was switched then cancel editing.
240: * @param e event
241: */
242: public void tableChanged(TableModelEvent e) {
243: cancelCellEditing();
244: }
245:
246: /** Removes listeners and frees resources.
247: */
248: private void detachEditor() {
249: node.removePropertyChangeListener(this );
250: tableModel.removeTableModelListener(this );
251: node = null;
252: prop = null;
253: }
254:
255: private FocusedPropertyPanel getRenderer(Property p, Node n) {
256: TTVEnvBridge bridge = TTVEnvBridge.getInstance(this );
257: bridge.setCurrentBeans(new Node[] { n });
258:
259: if (renderer == null) {
260: renderer = new FocusedPropertyPanel(p,
261: PropertyPanel.PREF_READ_ONLY
262: | PropertyPanel.PREF_TABLEUI);
263: renderer.putClientProperty("beanBridgeIdentifier", this ); //NOI18N
264: }
265:
266: renderer.setProperty(p);
267: renderer.putClientProperty("flat", Boolean.TRUE);
268:
269: return renderer;
270: }
271:
272: /** Getter for actual cell renderer.
273: * @param table
274: * @param value
275: * @param isSelected
276: * @param hasFocus
277: * @param row
278: * @param column
279: * @return <code>PropertyPanel</code>
280: */
281: public Component getTableCellRendererComponent(JTable table,
282: Object value, boolean isSelected, boolean hasFocus,
283: int row, int column) {
284: // Header renderer
285: if (row == -1) {
286: Component comp = headerRenderer
287: .getTableCellRendererComponent(table, value,
288: isSelected, hasFocus, row, column);
289:
290: if (comp instanceof JComponent) {
291: String tip = (column > 0) ? tableModel
292: .propertyForColumn(column)
293: .getShortDescription() : table.getColumnName(0);
294: ((JComponent) comp).setToolTipText(tip);
295: }
296:
297: return comp;
298: }
299:
300: Property prop = (Property) value;
301: Node node = tableModel.nodeForRow(row);
302:
303: if (prop != null) {
304: FocusedPropertyPanel propPanel = getRenderer(prop, node);
305: propPanel.setFocused(hasFocus);
306:
307: String tooltipText = null;
308:
309: try {
310: Object tooltipValue = prop.getValue();
311:
312: if (null != tooltipValue) {
313: tooltipText = tooltipValue.toString();
314: }
315: } catch (IllegalAccessException eaE) {
316: Logger.getLogger(TableSheetCell.class.getName()).log(
317: Level.WARNING, null, eaE);
318: } catch (InvocationTargetException itE) {
319: Logger.getLogger(TableSheetCell.class.getName()).log(
320: Level.WARNING, null, itE);
321: }
322:
323: propPanel.setToolTipText(createHtmlTooltip(tooltipText,
324: propPanel.getFont()));
325: propPanel.setOpaque(true);
326:
327: if (isSelected) {
328: Component focusOwner = KeyboardFocusManager
329: .getCurrentKeyboardFocusManager()
330: .getFocusOwner();
331:
332: boolean tableHasFocus = (table == focusOwner)
333: || table.isAncestorOf(focusOwner)
334: || (focusOwner instanceof Container && ((Container) focusOwner)
335: .isAncestorOf(table));
336:
337: if ((table == focusOwner) && table.isEditing()) {
338: //XXX really need to check if the editor has focus
339: tableHasFocus = true;
340: }
341:
342: propPanel.setBackground(tableHasFocus ? table
343: .getSelectionBackground() : TreeTable
344: .getUnfocusedSelectedBackground());
345:
346: propPanel.setForeground(tableHasFocus ? table
347: .getSelectionForeground() : TreeTable
348: .getUnfocusedSelectedForeground());
349: } else {
350: propPanel.setBackground(table.getBackground());
351: propPanel.setForeground(table.getForeground());
352: }
353:
354: return propPanel;
355: }
356:
357: if (nullPanel == null) {
358: nullPanel = new NullPanel(node);
359: nullPanel.setOpaque(true);
360: } else {
361: nullPanel.setNode(node);
362: }
363:
364: if (isSelected) {
365: Component focusOwner = KeyboardFocusManager
366: .getCurrentKeyboardFocusManager().getFocusOwner();
367:
368: boolean tableHasFocus = hasFocus
369: || (table == focusOwner)
370: || table.isAncestorOf(focusOwner)
371: || (focusOwner instanceof Container && ((Container) focusOwner)
372: .isAncestorOf(table));
373:
374: nullPanel.setBackground(tableHasFocus ? table
375: .getSelectionBackground() : TreeTable
376: .getUnfocusedSelectedBackground());
377:
378: //XXX may want to handle inverse theme here and use brighter if
379: //below a threshold. Deferred to centralized color management
380: //being implemented.
381: nullPanel.setForeground(table.getSelectionForeground()
382: .darker());
383: } else {
384: nullPanel.setBackground(table.getBackground());
385: nullPanel.setForeground(table.getForeground());
386: }
387:
388: nullPanel.setFocused(hasFocus);
389:
390: return nullPanel;
391: }
392:
393: private PropertyPanel getEditor(Property p, Node n) {
394: int prefs = PropertyPanel.PREF_TABLEUI;
395:
396: TTVEnvBridge bridge = TTVEnvBridge.getInstance(this );
397:
398: //workaround for issue 38132 - use env bridge to pass the
399: //node to propertypanel so it can call PropertyEnv.setBeans()
400: //with it. The sad thing is almost nobody uses PropertyEnv.getBeans(),
401: //but we have to do it for all cases.
402: bridge.setCurrentBeans(new Node[] { n });
403:
404: if (editor == null) {
405: editor = new PropertyPanel(p, prefs);
406:
407: editor.putClientProperty("flat", Boolean.TRUE); //NOI18N
408: editor.putClientProperty("beanBridgeIdentifier", this ); //NOI18N
409:
410: editor.setProperty(p);
411:
412: return editor;
413: }
414:
415: editor.setProperty(p);
416:
417: //Okay, the property panel has already grabbed the beans, clear
418: //them so no references are held.
419: return editor;
420: }
421:
422: void updateUI() {
423: headerRenderer = new JTable().getTableHeader()
424: .getDefaultRenderer();
425: }
426:
427: private static String getString(String key) {
428: return NbBundle.getBundle(TableSheetCell.class).getString(key);
429: }
430:
431: /**
432: * HTML-ize a tooltip, splitting long lines. It's package private for unit
433: * testing.
434: */
435: static String createHtmlTooltip(String value, Font font) {
436: if (value == null) {
437: return "null"; // NOI18N
438: }
439:
440: // break up massive tooltips
441: String token = null;
442:
443: if (value.indexOf(" ") != -1) { //NOI18N
444: token = " "; //NOI18N
445: } else if (value.indexOf(",") != -1) { //NOI18N
446: token = ","; //NOI18N
447: } else if (value.indexOf(";") != -1) { //NOI18N
448: token = ";"; //NOI18N
449: } else if (value.indexOf("/") != -1) { //NOI18N
450: token = "/"; //NOI18N
451: } else if (value.indexOf(">") != -1) { //NOI18N
452: token = ">"; //NOI18N
453: } else if (value.indexOf("\\") != -1) { //NOI18N
454: token = "\\"; //NOI18N
455: } else {
456: //give up
457: return makeDisplayble(value, font);
458: }
459:
460: StringTokenizer tk = new StringTokenizer(value, token, true);
461:
462: StringBuffer sb = new StringBuffer(value.length() + 20);
463: sb.append("<html>"); //NOI18N
464:
465: int charCount = 0;
466: int lineCount = 0;
467:
468: while (tk.hasMoreTokens()) {
469: String a = tk.nextToken();
470: a = makeDisplayble(a, font);
471: charCount += a.length();
472: sb.append(a);
473:
474: if (tk.hasMoreTokens()) {
475: charCount++;
476: }
477:
478: if (charCount > 80) {
479: sb.append("<br>"); //NOI18N
480: charCount = 0;
481: lineCount++;
482:
483: if (lineCount > 10) {
484: //Don't let things like VCS variables create
485: //a tooltip bigger than the screen. 99% of the
486: //time this is not a problem.
487: sb.append(NbBundle.getMessage(TableSheetCell.class,
488: "MSG_ELLIPSIS")); //NOI18N
489:
490: return sb.toString();
491: }
492: }
493: }
494:
495: sb.append("</html>"); //NOI18N
496:
497: return sb.toString();
498: }
499:
500: /**
501: * Makes the given String displayble. Probably there doesn't exists
502: * perfect solution for all situation. (someone prefer display those
503: * squares for undisplayable chars, someone unicode placeholders). So lets
504: * try do the best compromise.
505: */
506: private static String makeDisplayble(String str, Font f) {
507: if (null == str) {
508: return str;
509: }
510:
511: if (null == f) {
512: f = new JLabel().getFont();
513: }
514:
515: StringBuffer buf = new StringBuffer((int) (str.length() * 1.3)); // x -> \u1234
516: char[] chars = str.toCharArray();
517:
518: for (int i = 0; i < chars.length; i++) {
519: char c = chars[i];
520:
521: switch (c) {
522: case '\t':
523: buf.append(" " + // NOI18N
524: " "); // NOI18N
525: break;
526:
527: case '\n':
528: break;
529:
530: case '\r':
531: break;
532:
533: case '\b':
534: buf.append("\\b");
535:
536: break; // NOI18N
537:
538: case '\f':
539: buf.append("\\f");
540:
541: break; // NOI18N
542:
543: default:
544:
545: if (!processHtmlEntity(buf, c)) {
546: if ((null == f) || f.canDisplay(c)) {
547: buf.append(c);
548: } else {
549: buf.append("\\u"); // NOI18N
550:
551: String hex = Integer.toHexString(c);
552:
553: for (int j = 0; j < (4 - hex.length()); j++)
554: buf.append('0');
555:
556: buf.append(hex);
557: }
558: }
559: }
560: }
561:
562: return buf.toString();
563: }
564:
565: private static boolean processHtmlEntity(StringBuffer buf, char c) {
566: switch (c) {
567: case '>':
568: buf.append(">");
569:
570: break; // NOI18N
571:
572: case '<':
573: buf.append("<");
574:
575: break; // NOI18N
576:
577: case '&':
578: buf.append("&");
579:
580: break; // NOI18N
581:
582: default:
583: return false;
584: }
585:
586: return true;
587: }
588:
589: private static class NullPanel extends JPanel {
590: private Reference<Node> weakNode;
591: private boolean focused = false;
592:
593: NullPanel(Node node) {
594: this .weakNode = new WeakReference<Node>(node);
595: }
596:
597: void setNode(Node node) {
598: this .weakNode = new WeakReference<Node>(node);
599: }
600:
601: public AccessibleContext getAccessibleContext() {
602: if (accessibleContext == null) {
603: accessibleContext = new AccessibleNullPanel();
604: }
605:
606: return accessibleContext;
607: }
608:
609: public void setFocused(boolean val) {
610: focused = val;
611: }
612:
613: public void paintComponent(Graphics g) {
614: super .paintComponent(g);
615:
616: if (focused) {
617: Color bdr = UIManager
618: .getColor("Tree.selectionBorderColor"); //NOI18N
619:
620: if (bdr == null) {
621: //Button focus color doesn't work on win classic - better to
622: //get the color from a value we know will work - Tim
623: if (getForeground().equals(Color.BLACK)) { //typical
624: bdr = getBackground().darker();
625: } else {
626: bdr = getForeground().darker();
627: }
628: }
629:
630: g.setColor(bdr);
631: g.drawRect(1, 1, getWidth() - 3, getHeight() - 3);
632: g.setColor(bdr);
633: }
634: }
635:
636: public void addComponentListener(
637: java.awt.event.ComponentListener l) {
638: //do nothing
639: }
640:
641: public void addHierarchyListener(
642: java.awt.event.HierarchyListener l) {
643: //do nothing
644: }
645:
646: public void repaint() {
647: //do nothing
648: }
649:
650: public void repaint(int x, int y, int width, int height) {
651: //do nothing
652: }
653:
654: public void invalidate() {
655: //do nothing
656: }
657:
658: public void revalidate() {
659: //do nothing
660: }
661:
662: public void validate() {
663: //do nothing
664: }
665:
666: public void firePropertyChange(String s, Object a, Object b) {
667: //do nothing
668: }
669:
670: private class AccessibleNullPanel extends AccessibleJPanel {
671: AccessibleNullPanel() {
672: }
673:
674: public String getAccessibleName() {
675: String name = super .getAccessibleName();
676:
677: if (name == null) {
678: name = getString("ACS_NullPanel");
679: }
680:
681: return name;
682: }
683:
684: public String getAccessibleDescription() {
685: String description = super .getAccessibleDescription();
686:
687: if (description == null) {
688: Node node = (Node) weakNode.get();
689:
690: if (node != null) {
691: description = MessageFormat.format(
692: getString("ACSD_NullPanel"),
693: new Object[] { node.getDisplayName() });
694: }
695: }
696:
697: return description;
698: }
699: }
700: }
701:
702: /** Table cell renderer component. Paints focus border on property panel. */
703: private static class FocusedPropertyPanel extends PropertyPanel {
704: //XXX delete this class when new property panel is committed
705: boolean focused;
706:
707: public FocusedPropertyPanel(Property p, int preferences) {
708: super (p, preferences);
709: }
710:
711: public void setFocused(boolean focused) {
712: this .focused = focused;
713: }
714:
715: public String getToolTipText() {
716: String super Tooltip = super .getToolTipText();
717: String propertyTooltip = getProperty()
718: .getShortDescription();
719: if (propertyTooltip != null) {
720: return propertyTooltip;
721: } else {
722: return super Tooltip;
723: }
724: }
725:
726: public void addComponentListener(
727: java.awt.event.ComponentListener l) {
728: //do nothing
729: }
730:
731: public void addHierarchyListener(
732: java.awt.event.HierarchyListener l) {
733: //do nothing
734: }
735:
736: public void repaint(long tm, int x, int y, int width, int height) {
737: //do nothing
738: }
739:
740: public void revalidate() {
741: //do nothing
742: }
743:
744: public void firePropertyChange(String s, Object a, Object b) {
745: //do nothing
746: if ("flat".equals(s)) {
747: super .firePropertyChange(s, a, b);
748: }
749: }
750:
751: public boolean isValid() {
752: return true;
753: }
754:
755: public boolean isShowing() {
756: return true;
757: }
758:
759: public void update(Graphics g) {
760: //do nothing
761: }
762:
763: public void paint(Graphics g) {
764: //do this for self-painting editors in Options window - because
765: //we've turned off most property changes, the background won't be
766: //painted correctly otherwise
767: Color c = getBackground();
768: Color old = g.getColor();
769: g.setColor(c);
770: g.fillRect(0, 0, getWidth(), getHeight());
771: g.setColor(old);
772:
773: super .paint(g);
774:
775: if (focused) {
776: Color bdr = UIManager
777: .getColor("Tree.selectionBorderColor"); //NOI18N
778:
779: if (bdr == null) {
780: //Button focus color doesn't work on win classic - better to
781: //get the color from a value we know will work - Tim
782: if (getForeground().equals(Color.BLACK)) { //typical
783: bdr = getBackground().darker();
784: } else {
785: bdr = getForeground().darker();
786: }
787: }
788:
789: g.setColor(bdr);
790: g.drawRect(1, 1, getWidth() - 3, getHeight() - 3);
791: }
792:
793: g.setColor(old);
794: }
795:
796: ////////////////// Accessibility support ///////////////////////////////
797: public AccessibleContext getAccessibleContext() {
798: if (accessibleContext == null) {
799: accessibleContext = new AccessibleFocusedPropertyPanel();
800: }
801:
802: return accessibleContext;
803: }
804:
805: private class AccessibleFocusedPropertyPanel extends
806: AccessibleJComponent {
807: AccessibleFocusedPropertyPanel() {
808: }
809:
810: public AccessibleRole getAccessibleRole() {
811: return AccessibleRole.PANEL;
812: }
813:
814: public String getAccessibleName() {
815: FeatureDescriptor fd = ((ExPropertyModel) getModel())
816: .getFeatureDescriptor();
817: PropertyEditor editor = getPropertyEditor();
818:
819: return MessageFormat
820: .format(
821: getString("ACS_PropertyPanelRenderer"),
822: new Object[] {
823: fd.getDisplayName(),
824: (editor == null) ? getString("CTL_No_value")
825: : editor.getAsText() });
826: }
827:
828: public String getAccessibleDescription() {
829: FeatureDescriptor fd = ((ExPropertyModel) getModel())
830: .getFeatureDescriptor();
831: Node node = (Node) ((ExPropertyModel) getModel())
832: .getBeans()[0];
833: Class clazz = getModel().getPropertyType();
834:
835: return MessageFormat
836: .format(
837: getString("ACSD_PropertyPanelRenderer"),
838: new Object[] {
839: fd.getShortDescription(),
840: (clazz == null) ? getString("CTL_No_type")
841: : clazz.getName(),
842: node.getDisplayName() });
843: }
844: }
845: }
846: }
|