Fixed: <rdar://problem/3936844> Mail: Messages with rich text do not print
[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         // printRect can be null if WebCore's pagination is not used (ie Mail).
413         if (!c->printRect().isNull() && _ty + m_height + paddingBottom() + borderBottom() >= c->printRect().y() + c->printRect().height()) {
414             if (_ty < c->truncatedAt())
415                 c->setBestTruncatedAt(_ty, this);
416             // Let's print this on the next page.
417             return; 
418         }
419     }
420     
421     int offset = fm.ascent()*2/3;
422     bool haveImage = m_listImage && !m_listImage->isErrorImage();
423     if (haveImage)
424         offset = m_listImage->pixmap().width();
425     
426     int xoff = 0;
427     int yoff = fm.ascent() - offset;
428
429     int bulletWidth = offset/2;
430     if (offset%2)
431         bulletWidth++;
432     if (!isInside()) {
433         if (listItem->style()->direction() == LTR)
434             xoff = -cMarkerPadding - offset;
435         else
436             xoff = cMarkerPadding + (haveImage ? 0 : (offset - bulletWidth));
437     }
438     else if (style()->direction() == RTL)
439         xoff += haveImage ? cMarkerPadding : (m_width - bulletWidth);
440     
441     if (m_listImage && !m_listImage->isErrorImage()) {
442         p->drawPixmap(QPoint(_tx + xoff, _ty), m_listImage->pixmap());
443         return;
444     }
445
446 #ifdef BOX_DEBUG
447     p->setPen( Qt::red );
448     p->drawRect( _tx + xoff, _ty + yoff, offset, offset );
449 #endif
450
451     const QColor color( style()->color() );
452     p->setPen( color );
453
454     switch(style()->listStyleType()) {
455     case DISC:
456         p->setBrush(color);
457         p->drawEllipse(_tx + xoff, _ty + (3 * yoff)/2, bulletWidth, bulletWidth);
458         return;
459     case CIRCLE:
460         p->setBrush(Qt::NoBrush);
461         p->drawEllipse(_tx + xoff, _ty + (3 * yoff)/2, bulletWidth, bulletWidth);
462         return;
463     case SQUARE:
464         p->setBrush(color);
465         p->drawRect(_tx + xoff, _ty + (3 * yoff)/2, bulletWidth, bulletWidth);
466         return;
467     case LNONE:
468         return;
469     default:
470         if (!m_item.isEmpty()) {
471 #if APPLE_CHANGES
472             // Text should be drawn on the baseline, so we add in the ascent of the font. 
473             // For some inexplicable reason, this works in Konqueror.  I'm not sure why.
474             // - dwh
475             _ty += fm.ascent();
476 #else
477             //_ty += fm.ascent() - fm.height()/2 + 1;
478 #endif
479
480             if (isInside()) {
481                 if( style()->direction() == LTR) {
482                     p->drawText(_tx, _ty, 0, 0, Qt::AlignLeft|Qt::DontClip, m_item);
483                     p->drawText(_tx + fm.width(m_item), _ty, 0, 0, Qt::AlignLeft|Qt::DontClip, 
484                                 QString::fromLatin1(". "));
485                 }
486                 else {
487                     const QString& punct(QString::fromLatin1(" ."));
488                     p->drawText(_tx, _ty, 0, 0, Qt::AlignLeft|Qt::DontClip, punct);
489                     p->drawText(_tx + fm.width(punct), _ty, 0, 0, Qt::AlignLeft|Qt::DontClip, m_item);
490                 }
491             } else {
492                 if (style()->direction() == LTR) {
493                     const QString& punct(QString::fromLatin1(". "));
494                     p->drawText(_tx-offset/2, _ty, 0, 0, Qt::AlignRight|Qt::DontClip, punct);
495                     p->drawText(_tx-offset/2-fm.width(punct), _ty, 0, 0, Qt::AlignRight|Qt::DontClip, m_item);
496                 }
497                 else {
498                     const QString& punct(QString::fromLatin1(" ."));
499                     p->drawText(_tx+offset/2, _ty, 0, 0, Qt::AlignLeft|Qt::DontClip, punct);
500                     p->drawText(_tx+offset/2+fm.width(punct), _ty, 0, 0, Qt::AlignLeft|Qt::DontClip, m_item);
501                 }
502             }
503         }
504     }
505 }
506
507 void RenderListMarker::layout()
508 {
509     KHTMLAssert( needsLayout() );
510     // ### KHTMLAssert( minMaxKnown() );
511     if ( !minMaxKnown() )
512         calcMinMaxWidth();
513     setNeedsLayout(false);
514 }
515
516 void RenderListMarker::setPixmap( const QPixmap &p, const QRect& r, CachedImage *o)
517 {
518     if(o != m_listImage) {
519         RenderBox::setPixmap(p, r, o);
520         return;
521     }
522
523     if (m_width != m_listImage->pixmap_size().width() || m_height != m_listImage->pixmap_size().height())
524         setNeedsLayoutAndMinMaxRecalc();
525     else
526         repaint();
527 }
528
529 void RenderListMarker::calcMinMaxWidth()
530 {
531     KHTMLAssert( !minMaxKnown() );
532
533     m_width = 0;
534
535     if (m_listImage) {
536         if (isInside())
537             m_width = m_listImage->pixmap().width() + cMarkerPadding;
538         m_height = m_listImage->pixmap().height();
539         m_minWidth = m_maxWidth = m_width;
540         setMinMaxKnown();
541         return;
542     }
543
544     if (m_value < 0) // not yet calculated
545         m_listItem->calcListValue();
546
547     const QFontMetrics &fm = style()->fontMetrics();
548     m_height = fm.ascent();
549
550     switch(style()->listStyleType())
551     {
552     case DISC:
553     case CIRCLE:
554     case SQUARE:
555         if (isInside())
556             m_width = m_height;
557         goto end;
558     case ARMENIAN:
559     case GEORGIAN:
560     case CJK_IDEOGRAPHIC:
561     case HIRAGANA:
562     case KATAKANA:
563     case HIRAGANA_IROHA:
564     case KATAKANA_IROHA:
565     case DECIMAL_LEADING_ZERO:
566         // ### unsupported, we use decimal instead
567     case LDECIMAL:
568         m_item.sprintf( "%ld", m_value );
569         break;
570     case LOWER_ROMAN:
571         m_item = toRoman( m_value, false );
572         break;
573     case UPPER_ROMAN:
574         m_item = toRoman( m_value, true );
575         break;
576     case LOWER_GREEK:
577      {
578         int number = m_value - 1;
579         int l = (number % 24);
580
581         if (l>16) {l++;} // Skip GREEK SMALL LETTER FINAL SIGMA
582
583         m_item = QChar(945 + l);
584         for (int i = 0; i < (number / 24); i++) {
585             m_item += QString::fromLatin1("'");
586         }
587         break;
588      }
589     case HEBREW:
590         m_item = toHebrew( m_value );
591         break;
592     case LOWER_ALPHA:
593     case LOWER_LATIN:
594         m_item = toLetter( m_value, 'a' );
595         break;
596     case UPPER_ALPHA:
597     case UPPER_LATIN:
598         m_item = toLetter( m_value, 'A' );
599         break;
600     case LNONE:
601         break;
602     }
603
604     if (isInside())
605         m_width = fm.width(m_item) + fm.width(QString::fromLatin1(". "));
606
607 end:
608
609     m_minWidth = m_width;
610     m_maxWidth = m_width;
611
612     setMinMaxKnown();
613 }
614
615 void RenderListMarker::calcWidth()
616 {
617     RenderBox::calcWidth();
618 }
619
620 short RenderListMarker::lineHeight(bool, bool) const
621 {
622     if (!m_listImage)
623         return m_listItem->lineHeight(false, true);
624     return height();
625 }
626
627 short RenderListMarker::baselinePosition(bool, bool) const
628 {
629     if (!m_listImage) {
630         const QFontMetrics &fm = style()->fontMetrics();
631         return fm.ascent() + (lineHeight(false) - fm.height())/2;
632     }
633     return height();
634 }
635
636 bool RenderListMarker::isInside() const
637 {
638     return m_listItem->notInList() || style()->listStylePosition() == INSIDE;
639 }
640
641 #undef BOX_DEBUG