Reviewed by Eric Seidel.
[WebKit-https.git] / WebCore / rendering / RenderMenuList.cpp
1 /**
2  * This file is part of the select element renderer in WebCore.
3  *
4  * Copyright (C) 2006, 2007 Apple Inc. All rights reserved.
5  *               2009 Torch Mobile Inc. All rights reserved. (http://www.torchmobile.com/)
6  *
7  * This library is free software; you can redistribute it and/or
8  * modify it under the terms of the GNU Library General Public
9  * License as published by the Free Software Foundation; either
10  * version 2 of the License, or (at your option) any later version.
11  *
12  * This library is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15  * Library General Public License for more details.
16  *
17  * You should have received a copy of the GNU Library General Public License
18  * along with this library; see the file COPYING.LIB.  If not, write to
19  * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
20  * Boston, MA 02110-1301, USA.
21  *
22  */
23
24 #include "config.h"
25 #include "RenderMenuList.h"
26
27 #include "CSSStyleSelector.h"
28 #include "Document.h"
29 #include "FontSelector.h"
30 #include "FrameView.h"
31 #include "GraphicsContext.h"
32 #include "HTMLNames.h"
33 #include "HTMLSelectElement.h"
34 #include "OptionElement.h"
35 #include "OptionGroupElement.h"
36 #include "PopupMenu.h"
37 #include "RenderBR.h"
38 #include "RenderScrollbar.h"
39 #include "RenderText.h"
40 #include "RenderTheme.h"
41 #include "NodeRenderStyle.h"
42 #include <math.h>
43
44 using namespace std;
45
46 namespace WebCore {
47
48 using namespace HTMLNames;
49
50 RenderMenuList::RenderMenuList(HTMLSelectElement* element)
51     : RenderFlexibleBox(element)
52     , m_buttonText(0)
53     , m_innerBlock(0)
54     , m_optionsChanged(true)
55     , m_optionsWidth(0)
56     , m_popup(0)
57     , m_popupIsVisible(false)
58 {
59 }
60
61 RenderMenuList::~RenderMenuList()
62 {
63     if (m_popup)
64         m_popup->disconnectClient();
65     m_popup = 0;
66 }
67
68 // this static cast is safe because RenderMenuLists are only created for HTMLSelectElements
69 HTMLSelectElement* RenderMenuList::selectElement()
70 {
71     return static_cast<HTMLSelectElement*>(node());
72 }
73
74 void RenderMenuList::createInnerBlock()
75 {
76     if (m_innerBlock) {
77         ASSERT(firstChild() == m_innerBlock);
78         ASSERT(!m_innerBlock->nextSibling());
79         return;
80     }
81
82     // Create an anonymous block.
83     ASSERT(!firstChild());
84     m_innerBlock = createAnonymousBlock();
85     adjustInnerStyle();
86     RenderFlexibleBox::addChild(m_innerBlock);
87 }
88
89 void RenderMenuList::adjustInnerStyle()
90 {
91     m_innerBlock->style()->setBoxFlex(1.0f);
92     
93     m_innerBlock->style()->setPaddingLeft(Length(theme()->popupInternalPaddingLeft(style()), Fixed));
94     m_innerBlock->style()->setPaddingRight(Length(theme()->popupInternalPaddingRight(style()), Fixed));
95     m_innerBlock->style()->setPaddingTop(Length(theme()->popupInternalPaddingTop(style()), Fixed));
96     m_innerBlock->style()->setPaddingBottom(Length(theme()->popupInternalPaddingBottom(style()), Fixed));
97         
98     if (PopupMenu::itemWritingDirectionIsNatural()) {
99         // Items in the popup will not respect the CSS text-align and direction properties,
100         // so we must adjust our own style to match.
101         m_innerBlock->style()->setTextAlign(LEFT);
102         TextDirection direction = (m_buttonText && m_buttonText->text()->defaultWritingDirection() == WTF::Unicode::RightToLeft) ? RTL : LTR;
103         m_innerBlock->style()->setDirection(direction);
104     }
105 }
106
107 void RenderMenuList::addChild(RenderObject* newChild, RenderObject* beforeChild)
108 {
109     createInnerBlock();
110     m_innerBlock->addChild(newChild, beforeChild);
111 }
112
113 void RenderMenuList::removeChild(RenderObject* oldChild)
114 {
115     if (oldChild == m_innerBlock || !m_innerBlock) {
116         RenderFlexibleBox::removeChild(oldChild);
117         m_innerBlock = 0;
118     } else
119         m_innerBlock->removeChild(oldChild);
120 }
121
122 void RenderMenuList::styleDidChange(RenderStyle::Diff diff, const RenderStyle* oldStyle)
123 {
124     RenderBlock::styleDidChange(diff, oldStyle);
125
126     if (m_buttonText)
127         m_buttonText->setStyle(style());
128     if (m_innerBlock) // RenderBlock handled updating the anonymous block's style.
129         adjustInnerStyle();
130
131     setReplaced(isInline());
132
133     bool fontChanged = !oldStyle || oldStyle->font() != style()->font();
134     if (fontChanged)
135         updateOptionsWidth();
136 }
137
138 void RenderMenuList::updateOptionsWidth()
139 {
140     float maxOptionWidth = 0;
141     const Vector<HTMLElement*>& listItems = static_cast<HTMLSelectElement*>(node())->listItems();
142     int size = listItems.size();    
143     for (int i = 0; i < size; ++i) {
144         HTMLElement* element = listItems[i];
145         OptionElement* optionElement = optionElementForElement(element);
146         if (!optionElement)
147             continue;
148
149         String text = optionElement->textIndentedToRespectGroupLabel();
150         if (!text.isEmpty())
151             maxOptionWidth = max(maxOptionWidth, style()->font().floatWidth(text));
152     }
153
154     int width = static_cast<int>(ceilf(maxOptionWidth));
155     if (m_optionsWidth == width)
156         return;
157
158     m_optionsWidth = width;
159     setNeedsLayoutAndPrefWidthsRecalc();
160 }
161
162 void RenderMenuList::updateFromElement()
163 {
164     if (m_optionsChanged) {
165         updateOptionsWidth();
166         m_optionsChanged = false;
167     }
168
169     if (m_popupIsVisible)
170         m_popup->updateFromElement();
171     else
172         setTextFromOption(static_cast<HTMLSelectElement*>(node())->selectedIndex());
173 }
174
175 void RenderMenuList::setTextFromOption(int optionIndex)
176 {
177     HTMLSelectElement* select = static_cast<HTMLSelectElement*>(node());
178     const Vector<HTMLElement*>& listItems = select->listItems();
179     int size = listItems.size();
180
181     int i = select->optionToListIndex(optionIndex);
182     String text = "";
183     if (i >= 0 && i < size) {
184         if (OptionElement* optionElement = optionElementForElement(listItems[i]))
185             text = optionElement->textIndentedToRespectGroupLabel();
186     }
187
188     setText(text.stripWhiteSpace());
189 }
190
191 void RenderMenuList::setText(const String& s)
192 {
193     if (s.isEmpty()) {
194         if (!m_buttonText || !m_buttonText->isBR()) {
195             if (m_buttonText)
196                 m_buttonText->destroy();
197             m_buttonText = new (renderArena()) RenderBR(document());
198             m_buttonText->setStyle(style());
199             addChild(m_buttonText);
200         }
201     } else {
202         if (m_buttonText && !m_buttonText->isBR())
203             m_buttonText->setText(s.impl());
204         else {
205             if (m_buttonText)
206                 m_buttonText->destroy();
207             m_buttonText = new (renderArena()) RenderText(document(), s.impl());
208             m_buttonText->setStyle(style());
209             addChild(m_buttonText);
210         }
211         adjustInnerStyle();
212     }
213 }
214
215 String RenderMenuList::text() const
216 {
217     return m_buttonText ? m_buttonText->text() : 0;
218 }
219
220 IntRect RenderMenuList::controlClipRect(int tx, int ty) const
221 {
222     // Clip to the intersection of the content box and the content box for the inner box
223     // This will leave room for the arrows which sit in the inner box padding,
224     // and if the inner box ever spills out of the outer box, that will get clipped too.
225     IntRect outerBox(tx + borderLeft() + paddingLeft(), 
226                    ty + borderTop() + paddingTop(),
227                    contentWidth(), 
228                    contentHeight());
229     
230     IntRect innerBox(tx + m_innerBlock->x() + m_innerBlock->paddingLeft(), 
231                    ty + m_innerBlock->y() + m_innerBlock->paddingTop(),
232                    m_innerBlock->contentWidth(), 
233                    m_innerBlock->contentHeight());
234
235     return intersection(outerBox, innerBox);
236 }
237
238 void RenderMenuList::calcPrefWidths()
239 {
240     m_minPrefWidth = 0;
241     m_maxPrefWidth = 0;
242     
243     if (style()->width().isFixed() && style()->width().value() > 0)
244         m_minPrefWidth = m_maxPrefWidth = calcContentBoxWidth(style()->width().value());
245     else
246         m_maxPrefWidth = max(m_optionsWidth, theme()->minimumMenuListSize(style())) + m_innerBlock->paddingLeft() + m_innerBlock->paddingRight();
247
248     if (style()->minWidth().isFixed() && style()->minWidth().value() > 0) {
249         m_maxPrefWidth = max(m_maxPrefWidth, calcContentBoxWidth(style()->minWidth().value()));
250         m_minPrefWidth = max(m_minPrefWidth, calcContentBoxWidth(style()->minWidth().value()));
251     } else if (style()->width().isPercent() || (style()->width().isAuto() && style()->height().isPercent()))
252         m_minPrefWidth = 0;
253     else
254         m_minPrefWidth = m_maxPrefWidth;
255
256     if (style()->maxWidth().isFixed() && style()->maxWidth().value() != undefinedLength) {
257         m_maxPrefWidth = min(m_maxPrefWidth, calcContentBoxWidth(style()->maxWidth().value()));
258         m_minPrefWidth = min(m_minPrefWidth, calcContentBoxWidth(style()->maxWidth().value()));
259     }
260
261     int toAdd = paddingLeft() + paddingRight() + borderLeft() + borderRight();
262     m_minPrefWidth += toAdd;
263     m_maxPrefWidth += toAdd;
264
265     setPrefWidthsDirty(false);
266 }
267
268 void RenderMenuList::showPopup()
269 {
270     if (m_popupIsVisible)
271         return;
272
273     // Create m_innerBlock here so it ends up as the first child.
274     // This is important because otherwise we might try to create m_innerBlock
275     // inside the showPopup call and it would fail.
276     createInnerBlock();
277     if (!m_popup)
278         m_popup = PopupMenu::create(this);
279     HTMLSelectElement* select = static_cast<HTMLSelectElement*>(node());
280     m_popupIsVisible = true;
281
282     // Compute the top left taking transforms into account, but use
283     // the actual width of the element to size the popup.
284     FloatPoint absTopLeft = localToAbsolute(FloatPoint(), false, true);
285     IntRect absBounds = absoluteBoundingBoxRect();
286     absBounds.setLocation(roundedIntPoint(absTopLeft));
287     m_popup->show(absBounds, document()->view(),
288         select->optionToListIndex(select->selectedIndex()));
289 }
290
291 void RenderMenuList::hidePopup()
292 {
293     if (m_popup)
294         m_popup->hide();
295     m_popupIsVisible = false;
296 }
297
298 void RenderMenuList::valueChanged(unsigned listIndex, bool fireOnChange)
299 {
300     HTMLSelectElement* select = static_cast<HTMLSelectElement*>(node());
301     select->setSelectedIndex(select->listToOptionIndex(listIndex), true, fireOnChange);
302 }
303
304 String RenderMenuList::itemText(unsigned listIndex) const
305 {
306     HTMLSelectElement* select = static_cast<HTMLSelectElement*>(node());
307     HTMLElement* element = select->listItems()[listIndex];
308     if (OptionGroupElement* optionGroupElement = optionGroupElementForElement(element))
309         return optionGroupElement->groupLabelText();
310     else if (OptionElement* optionElement = optionElementForElement(element))
311         return optionElement->textIndentedToRespectGroupLabel();
312     return String();
313 }
314
315 bool RenderMenuList::itemIsEnabled(unsigned listIndex) const
316 {
317     HTMLSelectElement* select = static_cast<HTMLSelectElement*>(node());
318     HTMLElement* element = select->listItems()[listIndex];
319     if (!element->hasTagName(optionTag))
320         return false;
321     bool groupEnabled = true;
322     if (element->parentNode() && element->parentNode()->hasTagName(optgroupTag))
323         groupEnabled = element->parentNode()->isEnabled();
324     return element->isEnabled() && groupEnabled;
325 }
326
327 PopupMenuStyle RenderMenuList::itemStyle(unsigned listIndex) const
328 {
329     HTMLSelectElement* select = static_cast<HTMLSelectElement*>(node());
330     HTMLElement* element = select->listItems()[listIndex];
331     
332     RenderStyle* style = element->renderStyle() ? element->renderStyle() : element->computedStyle();
333     return style ? PopupMenuStyle(style->color(), itemBackgroundColor(listIndex), style->font(), style->visibility() == VISIBLE) : menuStyle();
334 }
335
336 Color RenderMenuList::itemBackgroundColor(unsigned listIndex) const
337 {
338     HTMLSelectElement* select = static_cast<HTMLSelectElement*>(node());
339     HTMLElement* element = select->listItems()[listIndex];
340
341     Color backgroundColor;
342     if (element->renderStyle())
343         backgroundColor = element->renderStyle()->backgroundColor();
344     // If the item has an opaque background color, return that.
345     if (!backgroundColor.hasAlpha())
346         return backgroundColor;
347
348     // Otherwise, the item's background is overlayed on top of the menu background.
349     backgroundColor = style()->backgroundColor().blend(backgroundColor);
350     if (!backgroundColor.hasAlpha())
351         return backgroundColor;
352
353     // If the menu background is not opaque, then add an opaque white background behind.
354     return Color(Color::white).blend(backgroundColor);
355 }
356
357 PopupMenuStyle RenderMenuList::menuStyle() const
358 {
359
360     RenderStyle* s = m_innerBlock ? m_innerBlock->style() : style();
361     return PopupMenuStyle(s->color(), s->backgroundColor(), s->font(), s->visibility() == VISIBLE);
362 }
363
364 HostWindow* RenderMenuList::hostWindow() const
365 {
366     return document()->view()->hostWindow();
367 }
368
369 PassRefPtr<Scrollbar> RenderMenuList::createScrollbar(ScrollbarClient* client, ScrollbarOrientation orientation, ScrollbarControlSize controlSize)
370 {
371     RefPtr<Scrollbar> widget;
372     bool hasCustomScrollbarStyle = style()->hasPseudoStyle(RenderStyle::SCROLLBAR);
373     if (hasCustomScrollbarStyle)
374         widget = RenderScrollbar::createCustomScrollbar(client, orientation, this);
375     else
376         widget = Scrollbar::createNativeScrollbar(client, orientation, controlSize);
377     return widget.release();
378 }
379
380 int RenderMenuList::clientInsetLeft() const
381 {
382     return 0;
383 }
384
385 int RenderMenuList::clientInsetRight() const
386 {
387     return 0;
388 }
389
390 int RenderMenuList::clientPaddingLeft() const
391 {
392     return paddingLeft();
393 }
394
395 int RenderMenuList::clientPaddingRight() const
396 {
397     return paddingRight();
398 }
399
400 int RenderMenuList::listSize() const
401 {
402     HTMLSelectElement* select = static_cast<HTMLSelectElement*>(node());
403     return select->listItems().size();
404 }
405
406 int RenderMenuList::selectedIndex() const
407 {
408     HTMLSelectElement* select = static_cast<HTMLSelectElement*>(node());
409     return select->optionToListIndex(select->selectedIndex());
410 }
411
412 bool RenderMenuList::itemIsSeparator(unsigned listIndex) const
413 {
414     HTMLSelectElement* select = static_cast<HTMLSelectElement*>(node());
415     HTMLElement* element = select->listItems()[listIndex];
416     return element->hasTagName(hrTag);
417 }
418
419 bool RenderMenuList::itemIsLabel(unsigned listIndex) const
420 {
421     HTMLSelectElement* select = static_cast<HTMLSelectElement*>(node());
422     HTMLElement* element = select->listItems()[listIndex];
423     return element->hasTagName(optgroupTag);
424 }
425
426 bool RenderMenuList::itemIsSelected(unsigned listIndex) const
427 {
428     HTMLSelectElement* select = static_cast<HTMLSelectElement*>(node());
429     HTMLElement* element = select->listItems()[listIndex];
430     if (OptionElement* optionElement = optionElementForElement(element))
431         return optionElement->selected();
432     return false;
433 }
434
435 void RenderMenuList::setTextFromItem(unsigned listIndex)
436 {
437     HTMLSelectElement* select = static_cast<HTMLSelectElement*>(node());
438     setTextFromOption(select->listToOptionIndex(listIndex));
439 }
440
441 FontSelector* RenderMenuList::fontSelector() const
442 {
443     return document()->styleSelector()->fontSelector();
444 }
445
446 }