Reviewed by Darin.
[WebKit-https.git] / WebCore / khtml / rendering / render_list.cpp
1 /**
2  * This file is part of the DOM implementation for KDE.
3  *
4  * Copyright (C) 1999 Lars Knoll (knoll@kde.org)
5  *           (C) 1999 Antti Koivisto (koivisto@kde.org)
6  * Copyright (C) 2003 Apple Computer, Inc.
7  *
8  * This library is free software; you can redistribute it and/or
9  * modify it under the terms of the GNU Library General Public
10  * License as published by the Free Software Foundation; either
11  * version 2 of the License, or (at your option) any later version.
12  *
13  * This library is distributed in the hope that it will be useful,
14  * but WITHOUT ANY WARRANTY; without even the implied warranty of
15  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
16  * Library General Public License for more details.
17  *
18  * You should have received a copy of the GNU Library General Public License
19  * along with this library; see the file COPYING.LIB.  If not, write to
20  * the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
21  * Boston, MA 02111-1307, USA.
22  *
23  */
24
25 #include "render_list.h"
26 #include "rendering/render_canvas.h"
27
28 #include "xml/dom_docimpl.h"
29 #include "misc/htmltags.h"
30
31 #include <qpainter.h>
32
33 #include "misc/helper.h"
34
35 #include <kdebug.h>
36
37 //#define BOX_DEBUG
38
39 using DOM::DocumentImpl;
40 using namespace khtml;
41
42 const int cMarkerPadding = 7;
43
44 static QString toRoman( int number, bool upper )
45 {
46     QString roman;
47     QChar ldigits[] = { 'i', 'v', 'x', 'l', 'c', 'd', 'm' };
48     QChar udigits[] = { 'I', 'V', 'X', 'L', 'C', 'D', 'M' };
49     QChar *digits = upper ? udigits : ldigits;
50     int i, d = 0;
51
52     do
53     {
54         int num = number % 10;
55
56         if ( num % 5 < 4 )
57             for ( i = num % 5; i > 0; i-- )
58                 roman.insert( 0, digits[ d ] );
59
60         if ( num >= 4 && num <= 8)
61             roman.insert( 0, digits[ d+1 ] );
62
63         if ( num == 9 )
64             roman.insert( 0, digits[ d+2 ] );
65
66         if ( num % 5 == 4 )
67             roman.insert( 0, digits[ d ] );
68
69         number /= 10;
70         d += 2;
71     }
72     while ( number );
73
74     return roman;
75 }
76
77 static QString toLetter( int number, int base ) {
78     number--;
79     QString letter = (QChar) (base + (number % 26));
80     // Add a single quote at the end of the alphabet.
81     for (int i = 0; i < (number / 26); i++) {
82        letter += '\'';
83     }
84     return letter;
85 }
86
87 static QString toHebrew( int number ) {
88     const QChar tenDigit[] = {1497, 1499, 1500, 1502, 1504, 1505, 1506, 1508, 1510};
89
90     QString letter;
91     if (number>999) {
92         letter = toHebrew(number/1000) + QString::fromLatin1("'");
93         number = number%1000;
94     }
95
96     int hunderts = (number/400);
97     if (hunderts > 0) {
98         for(int i=0; i<hunderts; i++) {
99             letter += QChar(1511 + 3);
100         }
101     }
102     number = number % 400;
103     if ((number / 100) != 0) {
104         letter += QChar (1511 + (number / 100) -1);
105     }
106     number = number % 100;
107     int tens = number/10;
108     if (tens > 0 && !(number == 15 || number == 16)) {
109         letter += tenDigit[tens-1];
110     }
111     if (number == 15 || number == 16) { // special because of religious
112         letter += QChar(1487 + 9);       // reasons
113         letter += QChar(1487 + number - 9);
114     } else {
115         number = number % 10;
116         if (number != 0) {
117             letter += QChar (1487 + number);
118         }
119     }
120     return letter;
121 }
122
123 // -------------------------------------------------------------------------
124
125 RenderListItem::RenderListItem(DOM::NodeImpl* node)
126     : RenderBlock(node), _notInList(false)
127 {
128     // init RenderObject attributes
129     setInline(false);   // our object is not Inline
130
131     predefVal = -1;
132     m_marker = 0;
133 }
134
135 void RenderListItem::setStyle(RenderStyle *_style)
136 {
137     RenderBlock::setStyle(_style);
138
139     if (style()->listStyleType() != LNONE ||
140         (style()->listStyleImage() && !style()->listStyleImage()->isErrorImage())) {
141         RenderStyle *newStyle = new (renderArena()) RenderStyle();
142         newStyle->ref();
143         // The marker always inherits from the list item, regardless of where it might end
144         // up (e.g., in some deeply nested line box).  See CSS3 spec.
145         newStyle->inheritFrom(style()); 
146         if (!m_marker) {
147             m_marker = new (renderArena()) RenderListMarker(document());
148             m_marker->setStyle(newStyle);
149             m_marker->setListItem(this);
150         }
151         else
152             m_marker->setStyle(newStyle);
153         newStyle->deref(renderArena());
154     } else if (m_marker) {
155         m_marker->detach();
156         m_marker = 0;
157     }
158 }
159
160 RenderListItem::~RenderListItem()
161 {
162 }
163
164 void RenderListItem::detach()
165 {    
166     if (m_marker) {
167         m_marker->detach();
168         m_marker = 0;
169     }
170     RenderBlock::detach();
171 }
172
173 void RenderListItem::calcListValue()
174 {
175     // only called from the marker so..
176     KHTMLAssert(m_marker);
177
178     if(predefVal != -1)
179         m_marker->m_value = predefVal;
180     else if(!previousSibling())
181         m_marker->m_value = 1;
182     else {
183         RenderObject *o = previousSibling();
184         while ( o && (!o->isListItem() || o->style()->listStyleType() == LNONE) )
185             o = o->previousSibling();
186         if( o && o->isListItem() && o->style()->listStyleType() != LNONE ) {
187             RenderListItem *item = static_cast<RenderListItem *>(o);
188             m_marker->m_value = item->value() + 1;
189         }
190         else
191             m_marker->m_value = 1;
192     }
193 }
194
195 static RenderObject* getParentOfFirstLineBox(RenderObject* curr, RenderObject* marker)
196 {
197     RenderObject* firstChild = curr->firstChild();
198     if (!firstChild)
199         return 0;
200         
201     for (RenderObject* currChild = firstChild;
202          currChild; currChild = currChild->nextSibling()) {
203         if (currChild == marker)
204             continue;
205             
206         if (currChild->isInline())
207             return curr;
208         
209         if (currChild->isFloating() || currChild->isPositioned())
210             continue;
211             
212         if (currChild->isTable() || !currChild->isRenderBlock())
213             break;
214         
215         if (currChild->style()->htmlHacks() && currChild->element() &&
216             (currChild->element()->id() == ID_UL || currChild->element()->id() == ID_OL))
217             break;
218             
219         RenderObject* lineBox = getParentOfFirstLineBox(currChild, marker);
220         if (lineBox)
221             return lineBox;
222     }
223     
224     return 0;
225 }
226
227 void RenderListItem::updateMarkerLocation()
228 {
229     // Sanity check the location of our marker.
230     if (m_marker) {
231         RenderObject* markerPar = m_marker->parent();
232         RenderObject* lineBoxParent = getParentOfFirstLineBox(this, m_marker);
233         if (!lineBoxParent) {
234             // If the marker is currently contained inside an anonymous box,
235             // then we are the only item in that anonymous box (since no line box
236             // parent was found).  It's ok to just leave the marker where it is
237             // in this case.
238             if (markerPar && markerPar->isAnonymousBlock())
239                 lineBoxParent = markerPar;
240             else
241                 lineBoxParent = this;
242         }
243         if (markerPar != lineBoxParent)
244         {
245             if (markerPar)
246                 markerPar->removeChild(m_marker);
247             if (!lineBoxParent)
248                 lineBoxParent = this;
249             lineBoxParent->addChild(m_marker, lineBoxParent->firstChild());
250             if (!m_marker->minMaxKnown())
251                 m_marker->calcMinMaxWidth();
252             recalcMinMaxWidths();
253         }
254     }
255 }
256
257 void RenderListItem::calcMinMaxWidth()
258 {
259     // Make sure our marker is in the correct location.
260     updateMarkerLocation();
261     if (!minMaxKnown())
262         RenderBlock::calcMinMaxWidth();
263 }
264
265 void RenderListItem::layout( )
266 {
267     KHTMLAssert( needsLayout() );
268     KHTMLAssert( minMaxKnown() );
269
270     updateMarkerLocation();    
271     RenderBlock::layout();
272 }
273
274 void RenderListItem::paint(PaintInfo& i, int _tx, int _ty)
275 {
276     if (!m_height)
277         return;
278
279 #ifdef DEBUG_LAYOUT
280     kdDebug( 6040 ) << nodeName().string() << "(LI)::paint()" << endl;
281 #endif
282     RenderBlock::paint(i, _tx, _ty);
283 }
284
285 QRect RenderListItem::getAbsoluteRepaintRect()
286 {
287     QRect result = RenderBlock::getAbsoluteRepaintRect();
288     if (m_marker && !m_marker->isInside()) {
289         // This can be a sloppy and imprecise offset as long as it's always too big.
290         int pixHeight = style()->htmlFont().getFontDef().computedPixelSize();
291         int offset = pixHeight*2/3;
292         bool haveImage = m_marker->listImage() && !m_marker->listImage()->isErrorImage();
293         if (haveImage)
294             offset = m_marker->listImage()->pixmap().width();
295         int bulletWidth = offset/2;
296         if (offset%2)
297             bulletWidth++;
298         int xoff = 0;
299         if (style()->direction() == LTR)
300             xoff = -cMarkerPadding - offset;
301         else
302             xoff = cMarkerPadding + (haveImage ? 0 : (offset - bulletWidth));
303
304         if (xoff < 0) {
305             result.setX(result.x() + xoff);
306             result.setWidth(result.width() - xoff);
307         }
308         else
309             result.setWidth(result.width() + xoff);
310     }
311     return result;
312 }
313
314 // -----------------------------------------------------------
315
316 RenderListMarker::RenderListMarker(DocumentImpl* document)
317     : RenderBox(document), m_listImage(0), m_value(-1)
318 {
319     // init RenderObject attributes
320     setInline(true);   // our object is Inline
321     setReplaced(true); // pretend to be replaced
322     // val = -1;
323     // m_listImage = 0;
324 }
325
326 RenderListMarker::~RenderListMarker()
327 {
328     if(m_listImage)
329         m_listImage->deref(this);
330 }
331
332 void RenderListMarker::setStyle(RenderStyle *s)
333 {
334     if ( s && style() && s->listStylePosition() != style()->listStylePosition() )
335         setNeedsLayoutAndMinMaxRecalc();
336     
337     RenderBox::setStyle(s);
338
339     if ( m_listImage != style()->listStyleImage() ) {
340         if(m_listImage)  m_listImage->deref(this);
341         m_listImage = style()->listStyleImage();
342         if(m_listImage)  m_listImage->ref(this);
343     }
344 }
345
346 InlineBox* RenderListMarker::createInlineBox(bool, bool isRootLineBox, bool)
347 {
348     KHTMLAssert(!isRootLineBox);
349     ListMarkerBox* box = new (renderArena()) ListMarkerBox(this);
350     m_inlineBoxWrapper = box;
351     return box;
352 }
353
354 void RenderListMarker::paint(PaintInfo& i, int _tx, int _ty)
355 {
356     if (i.phase != PaintActionForeground)
357         return;
358     
359     if (style()->visibility() != VISIBLE)  return;
360
361     _tx += m_x;
362     _ty += m_y;
363
364     if ((_ty > i.r.y() + i.r.height()) || (_ty + m_height < i.r.y()))
365         return;
366
367     if (shouldPaintBackgroundOrBorder()) 
368         paintBoxDecorations(i, _tx, _ty);
369
370 #ifdef DEBUG_LAYOUT
371     kdDebug( 6040 ) << nodeName().string() << "(ListMarker)::paintObject(" << _tx << ", " << _ty << ")" << endl;
372 #endif
373
374     QPainter* p = i.p;
375     p->setFont(style()->font());
376     const QFontMetrics fm = p->fontMetrics();
377     
378     // The marker needs to adjust its tx, for the case where it's an outside marker.
379     RenderObject* listItem = 0;
380     int leftLineOffset = 0;
381     int rightLineOffset = 0;
382     if (!isInside()) {
383         listItem = this;
384         int yOffset = 0;
385         int xOffset = 0;
386         while (listItem && listItem != m_listItem) {
387             yOffset += listItem->yPos();
388             xOffset += listItem->xPos();
389             listItem = listItem->parent();
390         }
391         
392         // Now that we have our xoffset within the listbox, we need to adjust ourselves by the delta
393         // between our current xoffset and our desired position (which is just outside the border box
394         // of the list item).
395         if (style()->direction() == LTR) {
396             leftLineOffset = m_listItem->leftRelOffset(yOffset, m_listItem->leftOffset(yOffset));
397             _tx -= (xOffset - leftLineOffset) + m_listItem->paddingLeft() + m_listItem->borderLeft();
398         }
399         else {
400             rightLineOffset = m_listItem->rightRelOffset(yOffset, m_listItem->rightOffset(yOffset));
401             _tx += (rightLineOffset-xOffset) + m_listItem->paddingRight() + m_listItem->borderRight();
402         }
403     }
404
405     bool isPrinting = (p->device()->devType() == QInternal::Printer);
406     if (isPrinting) {
407         if (_ty < i.r.y())
408             // This has been printed already we suppose.
409             return;
410         
411         RenderCanvas* c = canvas();
412         if (_ty + m_height + paddingBottom() + borderBottom() >= c->printRect().y() + c->printRect().height()) {
413             if (_ty < c->truncatedAt())
414                 c->setBestTruncatedAt(_ty, this);
415             // Let's print this on the next page.
416             return; 
417         }
418     }
419     
420     int offset = fm.ascent()*2/3;
421     bool haveImage = m_listImage && !m_listImage->isErrorImage();
422     if (haveImage)
423         offset = m_listImage->pixmap().width();
424     
425     int xoff = 0;
426     int yoff = fm.ascent() - offset;
427
428     int bulletWidth = offset/2;
429     if (offset%2)
430         bulletWidth++;
431     if (!isInside()) {
432         if (listItem->style()->direction() == LTR)
433             xoff = -cMarkerPadding - offset;
434         else
435             xoff = cMarkerPadding + (haveImage ? 0 : (offset - bulletWidth));
436     }
437     else if (style()->direction() == RTL)
438         xoff += haveImage ? cMarkerPadding : (m_width - bulletWidth);
439     
440     if (m_listImage && !m_listImage->isErrorImage()) {
441         p->drawPixmap(QPoint(_tx + xoff, _ty), m_listImage->pixmap());
442         return;
443     }
444
445 #ifdef BOX_DEBUG
446     p->setPen( Qt::red );
447     p->drawRect( _tx + xoff, _ty + yoff, offset, offset );
448 #endif
449
450     const QColor color( style()->color() );
451     p->setPen( color );
452
453     switch(style()->listStyleType()) {
454     case DISC:
455         p->setBrush(color);
456         p->drawEllipse(_tx + xoff, _ty + (3 * yoff)/2, bulletWidth, bulletWidth);
457         return;
458     case CIRCLE:
459         p->setBrush(Qt::NoBrush);
460         p->drawEllipse(_tx + xoff, _ty + (3 * yoff)/2, bulletWidth, bulletWidth);
461         return;
462     case SQUARE:
463         p->setBrush(color);
464         p->drawRect(_tx + xoff, _ty + (3 * yoff)/2, bulletWidth, bulletWidth);
465         return;
466     case LNONE:
467         return;
468     default:
469         if (!m_item.isEmpty()) {
470 #if APPLE_CHANGES
471             // Text should be drawn on the baseline, so we add in the ascent of the font. 
472             // For some inexplicable reason, this works in Konqueror.  I'm not sure why.
473             // - dwh
474             _ty += fm.ascent();
475 #else
476             //_ty += fm.ascent() - fm.height()/2 + 1;
477 #endif
478
479             if (isInside()) {
480                 if( style()->direction() == LTR) {
481                     p->drawText(_tx, _ty, 0, 0, Qt::AlignLeft|Qt::DontClip, m_item);
482                     p->drawText(_tx + fm.width(m_item), _ty, 0, 0, Qt::AlignLeft|Qt::DontClip, 
483                                 QString::fromLatin1(". "));
484                 }
485                 else {
486                     const QString& punct(QString::fromLatin1(" ."));
487                     p->drawText(_tx, _ty, 0, 0, Qt::AlignLeft|Qt::DontClip, punct);
488                     p->drawText(_tx + fm.width(punct), _ty, 0, 0, Qt::AlignLeft|Qt::DontClip, m_item);
489                 }
490             } else {
491                 if (style()->direction() == LTR) {
492                     const QString& punct(QString::fromLatin1(". "));
493                     p->drawText(_tx-offset/2, _ty, 0, 0, Qt::AlignRight|Qt::DontClip, punct);
494                     p->drawText(_tx-offset/2-fm.width(punct), _ty, 0, 0, Qt::AlignRight|Qt::DontClip, m_item);
495                 }
496                 else {
497                     const QString& punct(QString::fromLatin1(" ."));
498                     p->drawText(_tx+offset/2, _ty, 0, 0, Qt::AlignLeft|Qt::DontClip, punct);
499                     p->drawText(_tx+offset/2+fm.width(punct), _ty, 0, 0, Qt::AlignLeft|Qt::DontClip, m_item);
500                 }
501             }
502         }
503     }
504 }
505
506 void RenderListMarker::layout()
507 {
508     KHTMLAssert( needsLayout() );
509     // ### KHTMLAssert( minMaxKnown() );
510     if ( !minMaxKnown() )
511         calcMinMaxWidth();
512     setNeedsLayout(false);
513 }
514
515 void RenderListMarker::setPixmap( const QPixmap &p, const QRect& r, CachedImage *o)
516 {
517     if(o != m_listImage) {
518         RenderBox::setPixmap(p, r, o);
519         return;
520     }
521
522     if (m_width != m_listImage->pixmap_size().width() || m_height != m_listImage->pixmap_size().height())
523         setNeedsLayoutAndMinMaxRecalc();
524     else
525         repaint();
526 }
527
528 void RenderListMarker::calcMinMaxWidth()
529 {
530     KHTMLAssert( !minMaxKnown() );
531
532     m_width = 0;
533
534     if (m_listImage) {
535         if (isInside())
536             m_width = m_listImage->pixmap().width() + cMarkerPadding;
537         m_height = m_listImage->pixmap().height();
538         m_minWidth = m_maxWidth = m_width;
539         setMinMaxKnown();
540         return;
541     }
542
543     if (m_value < 0) // not yet calculated
544         m_listItem->calcListValue();
545
546     const QFontMetrics &fm = style()->fontMetrics();
547     m_height = fm.ascent();
548
549     switch(style()->listStyleType())
550     {
551     case DISC:
552     case CIRCLE:
553     case SQUARE:
554         if (isInside())
555             m_width = m_height;
556         goto end;
557     case ARMENIAN:
558     case GEORGIAN:
559     case CJK_IDEOGRAPHIC:
560     case HIRAGANA:
561     case KATAKANA:
562     case HIRAGANA_IROHA:
563     case KATAKANA_IROHA:
564     case DECIMAL_LEADING_ZERO:
565         // ### unsupported, we use decimal instead
566     case LDECIMAL:
567         m_item.sprintf( "%ld", m_value );
568         break;
569     case LOWER_ROMAN:
570         m_item = toRoman( m_value, false );
571         break;
572     case UPPER_ROMAN:
573         m_item = toRoman( m_value, true );
574         break;
575     case LOWER_GREEK:
576      {
577         int number = m_value - 1;
578         int l = (number % 24);
579
580         if (l>16) {l++;} // Skip GREEK SMALL LETTER FINAL SIGMA
581
582         m_item = QChar(945 + l);
583         for (int i = 0; i < (number / 24); i++) {
584             m_item += QString::fromLatin1("'");
585         }
586         break;
587      }
588     case HEBREW:
589         m_item = toHebrew( m_value );
590         break;
591     case LOWER_ALPHA:
592     case LOWER_LATIN:
593         m_item = toLetter( m_value, 'a' );
594         break;
595     case UPPER_ALPHA:
596     case UPPER_LATIN:
597         m_item = toLetter( m_value, 'A' );
598         break;
599     case LNONE:
600         break;
601     }
602
603     if (isInside())
604         m_width = fm.width(m_item) + fm.width(QString::fromLatin1(". "));
605
606 end:
607
608     m_minWidth = m_width;
609     m_maxWidth = m_width;
610
611     setMinMaxKnown();
612 }
613
614 void RenderListMarker::calcWidth()
615 {
616     RenderBox::calcWidth();
617 }
618
619 short RenderListMarker::lineHeight(bool, bool) const
620 {
621     if (!m_listImage)
622         return m_listItem->lineHeight(false, true);
623     return height();
624 }
625
626 short RenderListMarker::baselinePosition(bool, bool) const
627 {
628     if (!m_listImage) {
629         const QFontMetrics &fm = style()->fontMetrics();
630         return fm.ascent() + (lineHeight(false) - fm.height())/2;
631     }
632     return height();
633 }
634
635 bool RenderListMarker::isInside() const
636 {
637     return m_listItem->notInList() || style()->listStylePosition() == INSIDE;
638 }
639
640 #undef BOX_DEBUG