afcda0829d8473227b84e5020adce7a6ee98a4f8
[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 static QString toRoman( int number, bool upper )
43 {
44     QString roman;
45     QChar ldigits[] = { 'i', 'v', 'x', 'l', 'c', 'd', 'm' };
46     QChar udigits[] = { 'I', 'V', 'X', 'L', 'C', 'D', 'M' };
47     QChar *digits = upper ? udigits : ldigits;
48     int i, d = 0;
49
50     do
51     {
52         int num = number % 10;
53
54         if ( num % 5 < 4 )
55             for ( i = num % 5; i > 0; i-- )
56                 roman.insert( 0, digits[ d ] );
57
58         if ( num >= 4 && num <= 8)
59             roman.insert( 0, digits[ d+1 ] );
60
61         if ( num == 9 )
62             roman.insert( 0, digits[ d+2 ] );
63
64         if ( num % 5 == 4 )
65             roman.insert( 0, digits[ d ] );
66
67         number /= 10;
68         d += 2;
69     }
70     while ( number );
71
72     return roman;
73 }
74
75 static QString toLetter( int number, int base ) {
76     number--;
77     QString letter = (QChar) (base + (number % 26));
78     // Add a single quote at the end of the alphabet.
79     for (int i = 0; i < (number / 26); i++) {
80        letter += '\'';
81     }
82     return letter;
83 }
84
85 static QString toHebrew( int number ) {
86     const QChar tenDigit[] = {1497, 1499, 1500, 1502, 1504, 1505, 1506, 1508, 1510};
87
88     QString letter;
89     if (number>999) {
90         letter = toHebrew(number/1000) + QString::fromLatin1("'");
91         number = number%1000;
92     }
93
94     int hunderts = (number/400);
95     if (hunderts > 0) {
96         for(int i=0; i<hunderts; i++) {
97             letter += QChar(1511 + 3);
98         }
99     }
100     number = number % 400;
101     if ((number / 100) != 0) {
102         letter += QChar (1511 + (number / 100) -1);
103     }
104     number = number % 100;
105     int tens = number/10;
106     if (tens > 0 && !(number == 15 || number == 16)) {
107         letter += tenDigit[tens-1];
108     }
109     if (number == 15 || number == 16) { // special because of religious
110         letter += QChar(1487 + 9);       // reasons
111         letter += QChar(1487 + number - 9);
112     } else {
113         number = number % 10;
114         if (number != 0) {
115             letter += QChar (1487 + number);
116         }
117     }
118     return letter;
119 }
120
121 // -------------------------------------------------------------------------
122
123 RenderListItem::RenderListItem(DOM::NodeImpl* node)
124     : RenderBlock(node), _notInList(false)
125 {
126     // init RenderObject attributes
127     setInline(false);   // our object is not Inline
128
129     predefVal = -1;
130     m_marker = 0;
131 }
132
133 void RenderListItem::setStyle(RenderStyle *_style)
134 {
135     RenderBlock::setStyle(_style);
136
137     if (style()->listStyleType() != LNONE ||
138         (style()->listStyleImage() && !style()->listStyleImage()->isErrorImage())) {
139         RenderStyle *newStyle = new (renderArena()) RenderStyle();
140         newStyle->ref();
141         // The marker always inherits from the list item, regardless of where it might end
142         // up (e.g., in some deeply nested line box).  See CSS3 spec.
143         newStyle->inheritFrom(style()); 
144         if (!m_marker) {
145             m_marker = new (renderArena()) RenderListMarker(document());
146             m_marker->setStyle(newStyle);
147             m_marker->setListItem(this);
148             _markerInstalledInParent = false;
149         }
150         else
151             m_marker->setStyle(newStyle);
152         newStyle->deref(renderArena());
153     } else if (m_marker) {
154         m_marker->detach();
155         m_marker = 0;
156     }
157 }
158
159 RenderListItem::~RenderListItem()
160 {
161 }
162
163 void RenderListItem::detach()
164 {    
165     if (m_marker && !_markerInstalledInParent) {
166         m_marker->detach();
167         m_marker = 0;
168     }
169     RenderBlock::detach();
170 }
171
172 void RenderListItem::calcListValue()
173 {
174     // only called from the marker so..
175     KHTMLAssert(m_marker);
176
177     if(predefVal != -1)
178         m_marker->m_value = predefVal;
179     else if(!previousSibling())
180         m_marker->m_value = 1;
181     else {
182         RenderObject *o = previousSibling();
183         while ( o && (!o->isListItem() || o->style()->listStyleType() == LNONE) )
184             o = o->previousSibling();
185         if( o && o->isListItem() && o->style()->listStyleType() != LNONE ) {
186             RenderListItem *item = static_cast<RenderListItem *>(o);
187             m_marker->m_value = item->value() + 1;
188         }
189         else
190             m_marker->m_value = 1;
191     }
192 }
193
194 static RenderObject* getParentOfFirstLineBox(RenderObject* curr, RenderObject* marker)
195 {
196     RenderObject* firstChild = curr->firstChild();
197     if (!firstChild)
198         return 0;
199         
200     for (RenderObject* currChild = firstChild;
201          currChild; currChild = currChild->nextSibling()) {
202         if (currChild == marker)
203             continue;
204             
205         if (currChild->isInline())
206             return curr;
207         
208         if (currChild->isFloating() || currChild->isPositioned())
209             continue;
210             
211         if (currChild->isTable() || !currChild->isRenderBlock())
212             break;
213         
214         if (currChild->style()->htmlHacks() && currChild->element() &&
215             (currChild->element()->id() == ID_UL || currChild->element()->id() == ID_OL))
216             break;
217             
218         RenderObject* lineBox = getParentOfFirstLineBox(currChild, marker);
219         if (lineBox)
220             return lineBox;
221     }
222     
223     return 0;
224 }
225
226 void RenderListItem::updateMarkerLocation()
227 {
228     // Sanity check the location of our marker.
229     if (m_marker) {
230         RenderObject* markerPar = m_marker->parent();
231         RenderObject* lineBoxParent = getParentOfFirstLineBox(this, m_marker);
232         if (!lineBoxParent) {
233             // If the marker is currently contained inside an anonymous box,
234             // then we are the only item in that anonymous box (since no line box
235             // parent was found).  It's ok to just leave the marker where it is
236             // in this case.
237             if (markerPar && markerPar->isAnonymousBlock())
238                 lineBoxParent = markerPar;
239             else
240                 lineBoxParent = this;
241         }
242         if (markerPar != lineBoxParent)
243         {
244             if (markerPar)
245                 markerPar->removeChild(m_marker);
246             if (!lineBoxParent)
247                 lineBoxParent = this;
248             lineBoxParent->addChild(m_marker, lineBoxParent->firstChild());
249             _markerInstalledInParent = true;
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         int xoff = 0;
293         if (style()->direction() == LTR)
294             xoff = -7 - offset;
295         else
296             xoff = offset;
297
298         if (m_marker->listImage() && !m_marker->listImage()->isErrorImage()) {
299             // For OUTSIDE bullets shrink back to only a 0.3em margin. 0.67 em is too
300             // much.  This brings the margin back to MacIE/Gecko/WinIE levels.
301             // For LTR don't forget to add in the width of the image to the offset as
302             // well (you are moving the image left, so you have to also add in the width
303             // of the image's border box as well). -dwh
304             if (style()->direction() == LTR)
305                 xoff -= m_marker->listImage()->pixmap().width() - pixHeight*1/3;
306             else
307                 xoff -= pixHeight*1/3;
308         }
309
310         if (xoff < 0) {
311             result.setX(result.x() + xoff);
312             result.setWidth(result.width() - xoff);
313         }
314         else
315             result.setWidth(result.width() + xoff);
316     }
317     return result;
318 }
319
320 // -----------------------------------------------------------
321
322 RenderListMarker::RenderListMarker(DocumentImpl* document)
323     : RenderBox(document), m_listImage(0), m_value(-1)
324 {
325     // init RenderObject attributes
326     setInline(true);   // our object is Inline
327     setReplaced(true); // pretend to be replaced
328     // val = -1;
329     // m_listImage = 0;
330 }
331
332 RenderListMarker::~RenderListMarker()
333 {
334     if(m_listImage)
335         m_listImage->deref(this);
336 }
337
338 void RenderListMarker::setStyle(RenderStyle *s)
339 {
340     if ( s && style() && s->listStylePosition() != style()->listStylePosition() )
341         setNeedsLayoutAndMinMaxRecalc();
342     
343     RenderBox::setStyle(s);
344
345     if ( m_listImage != style()->listStyleImage() ) {
346         if(m_listImage)  m_listImage->deref(this);
347         m_listImage = style()->listStyleImage();
348         if(m_listImage)  m_listImage->ref(this);
349     }
350 }
351
352
353 void RenderListMarker::paint(PaintInfo& i, int _tx, int _ty)
354 {
355     if (i.phase != PaintActionForeground)
356         return;
357     
358     if (style()->visibility() != VISIBLE)  return;
359
360     _tx += m_x;
361     _ty += m_y;
362
363     if ((_ty > i.r.y() + i.r.height()) || (_ty + m_height < i.r.y()))
364         return;
365
366     if (shouldPaintBackgroundOrBorder()) 
367         paintBoxDecorations(i, _tx, _ty);
368
369 #ifdef DEBUG_LAYOUT
370     kdDebug( 6040 ) << nodeName().string() << "(ListMarker)::paintObject(" << _tx << ", " << _ty << ")" << endl;
371 #endif
372
373     QPainter* p = i.p;
374     p->setFont(style()->font());
375     const QFontMetrics fm = p->fontMetrics();
376     int offset = fm.ascent()*2/3;
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     {
408         if (_ty < i.r.y())
409         {
410             // This has been printed already we suppose.
411             return;
412         }
413         if (_ty + m_height + paddingBottom() + borderBottom() >= i.r.y() + i.r.height())
414         {
415             RenderCanvas *rootObj = canvas();
416             if (_ty < rootObj->truncatedAt())
417 #if APPLE_CHANGES
418                 rootObj->setBestTruncatedAt(_ty, this);
419 #else
420                 rootObj->setTruncatedAt(_ty);
421 #endif
422             // Let's print this on the next page.
423             return; 
424         }
425     }
426     
427
428     int xoff = 0;
429     int yoff = fm.ascent() - offset;
430
431     if (!isInside())
432         if (listItem->style()->direction() == LTR)
433             xoff = -7 - offset;
434         else 
435             xoff = offset;
436         
437             
438     if ( m_listImage && !m_listImage->isErrorImage()) {
439         // For OUTSIDE bullets shrink back to only a 0.3em margin. 0.67 em is too
440         // much.  This brings the margin back to MacIE/Gecko/WinIE levels.  
441         // For LTR don't forget to add in the width of the image to the offset as
442         // well (you are moving the image left, so you have to also add in the width
443         // of the image's border box as well). -dwh
444         if (!isInside()) {
445             if (style()->direction() == LTR)
446                 xoff -= m_listImage->pixmap().width() - fm.ascent()*1/3;
447             else
448                 xoff -= fm.ascent()*1/3;
449         }
450         
451         p->drawPixmap( QPoint( _tx + xoff, _ty ), m_listImage->pixmap());
452         return;
453     }
454
455 #ifdef BOX_DEBUG
456     p->setPen( Qt::red );
457     p->drawRect( _tx + xoff, _ty + yoff, offset, offset );
458 #endif
459
460     const QColor color( style()->color() );
461     p->setPen( color );
462
463     switch(style()->listStyleType()) {
464     case DISC:
465         p->setBrush( color );
466         p->drawEllipse( _tx + xoff, _ty + (3 * yoff)/2, (offset>>1)+1, (offset>>1)+1 );
467         return;
468     case CIRCLE:
469         p->setBrush( Qt::NoBrush );
470         p->drawEllipse( _tx + xoff, _ty + (3 * yoff)/2, (offset>>1)+1, (offset>>1)+1 );
471         return;
472     case SQUARE:
473         p->setBrush( color );
474         p->drawRect( _tx + xoff, _ty + (3 * yoff)/2, (offset>>1)+1, (offset>>1)+1 );
475         return;
476     case LNONE:
477         return;
478     default:
479         if (!m_item.isEmpty()) {
480 #if APPLE_CHANGES
481             // Text should be drawn on the baseline, so we add in the ascent of the font. 
482             // For some inexplicable reason, this works in Konqueror.  I'm not sure why.
483             // - dwh
484             _ty += fm.ascent();
485 #else
486             //_ty += fm.ascent() - fm.height()/2 + 1;
487 #endif
488             if (isInside()) {
489                 if( style()->direction() == LTR)
490                     p->drawText(_tx, _ty, 0, 0, Qt::AlignLeft|Qt::DontClip, m_item);
491                 else
492                     p->drawText(_tx, _ty, 0, 0, Qt::AlignRight|Qt::DontClip, m_item);
493             } else {
494                 if(style()->direction() == LTR)
495                     p->drawText(_tx-offset/2, _ty, 0, 0, Qt::AlignRight|Qt::DontClip, m_item);
496                 else
497                     p->drawText(_tx+offset/2, _ty, 0, 0, Qt::AlignLeft|Qt::DontClip, m_item);
498             }
499         }
500     }
501 }
502
503 void RenderListMarker::layout()
504 {
505     KHTMLAssert( needsLayout() );
506     // ### KHTMLAssert( minMaxKnown() );
507     if ( !minMaxKnown() )
508         calcMinMaxWidth();
509     setNeedsLayout(false);
510 }
511
512 void RenderListMarker::setPixmap( const QPixmap &p, const QRect& r, CachedImage *o)
513 {
514     if(o != m_listImage) {
515         RenderBox::setPixmap(p, r, o);
516         return;
517     }
518
519     if (m_width != m_listImage->pixmap_size().width() || m_height != m_listImage->pixmap_size().height())
520         setNeedsLayoutAndMinMaxRecalc();
521     else
522         repaint();
523 }
524
525 void RenderListMarker::calcMinMaxWidth()
526 {
527     KHTMLAssert( !minMaxKnown() );
528
529     m_width = 0;
530
531     if(m_listImage) {
532         if (isInside())
533             m_width = m_listImage->pixmap().width() + 5;
534         m_height = m_listImage->pixmap().height();
535         m_minWidth = m_maxWidth = m_width;
536         setMinMaxKnown();
537         return;
538     }
539
540     if (m_value < 0) // not yet calculated
541         m_listItem->calcListValue();
542
543     const QFontMetrics &fm = style()->fontMetrics();
544     m_height = fm.ascent();
545
546     switch(style()->listStyleType())
547     {
548     case DISC:
549     case CIRCLE:
550     case SQUARE:
551         if (isInside()) {
552             m_width = m_height; //fm.ascent();
553         }
554         goto end;
555     case ARMENIAN:
556     case GEORGIAN:
557     case CJK_IDEOGRAPHIC:
558     case HIRAGANA:
559     case KATAKANA:
560     case HIRAGANA_IROHA:
561     case KATAKANA_IROHA:
562     case DECIMAL_LEADING_ZERO:
563         // ### unsupported, we use decimal instead
564     case LDECIMAL:
565         m_item.sprintf( "%ld", m_value );
566         break;
567     case LOWER_ROMAN:
568         m_item = toRoman( m_value, false );
569         break;
570     case UPPER_ROMAN:
571         m_item = toRoman( m_value, true );
572         break;
573     case LOWER_GREEK:
574      {
575         int number = m_value - 1;
576         int l = (number % 24);
577
578         if (l>16) {l++;} // Skip GREEK SMALL LETTER FINAL SIGMA
579
580         m_item = QChar(945 + l);
581         for (int i = 0; i < (number / 24); i++) {
582             m_item += QString::fromLatin1("'");
583         }
584         break;
585      }
586     case HEBREW:
587         m_item = toHebrew( m_value );
588         break;
589     case LOWER_ALPHA:
590     case LOWER_LATIN:
591         m_item = toLetter( m_value, 'a' );
592         break;
593     case UPPER_ALPHA:
594     case UPPER_LATIN:
595         m_item = toLetter( m_value, 'A' );
596         break;
597     case LNONE:
598         break;
599     }
600
601     m_item += QString::fromLatin1(". ");
602
603     if (isInside())
604         m_width = fm.width(m_item);
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     return height();
622 }
623
624 short RenderListMarker::baselinePosition(bool, bool) const
625 {
626     return height();
627 }
628
629 bool RenderListMarker::isInside() const
630 {
631     return m_listItem->notInList() || style()->listStylePosition() == INSIDE;
632 }
633
634 #undef BOX_DEBUG