KickJava   Java API By Example, From Geeks To Geeks.

Java > Open Source Codes > org > lobobrowser > html > renderer > RLine


1 /*
2     GNU LESSER GENERAL PUBLIC LICENSE
3     Copyright (C) 2006 The Lobo Project
4
5     This library is free software; you can redistribute it and/or
6     modify it under the terms of the GNU Lesser General Public
7     License as published by the Free Software Foundation; either
8     version 2.1 of the License, or (at your option) any later version.
9
10     This library is distributed in the hope that it will be useful,
11     but WITHOUT ANY WARRANTY; without even the implied warranty of
12     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13     Lesser General Public License for more details.
14
15     You should have received a copy of the GNU Lesser General Public
16     License along with this library; if not, write to the Free Software
17     Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
18
19     Contact info: xamjadmin@users.sourceforge.net
20 */

21 /*
22  * Created on Apr 16, 2005
23  */

24 package org.lobobrowser.html.renderer;
25
26 import java.awt.*;
27 import java.util.*;
28
29 import org.lobobrowser.html.domimpl.ModelNode;
30 import org.lobobrowser.html.style.RenderState;
31
32 /**
33  * @author J. H. S.
34  */

35 class RLine extends BaseRCollection {
36     private final ArrayList renderables = new ArrayList(8);
37     //private final RenderState startRenderState;
38
private int baseLineOffset;
39     private int desiredMaxWidth;
40     
41     public RLine(ModelNode modelNode, RenderableContainer container, int x, int y, int desiredMaxWidth, int height) {
42         // Note that in the case of RLine, modelNode is the context node
43
// at the beginning of the line, not a node that encloses the whole line.
44
super(container, modelNode);
45         this.x = x;
46         this.y = y;
47         this.width = 0;
48         this.height = height;
49         this.desiredMaxWidth = desiredMaxWidth;
50         // Layout here can always be "invalidated"
51
this.layoutUpTreeCanBeInvalidated = true;
52     }
53     
54     public int getBaselineOffset() {
55         return this.baseLineOffset;
56     }
57     
58     protected void invalidateLayoutLocal() {
59         // Workaround for fact that RBlockViewport does not
60
// get validated or invalidated.
61
this.layoutUpTreeCanBeInvalidated = true;
62     }
63     
64     /* (non-Javadoc)
65      * @see net.sourceforge.xamj.domimpl.markup.Renderable#paint(java.awt.Graphics)
66      */

67
68     public void paint(Graphics g) {
69         // Paint according to render state of the start of line first.
70
RenderState rs = this.modelNode.getRenderState();
71         if(rs != null) {
72             Color textColor = rs.getColor();
73             g.setColor(textColor);
74             Font font = rs.getFont();
75             g.setFont(font);
76         }
77         // Note that partial paints of the line can only be done
78
// if all RStyleChanger's are applied first.
79
Iterator i = this.renderables.iterator();
80         if(i != null) {
81             while(i.hasNext()) {
82                 Object JavaDoc r = i.next();
83                 if(r instanceof RElement) {
84                     // RElement's should be clipped.
85
RElement relement = (RElement) r;
86                     Graphics newG = g.create(relement.getX(), relement.getY(), relement.getWidth(), relement.getHeight());
87                     try {
88                         relement.paint(newG);
89                     } finally {
90                         newG.dispose();
91                     }
92                 }
93                 else if(r instanceof BoundableRenderable) {
94                     BoundableRenderable br = (BoundableRenderable) r;
95                     br.paintTranslated(g);
96                 }
97                 else {
98                     ((Renderable) r).paint(g);
99                 }
100             }
101         }
102     }
103         
104     public final void addStyleChanger(RStyleChanger sc) {
105         this.renderables.add(sc);
106     }
107     
108     /**
109      * This method adds and positions a renderable in the line, if possible.
110      * Note that RLine does not set sizes, but only origins.
111      * @throws OverflowException Thrown if the renderable overflows the line. All overflowing renderables are added to the exception.
112      */

113     public final void add(Renderable renderable, boolean allowOverflow) throws OverflowException {
114         if(renderable instanceof RWord) {
115             this.addWord((RWord) renderable, allowOverflow);
116         }
117         else if(renderable instanceof RBlank) {
118             this.addBlank((RBlank) renderable);
119         }
120         else if(renderable instanceof RElement) {
121             this.addElement((RElement) renderable, allowOverflow);
122         }
123         else if(renderable instanceof RSpacing) {
124             this.addSpacing((RSpacing) renderable);
125         }
126         else if(renderable instanceof RStyleChanger) {
127             this.addStyleChanger((RStyleChanger) renderable);
128         }
129         else {
130             throw new IllegalArgumentException JavaDoc("Can't add " + renderable);
131         }
132     }
133     
134     public final void addWord(RWord rword, boolean allowOverflow) throws OverflowException {
135         // Check if it fits horzizontally
136
int offset = this.width;
137         int wiwidth = rword.width;
138         if(!allowOverflow && (offset != 0 && offset + wiwidth > this.desiredMaxWidth)) {
139             ArrayList renderables = this.renderables;
140             ArrayList overflow = null;
141             boolean cancel = false;
142             // Check if other words need to be overflown (for example,
143
// a word just before a markup tag adjacent to the word
144
// we're trying to add). This is basically what RBlank is
145
// useful for.
146
for(int i = renderables.size(); --i >= 0;) {
147                 Renderable renderable = (Renderable) renderables.get(i);
148                 if(renderable instanceof RWord || !(renderable instanceof BoundableRenderable)) {
149                     if(overflow == null) {
150                         overflow = new ArrayList();
151                     }
152                     if(renderable != rword && renderable instanceof RWord && ((RWord) renderable).getX() == 0) {
153                         // Can't overflow words starting at offset zero.
154
// Note that all or none should be overflown.
155
cancel = true;
156                         // No need to set offset - set later.
157
break;
158                     }
159                     if(renderable instanceof RWord) {
160                         int newOffset = ((RWord) renderable).getBounds().x;
161                         this.width = newOffset;
162                     }
163                     overflow.add(0, renderable);
164                     renderables.remove(i);
165                 }
166                 else {
167                     break;
168                 }
169             }
170             if(!cancel) {
171                 if(overflow == null) {
172                     throw new OverflowException(Collections.singleton(rword));
173                 }
174                 else {
175                     overflow.add(rword);
176                     throw new OverflowException(overflow);
177                 }
178             }
179         }
180
181         // Add it
182

183         int extraHeight = 0;
184         int maxDescent = this.height - this.baseLineOffset;
185         if(rword.descent > maxDescent) {
186             extraHeight += (rword.descent - maxDescent);
187         }
188         int maxAscentPlusLeading = this.baseLineOffset;
189         if(rword.ascentPlusLeading > maxAscentPlusLeading) {
190             extraHeight += (rword.ascentPlusLeading - maxAscentPlusLeading);
191         }
192         if(extraHeight > 0) {
193             int newHeight = this.height + extraHeight;
194             this.adjustHeight(newHeight, newHeight, RElement.VALIGN_ABSBOTTOM);
195         }
196         this.renderables.add(rword);
197         rword.setParent(this);
198         int x = offset;
199         offset += wiwidth;
200         this.width = offset;
201         rword.setOrigin(x, this.baseLineOffset - rword.ascentPlusLeading);
202     }
203
204     public final void addBlank(RBlank rblank) {
205         //NOTE: Blanks may be added without concern for wrapping (?)
206
int x = this.width;
207         int width = rblank.width;
208         rblank.setOrigin(x, this.baseLineOffset - rblank.ascentPlusLeading);
209         this.renderables.add(rblank);
210         rblank.setParent(this);
211         this.width = x + width;
212     }
213
214     public final void addSpacing(RSpacing rblank) {
215         //NOTE: Spacing may be added without concern for wrapping (?)
216
int x = this.width;
217         int width = rblank.width;
218         rblank.setOrigin(x, (this.height - rblank.height) / 2);
219         this.renderables.add(rblank);
220         rblank.setParent(this);
221         this.width = x + width;
222     }
223
224     /**
225      *
226      * @param relement
227      * @param x
228      * @param elementHeight The required new line height.
229      * @param valign
230      */

231     private final void setElementY(RElement relement, int elementHeight, int valign) {
232         // At this point height should be more than what's needed.
233
int yoffset;
234         switch(valign) {
235         case RElement.VALIGN_ABSBOTTOM:
236             yoffset = this.height - elementHeight;
237             break;
238         case RElement.VALIGN_ABSMIDDLE:
239             yoffset = (this.height - elementHeight) / 2;
240             break;
241         case RElement.VALIGN_BASELINE:
242         case RElement.VALIGN_BOTTOM:
243             yoffset = this.baseLineOffset - elementHeight;
244             break;
245         case RElement.VALIGN_MIDDLE:
246             yoffset = this.baseLineOffset - elementHeight / 2;
247             break;
248         case RElement.VALIGN_TOP:
249             yoffset = 0;
250             break;
251         default:
252             yoffset = this.baseLineOffset - elementHeight;
253         }
254         //RLine only sets origins, not sizes.
255
//relement.setBounds(x, yoffset, width, height);
256
relement.setY(yoffset);
257     }
258     
259     public final void addElement(RElement relement, boolean allowOverflow) throws OverflowException {
260         // Check if it fits horizontally
261
int boundsw = this.width;
262         int desiredMaxWidth = this.desiredMaxWidth;
263 // int componentHeight = this.availHeight;
264
// (already layed out)
265
// relement.layout(desiredMaxWidth, componentHeight);
266
int pw = relement.getWidth();
267         int offset = boundsw;
268         if(!allowOverflow && (offset != 0 && offset + pw > desiredMaxWidth)) {
269             throw new OverflowException(Collections.singleton(relement));
270         }
271         //Note: Renderable for widget doesn't paint the widget, but
272
//it's needed for height readjustment.
273
int boundsh = this.height;
274         int ph = relement.getHeight();
275         int requiredHeight;
276         int valign = relement.getVAlign();
277         switch(valign) {
278         case RElement.VALIGN_BASELINE:
279         case RElement.VALIGN_BOTTOM:
280             requiredHeight = ph + (boundsh - this.baseLineOffset);
281             break;
282         case RElement.VALIGN_MIDDLE:
283             requiredHeight = Math.max(ph, ph / 2 + (boundsh - this.baseLineOffset));
284             break;
285         default:
286             requiredHeight = ph;
287             break;
288         }
289         if(requiredHeight > boundsh) {
290             // Height adjustment depends on bounds being already set.
291
this.adjustHeight(requiredHeight, ph, valign);
292         }
293         this.renderables.add(relement);
294         relement.setParent(this);
295         relement.setX(offset);
296         this.setElementY(relement, ph, valign);
297         int newX = offset + pw;
298         this.width = newX;
299     }
300
301 // /**
302
// * Positions line elements vertically.
303
// */
304
// public final void positionVertically() {
305
// ArrayList renderables = this.renderables;
306
//
307
// // Find word maximum metrics.
308
// int maxDescent = 0;
309
// int maxAscentPlusLeading = 0;
310
// int maxWordHeight = 0;
311
// for(Iterator i = renderables.iterator(); i.hasNext(); ) {
312
// Renderable r = (Renderable) i.next();
313
// if(r instanceof RWord) {
314
// RWord rword = (RWord) r;
315
// int descent = rword.descent;
316
// if(descent > maxDescent) {
317
// maxDescent = descent;
318
// }
319
// int ascentPlusLeading = rword.ascentPlusLeading;
320
// if(ascentPlusLeading > maxAscentPlusLeading) {
321
// maxAscentPlusLeading = ascentPlusLeading;
322
// }
323
// if(rword.height > maxWordHeight) {
324
// maxWordHeight = rword.height;
325
// }
326
// }
327
// }
328
//
329
// // Determine proper baseline
330
// int lineHeight = this.height;
331
// int baseLine = lineHeight - maxDescent;
332
// for(Iterator i = renderables.iterator(); i.hasNext(); ) {
333
// Renderable r = (Renderable) i.next();
334
// if(r instanceof RElement) {
335
// RElement relement = (RElement) r;
336
// switch(relement.getVAlign()) {
337
// case RElement.VALIGN_ABSBOTTOM:
338
// //TODO
339
// break;
340
// case RElement.VALIGN_ABSMIDDLE:
341
// int midWord = baseLine + maxDescent - maxWordHeight / 2;
342
// int halfElementHeight = relement.getHeight() / 2;
343
// if(midWord + halfElementHeight > lineHeight) {
344
// // Change baseLine
345
// midWord = lineHeight - halfElementHeight;
346
// baseLine = midWord + maxWordHeight / 2 - maxDescent;
347
// }
348
// else if(midWord - halfElementHeight < 0) {
349
// midWord = halfElementHeight;
350
// baseLine = midWord + maxWordHeight / 2 - maxDescent;
351
// }
352
// else {
353
// relement.setY(midWord - halfElementHeight);
354
// }
355
// break;
356
// }
357
// }
358
// }
359
//
360
// }
361

362     /**
363      * Rearrange line elements based on a new line height and
364      * alignment provided. All line elements are expected to
365      * have bounds preset.
366      * @param newHeight
367      * @param alignmentY
368      */

369     private void adjustHeight(int newHeight, int elementHeight, int valign) {
370         // Set new line height
371
//int oldHeight = this.height;
372
this.height = newHeight;
373         ArrayList renderables = this.renderables;
374         // Find max baseline
375
FontMetrics firstFm = this.modelNode.getRenderState().getFontMetrics();
376         int maxDescent = firstFm.getDescent();
377         int maxAscentPlusLeading = firstFm.getAscent() + firstFm.getLeading();
378         for(Iterator i = renderables.iterator(); i.hasNext();) {
379             Object JavaDoc r = i.next();
380             if(r instanceof RStyleChanger) {
381                 RStyleChanger rstyleChanger = (RStyleChanger) r;
382                 FontMetrics fm = rstyleChanger.getModelNode().getRenderState().getFontMetrics();
383                 int descent = fm.getDescent();
384                 if(descent > maxDescent) {
385                     maxDescent = descent;
386                 }
387                 int ascentPlusLeading = fm.getAscent() + fm.getLeading();
388                 if(ascentPlusLeading > maxAscentPlusLeading) {
389                     maxAscentPlusLeading = ascentPlusLeading;
390                 }
391             }
392         }
393         int textHeight = maxDescent + maxAscentPlusLeading;
394         
395         //TODO: Need to take into account previous RElement's and
396
//their alignments?
397

398         int baseline;
399         switch(valign) {
400         case RElement.VALIGN_ABSBOTTOM:
401             baseline = newHeight - maxDescent;
402             break;
403         case RElement.VALIGN_ABSMIDDLE:
404             baseline = (newHeight + textHeight) / 2 - maxDescent;
405             break;
406         case RElement.VALIGN_BASELINE:
407         case RElement.VALIGN_BOTTOM:
408             baseline = elementHeight;
409             break;
410         case RElement.VALIGN_MIDDLE:
411             baseline = newHeight / 2;
412             break;
413         case RElement.VALIGN_TOP:
414             baseline = maxAscentPlusLeading;
415             break;
416         default:
417             baseline = elementHeight;
418             break;
419         }
420         this.baseLineOffset = baseline;
421         
422         // Change bounds of renderables accordingly
423
for(Iterator i = renderables.iterator(); i.hasNext();) {
424             Object JavaDoc r = i.next();
425             if(r instanceof RWord) {
426                 RWord rword = (RWord) r;
427                 rword.setY(baseline - rword.ascentPlusLeading);
428             }
429             else if(r instanceof RBlank) {
430                 RBlank rblank = (RBlank) r;
431                 rblank.setY(baseline - rblank.ascentPlusLeading);
432             }
433             else if(r instanceof RElement) {
434                 RElement relement = (RElement) r;
435                 //int w = relement.getWidth();
436
this.setElementY(relement, relement.getHeight(), relement.getVAlign());
437             }
438             else {
439                 // RSpacing and RStyleChanger don't matter?
440
}
441         }
442         //TODO: Could throw OverflowException when we add floating widgets
443
}
444     
445     public boolean onMouseClick(java.awt.event.MouseEvent JavaDoc event, int x, int y) {
446         Renderable[] rarray = (Renderable[]) this.renderables.toArray(Renderable.EMPTY_ARRAY);
447         BoundableRenderable r = MarkupUtilities.findRenderable(rarray, x, y, false);
448         if(r != null) {
449             Rectangle rbounds = r.getBounds();
450             return r.onMouseClick(event, x - rbounds.x, y - rbounds.y);
451         }
452         else {
453             return true;
454         }
455     }
456     
457     public boolean onDoubleClick(java.awt.event.MouseEvent JavaDoc event, int x, int y) {
458         Renderable[] rarray = (Renderable[]) this.renderables.toArray(Renderable.EMPTY_ARRAY);
459         BoundableRenderable r = MarkupUtilities.findRenderable(rarray, x, y, false);
460         if(r != null) {
461             Rectangle rbounds = r.getBounds();
462             return r.onDoubleClick(event, x - rbounds.x, y - rbounds.y);
463         }
464         else {
465             return true;
466         }
467     }
468     
469     private BoundableRenderable mousePressTarget;
470     
471     public boolean onMousePressed(java.awt.event.MouseEvent JavaDoc event, int x, int y) {
472         Renderable[] rarray = (Renderable[]) this.renderables.toArray(Renderable.EMPTY_ARRAY);
473         BoundableRenderable r = MarkupUtilities.findRenderable(rarray, x, y, false);
474         if(r != null) {
475             this.mousePressTarget = r;
476             Rectangle rbounds = r.getBounds();
477             return r.onMousePressed(event, x - rbounds.x, y - rbounds.y);
478         }
479         else {
480             return true;
481         }
482     }
483     
484     public RenderableSpot getLowestRenderableSpot(int x, int y) {
485         Renderable[] rarray = (Renderable[]) this.renderables.toArray(Renderable.EMPTY_ARRAY);
486         BoundableRenderable br = MarkupUtilities.findRenderable(rarray, x, y, false);
487         if(br != null) {
488             Rectangle rbounds = br.getBounds();
489             return br.getLowestRenderableSpot(x - rbounds.x, y - rbounds.y);
490         }
491         else {
492             return new RenderableSpot(this, x, y);
493         }
494     }
495
496     public boolean onMouseReleased(java.awt.event.MouseEvent JavaDoc event, int x, int y) {
497         Renderable[] rarray = (Renderable[]) this.renderables.toArray(Renderable.EMPTY_ARRAY);
498         BoundableRenderable r = MarkupUtilities.findRenderable(rarray, x, y, false);
499         if(r != null) {
500             Rectangle rbounds = r.getBounds();
501             BoundableRenderable oldArmedRenderable = this.mousePressTarget;
502             if(oldArmedRenderable != null && r != oldArmedRenderable) {
503                 oldArmedRenderable.onMouseDisarmed(event);
504                 this.mousePressTarget = null;
505             }
506             return r.onMouseReleased(event, x - rbounds.x, y - rbounds.y);
507         }
508         else {
509             BoundableRenderable oldArmedRenderable = this.mousePressTarget;
510             if(oldArmedRenderable != null) {
511                 oldArmedRenderable.onMouseDisarmed(event);
512                 this.mousePressTarget = null;
513             }
514             return true;
515         }
516     }
517     
518     public boolean onMouseDisarmed(java.awt.event.MouseEvent JavaDoc event) {
519         BoundableRenderable target = this.mousePressTarget;
520         if(target != null) {
521             this.mousePressTarget = null;
522             return target.onMouseDisarmed(event);
523         }
524         else {
525             return true;
526         }
527     }
528     
529     public Color getBlockBackgroundColor() {
530         return this.container.getPaintedBackgroundColor();
531     }
532     
533     public final void adjustHorizontalBounds(int newX, int newMaxWidth) throws OverflowException {
534         this.x = newX;
535         this.desiredMaxWidth = newMaxWidth;
536         int topX = newX + newMaxWidth;
537         ArrayList renderables = this.renderables;
538         int size = renderables.size();
539         ArrayList overflown = null;
540         Rectangle lastInLine = null;
541         for(int i = 0; i < size; i++) {
542             Object JavaDoc r = renderables.get(i);
543             if(overflown == null) {
544                 if(r instanceof BoundableRenderable) {
545                     BoundableRenderable br = (BoundableRenderable) r;
546                     Rectangle brb = br.getBounds();
547                     int x2 = brb.x + brb.width;
548                     if(x2 > topX) {
549                         overflown = new ArrayList(1);
550                     }
551                     else {
552                         lastInLine = brb;
553                     }
554                 }
555             }
556             /* must not be else here */
557             if(overflown != null) {
558                 //TODO: This could break a word across markup boundary.
559
overflown.add(r);
560                 renderables.remove(i--);
561                 size--;
562             }
563         }
564         if(overflown != null) {
565             if(lastInLine != null) {
566                 this.width = lastInLine.x + lastInLine.width;
567             }
568             throw new OverflowException(overflown);
569         }
570     }
571
572 // /* (non-Javadoc)
573
// * @see org.xamjwg.html.renderer.BoundableRenderable#paintSelection(java.awt.Graphics, boolean, org.xamjwg.html.renderer.RenderablePoint, org.xamjwg.html.renderer.RenderablePoint)
574
// */
575
// public boolean paintSelection(Graphics g, boolean inSelection, RenderableSpot startPoint, RenderableSpot endPoint) {
576
// Iterator i = this.renderables.iterator();
577
// if(!inSelection) {
578
// BoundableRenderable startR = startPoint.renderable;
579
// BoundableRenderable endR = endPoint.renderable;
580
// while(i.hasNext()) {
581
// Object r = i.next();
582
// if(r instanceof RElement || r == startR || r == endR) {
583
// BoundableRenderable br = (BoundableRenderable) r;
584
// Rectangle bounds = br.getBounds();
585
// int offsetX = bounds.x;
586
// int offsetY = bounds.y;
587
// g.translate(offsetX, offsetY);
588
// //Graphics newG = g.create(bounds.x, bounds.y, bounds.width, bounds.height);
589
// try {
590
// boolean newInSelection = br.paintSelection(g, inSelection, startPoint, endPoint);
591
// if(newInSelection != inSelection || r == startR || r == endR) {
592
// inSelection = newInSelection;
593
// break;
594
// }
595
// } finally {
596
// g.translate(-offsetX, -offsetY);
597
// //newG.dispose();
598
// }
599
// }
600
// }
601
// }
602
// if(inSelection) {
603
// //TODO: Could be optimized by just scanning
604
// //for renderable and painting at the line level.
605
// while(i.hasNext()) {
606
// Object r = i.next();
607
// if(r instanceof BoundableRenderable) {
608
// BoundableRenderable br = (BoundableRenderable) r;
609
// Rectangle bounds = br.getBounds();
610
// int offsetX = bounds.x;
611
// int offsetY = bounds.y;
612
// g.translate(offsetX, offsetY);
613
// try {
614
// if(!br.paintSelection(g, inSelection, startPoint, endPoint)) {
615
// inSelection = false;
616
// break;
617
// }
618
// } finally {
619
// g.translate(-offsetX, -offsetY);
620
// }
621
// }
622
// }
623
// }
624
// return inSelection;
625
// }
626
//
627
// public boolean extractSelectionText(StringBuffer buffer, boolean inSelection, RenderableSpot startPoint, RenderableSpot endPoint) {
628
// // Optimized override for RLine?
629
// Iterator i = this.renderables.iterator();
630
// if(!inSelection) {
631
// BoundableRenderable startR = startPoint.renderable;
632
// BoundableRenderable endR = endPoint.renderable;
633
// while(i.hasNext()) {
634
// Object r = i.next();
635
// if(r instanceof RElement || r == startR || r == endR) {
636
// BoundableRenderable br = (BoundableRenderable) r;
637
// boolean newInSelection = br.extractSelectionText(buffer, inSelection, startPoint, endPoint);
638
// if(newInSelection != inSelection || r == startR || r == endR) {
639
// inSelection = newInSelection;
640
// break;
641
// }
642
// }
643
// }
644
// }
645
// if(inSelection) {
646
// //TODO: Could be optimized by just scanning
647
// //for renderable and painting at the line level.
648
// while(i.hasNext()) {
649
// Object r = i.next();
650
// if(r instanceof BoundableRenderable) {
651
// BoundableRenderable br = (BoundableRenderable) r;
652
// if(!br.extractSelectionText(buffer, inSelection, startPoint, endPoint)) {
653
// inSelection = false;
654
// break;
655
// }
656
// }
657
// }
658
// }
659
// return inSelection;
660
// }
661
//
662
/* (non-Javadoc)
663      * @see org.xamjwg.html.renderer.RCollection#getRenderables()
664      */

665     public Iterator getRenderables() {
666         return this.renderables.iterator();
667     }
668 }
669
Popular Tags