KickJava   Java API By Example, From Geeks To Geeks.

Java > Open Source Codes > org > openide > explorer > view > TableSheetCell


1 /*
2  * The contents of this file are subject to the terms of the Common Development
3  * and Distribution License (the License). You may not use this file except in
4  * compliance with the License.
5  *
6  * You can obtain a copy of the License at http://www.netbeans.org/cddl.html
7  * or http://www.netbeans.org/cddl.txt.
8  *
9  * When distributing Covered Code, include this CDDL Header Notice in each file
10  * and include the License file at http://www.netbeans.org/cddl.txt.
11  * If applicable, add the following below the CDDL Header, with the fields
12  * enclosed by brackets [] replaced by your own identifying information:
13  * "Portions Copyrighted [year] [name of copyright owner]"
14  *
15  * The Original Software is NetBeans. The Initial Developer of the Original
16  * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
17  * Microsystems, Inc. All Rights Reserved.
18  */

19 package org.openide.explorer.view;
20
21 import java.lang.ref.Reference JavaDoc;
22 import org.openide.explorer.propertysheet.*;
23 import org.openide.nodes.Node;
24
25 import org.openide.nodes.Node.Property;
26 import org.openide.util.NbBundle;
27
28 import java.awt.Color JavaDoc;
29 import java.awt.Component JavaDoc;
30 import java.awt.Container JavaDoc;
31 import java.awt.Font JavaDoc;
32 import java.awt.Graphics JavaDoc;
33 import java.awt.KeyboardFocusManager JavaDoc;
34
35 import java.beans.*;
36
37 import java.lang.ref.WeakReference JavaDoc;
38 import java.lang.reflect.InvocationTargetException JavaDoc;
39
40 import java.text.MessageFormat JavaDoc;
41
42 import java.util.EventObject JavaDoc;
43 import java.util.Map JavaDoc;
44 import java.util.StringTokenizer JavaDoc;
45 import java.util.WeakHashMap JavaDoc;
46 import java.util.logging.Level JavaDoc;
47 import java.util.logging.Logger JavaDoc;
48
49 import javax.accessibility.AccessibleContext JavaDoc;
50 import javax.accessibility.AccessibleRole JavaDoc;
51
52 import javax.swing.*;
53 import javax.swing.event.TableModelEvent JavaDoc;
54 import javax.swing.event.TableModelListener JavaDoc;
55 import javax.swing.table.*;
56
57 import org.netbeans.modules.openide.explorer.TTVEnvBridge;
58
59
60 /**
61  * TableCellEditor/Renderer implementation. Component returned is the PropertyPanel
62  *
63  * @author Jan Rojcek
64  */

65 class TableSheetCell extends AbstractCellEditor implements TableModelListener JavaDoc, PropertyChangeListener, TableCellEditor,
66     TableCellRenderer {
67     /* Table sheet cell works only with NodeTableModel */
68     private NodeTableModel tableModel;
69
70     /* Determines how to paint renderer */
71     private Boolean JavaDoc flat;
72
73     //
74
// Editor
75
//
76

77     /** Actually edited node (its property) */
78     private Node node;
79
80     /** Edited property */
81     private Property prop;
82
83     //
84
// Renderer
85
//
86

87     /** Default header renderer */
88     private TableCellRenderer headerRenderer = (new JTableHeader()).getDefaultRenderer();
89
90     /** Null panel is used if cell value is null */
91     private NullPanel nullPanel;
92
93     /** Two-tier cache for property panels
94      * Map<TreeNode, WeakHashMap<Node.Property, Reference<FocusedPropertyPanel>> */

95     private Map JavaDoc panelCache = new WeakHashMap JavaDoc(); // weak! #31275
96
private FocusedPropertyPanel renderer = null;
97     private PropertyPanel editor = null;
98
99     public TableSheetCell(NodeTableModel tableModel) {
100         this.tableModel = tableModel;
101         setFlat(false);
102     }
103
104     /**
105      * Set how to paint renderer.
106      * @param f <code>true</code> means flat, <code>false</code> means with button border
107      */

108     public void setFlat(boolean f) {
109         Color JavaDoc controlDkShadow = Color.lightGray;
110
111         if (UIManager.getColor("controlDkShadow") != null) {
112             controlDkShadow = UIManager.getColor("controlDkShadow"); // NOI18N
113
}
114
115         Color JavaDoc controlLtHighlight = Color.black;
116
117         if (UIManager.getColor("controlLtHighlight") != null) {
118             controlLtHighlight = UIManager.getColor("controlLtHighlight"); // NOI18N
119
}
120
121         Color JavaDoc buttonFocusColor = Color.blue;
122
123         if (UIManager.getColor("Button.focus") != null) {
124             buttonFocusColor = UIManager.getColor("Button.focus"); // NOI18N
125
}
126
127         flat = f ? Boolean.TRUE : Boolean.FALSE;
128     }
129
130     /** Returns <code>null<code>.
131      * @return <code>null</code>
132      */

133     public Object JavaDoc getCellEditorValue() {
134         return null;
135     }
136
137     /** Returns editor of property.
138      * @param table
139      * @param value
140      * @param isSelected
141      * @param r row
142      * @param c column
143      * @return <code>PropertyPanel</code>
144      */

145     public Component JavaDoc getTableCellEditorComponent(JTable table, Object JavaDoc value, boolean isSelected, int r, int c) {
146         prop = (Property) value;
147         node = tableModel.nodeForRow(r);
148         node.addPropertyChangeListener(this);
149         tableModel.addTableModelListener(this);
150
151         // create property panel
152
PropertyPanel propPanel = getEditor(prop, node);
153
154         propPanel.setBackground(table.getSelectionBackground());
155         propPanel.setForeground(table.getSelectionForeground());
156
157         //Fix for 35534, text shifts when editing. Maybe better fix possible
158
//in EditablePropertyDisplayer or InplaceEditorFactory.
159
propPanel.setBorder(BorderFactory.createMatteBorder(0, 1, 0, 0, table.getSelectionBackground()));
160
161         return propPanel;
162     }
163
164     /** Cell should not be selected
165      * @param ev event
166      * @return <code>false</code>
167      */

168     public boolean shouldSelectCell(EventObject JavaDoc ev) {
169         return true;
170     }
171
172     /** Return true.
173      * @param e event
174      * @return <code>true</code>
175      */

176     public boolean isCellEditable(EventObject JavaDoc e) {
177         return true;
178     }
179
180     /** Forwards node property change to property model
181      * @param evt event
182      */

183     public void propertyChange(PropertyChangeEvent evt) {
184         // stopCellEditing(); //XXX ?
185
((NodeTableModel) tableModel).fireTableDataChanged();
186     }
187
188     /**
189      * Detaches listeners.
190      * Calls <code>fireEditingStopped</code> and returns true.
191      * @return true
192      */

193     public boolean stopCellEditing() {
194         if (prop != null) {
195             detachEditor();
196         }
197
198         return super.stopCellEditing();
199     }
200
201     /**
202      * Detaches listeners.
203      * Calls <code>fireEditingCanceled</code>.
204      */

205     public void cancelCellEditing() {
206         if (prop != null) {
207             detachEditor();
208         }
209
210         super.cancelCellEditing();
211     }
212
213     /** Table has changed. If underlied property was switched then cancel editing.
214      * @param e event
215      */

216     public void tableChanged(TableModelEvent JavaDoc e) {
217         cancelCellEditing();
218     }
219
220     /** Removes listeners and frees resources.
221      */

222     private void detachEditor() {
223         node.removePropertyChangeListener(this);
224         tableModel.removeTableModelListener(this);
225         node = null;
226         prop = null;
227     }
228
229     private FocusedPropertyPanel getRenderer(Property p, Node n) {
230         TTVEnvBridge bridge = TTVEnvBridge.getInstance(this);
231         bridge.setCurrentBeans(new Node[] { n });
232
233         if (renderer == null) {
234             renderer = new FocusedPropertyPanel(p, PropertyPanel.PREF_READ_ONLY | PropertyPanel.PREF_TABLEUI);
235             renderer.putClientProperty("beanBridgeIdentifier", this); //NOI18N
236
}
237
238         renderer.setProperty(p);
239         renderer.putClientProperty("flat", Boolean.TRUE);
240
241         return renderer;
242     }
243
244     /** Getter for actual cell renderer.
245      * @param table
246      * @param value
247      * @param isSelected
248      * @param hasFocus
249      * @param row
250      * @param column
251      * @return <code>PropertyPanel</code>
252      */

253     public Component JavaDoc getTableCellRendererComponent(
254         JTable table, Object JavaDoc value, boolean isSelected, boolean hasFocus, int row, int column
255     ) {
256         // Header renderer
257
if (row == -1) {
258             Component JavaDoc comp = headerRenderer.getTableCellRendererComponent(
259                     table, value, isSelected, hasFocus, row, column
260                 );
261
262             if (comp instanceof JComponent) {
263                 String JavaDoc tip = (column > 0) ? tableModel.propertyForColumn(column).getShortDescription()
264                                           : table.getColumnName(0);
265                 ((JComponent) comp).setToolTipText(tip);
266             }
267
268             return comp;
269         }
270
271         Property prop = (Property) value;
272         Node node = tableModel.nodeForRow(row);
273
274         if (prop != null) {
275             FocusedPropertyPanel propPanel = getRenderer(prop, node);
276             propPanel.setFocused(hasFocus);
277
278             String JavaDoc tooltipText = null;
279
280             try {
281                 Object JavaDoc tooltipValue = prop.getValue();
282
283                 if (null != tooltipValue) {
284                     tooltipText = tooltipValue.toString();
285                 }
286             } catch (IllegalAccessException JavaDoc eaE) {
287                 Logger.getLogger(TableSheetCell.class.getName()).log(Level.WARNING, null, eaE);
288             } catch (InvocationTargetException JavaDoc itE) {
289                 Logger.getLogger(TableSheetCell.class.getName()).log(Level.WARNING, null, itE);
290             }
291
292             propPanel.setToolTipText(createHtmlTooltip(tooltipText, propPanel.getFont()));
293             propPanel.setOpaque(true);
294
295             if (isSelected) {
296                 Component JavaDoc focusOwner = KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner();
297
298                 boolean tableHasFocus = (table == focusOwner) || table.isAncestorOf(focusOwner) ||
299                     (focusOwner instanceof Container JavaDoc && ((Container JavaDoc) focusOwner).isAncestorOf(table));
300
301                 if ((table == focusOwner) && table.isEditing()) {
302                     //XXX really need to check if the editor has focus
303
tableHasFocus = true;
304                 }
305
306                 propPanel.setBackground(
307                     tableHasFocus ? table.getSelectionBackground() : TreeTable.getUnfocusedSelectedBackground()
308                 );
309
310                 propPanel.setForeground(
311                     tableHasFocus ? table.getSelectionForeground() : TreeTable.getUnfocusedSelectedForeground()
312                 );
313             } else {
314                 propPanel.setBackground(table.getBackground());
315                 propPanel.setForeground(table.getForeground());
316             }
317
318             return propPanel;
319         }
320
321         if (nullPanel == null) {
322             nullPanel = new NullPanel(node);
323             nullPanel.setOpaque(true);
324         } else {
325             nullPanel.setNode(node);
326         }
327
328         if (isSelected) {
329             Component JavaDoc focusOwner = KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner();
330
331             boolean tableHasFocus = hasFocus || (table == focusOwner) || table.isAncestorOf(focusOwner) ||
332                 (focusOwner instanceof Container JavaDoc && ((Container JavaDoc) focusOwner).isAncestorOf(table));
333
334             nullPanel.setBackground(
335                 tableHasFocus ? table.getSelectionBackground() : TreeTable.getUnfocusedSelectedBackground()
336             );
337
338             //XXX may want to handle inverse theme here and use brighter if
339
//below a threshold. Deferred to centralized color management
340
//being implemented.
341
nullPanel.setForeground(table.getSelectionForeground().darker());
342         } else {
343             nullPanel.setBackground(table.getBackground());
344             nullPanel.setForeground(table.getForeground());
345         }
346
347         nullPanel.setFocused(hasFocus);
348
349         return nullPanel;
350     }
351
352     private PropertyPanel getEditor(Property p, Node n) {
353         int prefs = PropertyPanel.PREF_TABLEUI;
354
355         TTVEnvBridge bridge = TTVEnvBridge.getInstance(this);
356
357         //workaround for issue 38132 - use env bridge to pass the
358
//node to propertypanel so it can call PropertyEnv.setBeans()
359
//with it. The sad thing is almost nobody uses PropertyEnv.getBeans(),
360
//but we have to do it for all cases.
361
bridge.setCurrentBeans(new Node[] { n });
362
363         if (editor == null) {
364             editor = new PropertyPanel(p, prefs);
365
366             editor.putClientProperty("flat", Boolean.TRUE); //NOI18N
367
editor.putClientProperty("beanBridgeIdentifier", this); //NOI18N
368

369             editor.setProperty(p);
370
371             return editor;
372         }
373
374         editor.setProperty(p);
375
376         //Okay, the property panel has already grabbed the beans, clear
377
//them so no references are held.
378
return editor;
379     }
380
381     private static String JavaDoc getString(String JavaDoc key) {
382         return NbBundle.getBundle(TableSheetCell.class).getString(key);
383     }
384
385     /**
386      * HTML-ize a tooltip, splitting long lines. It's package private for unit
387      * testing.
388      */

389     static String JavaDoc createHtmlTooltip(String JavaDoc value, Font JavaDoc font) {
390         if (value == null) {
391             return "null"; // NOI18N
392
}
393
394         // break up massive tooltips
395
String JavaDoc token = null;
396
397         if (value.indexOf(" ") != -1) { //NOI18N
398
token = " "; //NOI18N
399
} else if (value.indexOf(",") != -1) { //NOI18N
400
token = ","; //NOI18N
401
} else if (value.indexOf(";") != -1) { //NOI18N
402
token = ";"; //NOI18N
403
} else if (value.indexOf("/") != -1) { //NOI18N
404
token = "/"; //NOI18N
405
} else if (value.indexOf(">") != -1) { //NOI18N
406
token = ">"; //NOI18N
407
} else if (value.indexOf("\\") != -1) { //NOI18N
408
token = "\\"; //NOI18N
409
} else {
410             //give up
411
return makeDisplayble(value, font);
412         }
413
414         StringTokenizer JavaDoc tk = new StringTokenizer JavaDoc(value, token, true);
415
416         StringBuffer JavaDoc sb = new StringBuffer JavaDoc(value.length() + 20);
417         sb.append("<html>"); //NOI18N
418

419         int charCount = 0;
420         int lineCount = 0;
421
422         while (tk.hasMoreTokens()) {
423             String JavaDoc a = tk.nextToken();
424             a = makeDisplayble(a, font);
425             charCount += a.length();
426             sb.append(a);
427
428             if (tk.hasMoreTokens()) {
429                 charCount++;
430             }
431
432             if (charCount > 80) {
433                 sb.append("<br>"); //NOI18N
434
charCount = 0;
435                 lineCount++;
436
437                 if (lineCount > 10) {
438                     //Don't let things like VCS variables create
439
//a tooltip bigger than the screen. 99% of the
440
//time this is not a problem.
441
sb.append(NbBundle.getMessage(TableSheetCell.class, "MSG_ELLIPSIS")); //NOI18N
442

443                     return sb.toString();
444                 }
445             }
446         }
447
448         sb.append("</html>"); //NOI18N
449

450         return sb.toString();
451     }
452
453     /**
454      * Makes the given String displayble. Probably there doesn't exists
455      * perfect solution for all situation. (someone prefer display those
456      * squares for undisplayable chars, someone unicode placeholders). So lets
457      * try do the best compromise.
458      */

459     private static String JavaDoc makeDisplayble(String JavaDoc str, Font JavaDoc f) {
460         if (null == str) {
461             return str;
462         }
463
464         if (null == f) {
465             f = new JLabel().getFont();
466         }
467
468         StringBuffer JavaDoc buf = new StringBuffer JavaDoc((int) (str.length() * 1.3)); // x -> \u1234
469
char[] chars = str.toCharArray();
470
471         for (int i = 0; i < chars.length; i++) {
472             char c = chars[i];
473
474             switch (c) {
475             case '\t':
476                 buf.append("&nbsp;&nbsp;&nbsp;&nbsp;" + // NOI18N
477
"&nbsp;&nbsp;&nbsp;&nbsp;"
478                 ); // NOI18N
479
break;
480
481             case '\n':
482                 break;
483
484             case '\r':
485                 break;
486
487             case '\b':
488                 buf.append("\\b");
489
490                 break; // NOI18N
491

492             case '\f':
493                 buf.append("\\f");
494
495                 break; // NOI18N
496

497             default:
498
499                 if (!processHtmlEntity(buf, c)) {
500                     if ((null == f) || f.canDisplay(c)) {
501                         buf.append(c);
502                     } else {
503                         buf.append("\\u"); // NOI18N
504

505                         String JavaDoc hex = Integer.toHexString(c);
506
507                         for (int j = 0; j < (4 - hex.length()); j++)
508                             buf.append('0');
509
510                         buf.append(hex);
511                     }
512                 }
513             }
514         }
515
516         return buf.toString();
517     }
518
519     private static boolean processHtmlEntity(StringBuffer JavaDoc buf, char c) {
520         switch (c) {
521         case '>':
522             buf.append("&gt;");
523
524             break; // NOI18N
525

526         case '<':
527             buf.append("&lt;");
528
529             break; // NOI18N
530

531         case '&':
532             buf.append("&amp;");
533
534             break; // NOI18N
535

536         default:
537             return false;
538         }
539
540         return true;
541     }
542
543     private static class NullPanel extends JPanel {
544         private Reference JavaDoc<Node> weakNode;
545         private boolean focused = false;
546
547         NullPanel(Node node) {
548             this.weakNode = new WeakReference JavaDoc<Node>(node);
549         }
550
551         void setNode(Node node) {
552             this.weakNode = new WeakReference JavaDoc<Node>(node);
553         }
554
555         public AccessibleContext JavaDoc getAccessibleContext() {
556             if (accessibleContext == null) {
557                 accessibleContext = new AccessibleNullPanel();
558             }
559
560             return accessibleContext;
561         }
562
563         public void setFocused(boolean val) {
564             focused = val;
565         }
566
567         public void paintComponent(Graphics JavaDoc g) {
568             super.paintComponent(g);
569
570             if (focused) {
571                 Color JavaDoc bdr = UIManager.getColor("Tree.selectionBorderColor"); //NOI18N
572

573                 if (bdr == null) {
574                     //Button focus color doesn't work on win classic - better to
575
//get the color from a value we know will work - Tim
576
if (getForeground().equals(Color.BLACK)) { //typical
577
bdr = getBackground().darker();
578                     } else {
579                         bdr = getForeground().darker();
580                     }
581                 }
582
583                 g.setColor(bdr);
584                 g.drawRect(1, 1, getWidth() - 3, getHeight() - 3);
585                 g.setColor(bdr);
586             }
587         }
588
589         public void addComponentListener(java.awt.event.ComponentListener JavaDoc l) {
590             //do nothing
591
}
592
593         public void addHierarchyListener(java.awt.event.HierarchyListener JavaDoc l) {
594             //do nothing
595
}
596
597         public void repaint() {
598             //do nothing
599
}
600
601         public void repaint(int x, int y, int width, int height) {
602             //do nothing
603
}
604
605         public void invalidate() {
606             //do nothing
607
}
608
609         public void revalidate() {
610             //do nothing
611
}
612
613         public void validate() {
614             //do nothing
615
}
616
617         public void firePropertyChange(String JavaDoc s, Object JavaDoc a, Object JavaDoc b) {
618             //do nothing
619
}
620
621         private class AccessibleNullPanel extends AccessibleJPanel {
622             AccessibleNullPanel() {
623             }
624
625             public String JavaDoc getAccessibleName() {
626                 String JavaDoc name = super.getAccessibleName();
627
628                 if (name == null) {
629                     name = getString("ACS_NullPanel");
630                 }
631
632                 return name;
633             }
634
635             public String JavaDoc getAccessibleDescription() {
636                 String JavaDoc description = super.getAccessibleDescription();
637
638                 if (description == null) {
639                     Node node = (Node) weakNode.get();
640
641                     if (node != null) {
642                         description = MessageFormat.format(
643                                 getString("ACSD_NullPanel"), new Object JavaDoc[] { node.getDisplayName() }
644                             );
645                     }
646                 }
647
648                 return description;
649             }
650         }
651     }
652
653     /** Table cell renderer component. Paints focus border on property panel. */
654     private static class FocusedPropertyPanel extends PropertyPanel {
655         //XXX delete this class when new property panel is committed
656
boolean focused;
657
658         public FocusedPropertyPanel(Property p, int preferences) {
659             super(p, preferences);
660         }
661
662         public void setFocused(boolean focused) {
663             this.focused = focused;
664         }
665
666         public String JavaDoc getToolTipText() {
667             String JavaDoc superTooltip = super.getToolTipText();
668             String JavaDoc propertyTooltip = getProperty().getShortDescription();
669             if (propertyTooltip != null) {
670                 return propertyTooltip;
671             } else {
672                 return superTooltip;
673             }
674         }
675         
676         public void addComponentListener(java.awt.event.ComponentListener JavaDoc l) {
677             //do nothing
678
}
679
680         public void addHierarchyListener(java.awt.event.HierarchyListener JavaDoc l) {
681             //do nothing
682
}
683
684         public void repaint(long tm, int x, int y, int width, int height) {
685             //do nothing
686
}
687
688         public void revalidate() {
689             //do nothing
690
}
691
692         public void firePropertyChange(String JavaDoc s, Object JavaDoc a, Object JavaDoc b) {
693             //do nothing
694
if ("flat".equals(s)) {
695                 super.firePropertyChange(s, a, b);
696             }
697         }
698
699         public boolean isValid() {
700             return true;
701         }
702
703         public boolean isShowing() {
704             return true;
705         }
706
707         public void update(Graphics JavaDoc g) {
708             //do nothing
709
}
710
711         public void paint(Graphics JavaDoc g) {
712             //do this for self-painting editors in Options window - because
713
//we've turned off most property changes, the background won't be
714
//painted correctly otherwise
715
Color JavaDoc c = getBackground();
716             Color JavaDoc old = g.getColor();
717             g.setColor(c);
718             g.fillRect(0, 0, getWidth(), getHeight());
719             g.setColor(old);
720
721             super.paint(g);
722
723             if (focused) {
724                 Color JavaDoc bdr = UIManager.getColor("Tree.selectionBorderColor"); //NOI18N
725

726                 if (bdr == null) {
727                     //Button focus color doesn't work on win classic - better to
728
//get the color from a value we know will work - Tim
729
if (getForeground().equals(Color.BLACK)) { //typical
730
bdr = getBackground().darker();
731                     } else {
732                         bdr = getForeground().darker();
733                     }
734                 }
735
736                 g.setColor(bdr);
737                 g.drawRect(1, 1, getWidth() - 3, getHeight() - 3);
738             }
739
740             g.setColor(old);
741         }
742
743         ////////////////// Accessibility support ///////////////////////////////
744
public AccessibleContext JavaDoc getAccessibleContext() {
745             if (accessibleContext == null) {
746                 accessibleContext = new AccessibleFocusedPropertyPanel();
747             }
748
749             return accessibleContext;
750         }
751
752         private class AccessibleFocusedPropertyPanel extends AccessibleJComponent {
753             AccessibleFocusedPropertyPanel() {
754             }
755
756             public AccessibleRole JavaDoc getAccessibleRole() {
757                 return AccessibleRole.PANEL;
758             }
759
760             public String JavaDoc getAccessibleName() {
761                 FeatureDescriptor fd = ((ExPropertyModel) getModel()).getFeatureDescriptor();
762                 PropertyEditor editor = getPropertyEditor();
763
764                 return MessageFormat.format(
765                     getString("ACS_PropertyPanelRenderer"),
766                     new Object JavaDoc[] { fd.getDisplayName(), (editor == null) ? getString("CTL_No_value") : editor.getAsText() }
767                 );
768             }
769
770             public String JavaDoc getAccessibleDescription() {
771                 FeatureDescriptor fd = ((ExPropertyModel) getModel()).getFeatureDescriptor();
772                 Node node = (Node) ((ExPropertyModel) getModel()).getBeans()[0];
773                 Class JavaDoc clazz = getModel().getPropertyType();
774
775                 return MessageFormat.format(
776                     getString("ACSD_PropertyPanelRenderer"),
777                     new Object JavaDoc[] {
778                         fd.getShortDescription(), (clazz == null) ? getString("CTL_No_type") : clazz.getName(),
779                         node.getDisplayName()
780                     }
781                 );
782             }
783         }
784     }
785 }
786
Popular Tags