Reviewed by Kevin Decker.
[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  *
6  * This library is free software; you can redistribute it and/or
7  * modify it under the terms of the GNU Library General Public
8  * License as published by the Free Software Foundation; either
9  * version 2 of the License, or (at your option) any later version.
10  *
11  * This library is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14  * Library General Public License for more details.
15  *
16  * You should have received a copy of the GNU Library General Public License
17  * along with this library; see the file COPYING.LIB.  If not, write to
18  * the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
19  * Boston, MA 02111-1307, USA.
20  *
21  */
22
23 #include "config.h"
24 #include "RenderMenuList.h"
25
26 #include "Document.h"
27 #include "FrameView.h"
28 #include "GraphicsContext.h"
29 #include "HTMLNames.h"
30 #include "HTMLOptionElement.h"
31 #include "HTMLOptGroupElement.h"
32 #include "HTMLSelectElement.h"
33 #include "PopupMenu.h"
34 #include "RenderBR.h"
35 #include "RenderText.h"
36 #include "RenderTheme.h"
37 #include "TextStyle.h"
38 #include <math.h>
39
40 using namespace std;
41
42 namespace WebCore {
43
44 using namespace HTMLNames;
45
46 RenderMenuList::RenderMenuList(HTMLSelectElement* element)
47     : RenderFlexibleBox(element)
48     , m_buttonText(0)
49     , m_innerBlock(0)
50     , m_optionsChanged(true)
51     , m_optionsWidth(0)
52     , m_popup(0)
53     , m_popupIsVisible(false)
54 {
55 }
56
57 RenderMenuList::~RenderMenuList()
58 {
59     if (m_popup)
60         m_popup->disconnectClient();
61     m_popup = 0;
62 }
63
64 void RenderMenuList::createInnerBlock()
65 {
66     if (m_innerBlock) {
67         ASSERT(firstChild() == m_innerBlock);
68         ASSERT(!m_innerBlock->nextSibling());
69         return;
70     }
71
72     // Create an anonymous block.
73     ASSERT(!firstChild());
74     m_innerBlock = createAnonymousBlock();
75     m_innerBlock->style()->setBoxFlex(1.0f);
76     RenderFlexibleBox::addChild(m_innerBlock);
77 }
78
79 void RenderMenuList::addChild(RenderObject* newChild, RenderObject* beforeChild)
80 {
81     createInnerBlock();
82     m_innerBlock->addChild(newChild, beforeChild);
83 }
84
85 void RenderMenuList::removeChild(RenderObject* oldChild)
86 {
87     if (oldChild == m_innerBlock || !m_innerBlock) {
88         RenderFlexibleBox::removeChild(oldChild);
89         m_innerBlock = 0;
90     } else
91         m_innerBlock->removeChild(oldChild);
92 }
93
94 void RenderMenuList::setStyle(RenderStyle* newStyle)
95 {
96     bool fontChanged = !style() || style()->font() != newStyle->font();
97
98     RenderBlock::setStyle(newStyle);
99
100     if (m_buttonText)
101         m_buttonText->setStyle(newStyle);
102     if (m_innerBlock) // RenderBlock handled updating the anonymous block's style.
103         m_innerBlock->style()->setBoxFlex(1.0f);
104     setReplaced(isInline());
105     if (fontChanged)
106         updateOptionsWidth();
107 }
108
109 void RenderMenuList::updateOptionsWidth()
110 {
111     // FIXME: There is no longer any reason to use a text style with the font hacks disabled.
112     // It is a leftover from when the text was not drawn with the engine -- now that we render
113     // with the engine, we can just use the default style as the engine does.
114     TextStyle textStyle(0, 0, 0, false, false, false, false);
115
116     float maxOptionWidth = 0;
117     const Vector<HTMLElement*>& listItems = static_cast<HTMLSelectElement*>(node())->listItems();
118     int size = listItems.size();    
119     for (int i = 0; i < size; ++i) {
120         HTMLElement* element = listItems[i];
121         if (element->hasTagName(optionTag)) {
122             String text = static_cast<HTMLOptionElement*>(element)->optionText();
123             if (!text.isEmpty())
124                 maxOptionWidth = max(maxOptionWidth, style()->font().floatWidth(text.impl(), textStyle));
125         }
126     }
127
128     int width = static_cast<int>(ceilf(maxOptionWidth));
129     if (m_optionsWidth == width)
130         return;
131
132     m_optionsWidth = width;
133     setNeedsLayoutAndMinMaxRecalc();
134 }
135
136 void RenderMenuList::updateFromElement()
137 {
138     if (m_optionsChanged) {
139         updateOptionsWidth();
140         m_optionsChanged = false;
141     }
142
143     if (m_popupIsVisible)
144         m_popup->updateFromElement();
145     else
146         setTextFromOption(static_cast<HTMLSelectElement*>(node())->selectedIndex());
147 }
148
149 void RenderMenuList::setTextFromOption(int optionIndex)
150 {
151     HTMLSelectElement* select = static_cast<HTMLSelectElement*>(node());
152     const Vector<HTMLElement*>& listItems = select->listItems();
153     int size = listItems.size();
154
155     int i = select->optionToListIndex(optionIndex);
156     String text = "";
157     if (i >= 0 && i < size) {
158         HTMLElement* element = listItems[i];
159         if (element->hasTagName(optionTag))
160             text = static_cast<HTMLOptionElement*>(listItems[i])->optionText();
161     }
162     setText(text.stripWhiteSpace());
163 }
164
165 void RenderMenuList::setText(const String& s)
166 {
167     if (s.isEmpty()) {
168         if (!m_buttonText || !m_buttonText->isBR()) {
169             if (m_buttonText)
170                 m_buttonText->destroy();
171             m_buttonText = new (renderArena()) RenderBR(document());
172             m_buttonText->setStyle(style());
173             addChild(m_buttonText);
174         }
175     } else {
176         if (m_buttonText && !m_buttonText->isBR())
177             m_buttonText->setText(s.impl());
178         else {
179             if (m_buttonText)
180                 m_buttonText->destroy();
181             m_buttonText = new (renderArena()) RenderText(document(), s.impl());
182             m_buttonText->setStyle(style());
183             addChild(m_buttonText);
184         }
185     }
186 }
187
188 String RenderMenuList::text() const
189 {
190     return m_buttonText ? m_buttonText->text() : 0;
191 }
192
193 IntRect RenderMenuList::controlClipRect(int tx, int ty) const
194 {
195     // Clip to the content box, since the arrow sits in the padding space, and we don't want to draw over it.
196     return IntRect(tx + borderLeft() + paddingLeft(), 
197                    ty + borderTop() + paddingTop(),
198                    contentWidth(), contentHeight());
199 }
200
201 void RenderMenuList::calcMinMaxWidth()
202 {
203     m_minWidth = 0;
204     m_maxWidth = 0;
205
206     if (style()->width().isFixed() && style()->width().value() > 0)
207         m_minWidth = m_maxWidth = calcContentBoxWidth(style()->width().value());
208     else
209         m_maxWidth = max(m_optionsWidth, theme()->minimumMenuListSize(style()));
210
211     if (style()->minWidth().isFixed() && style()->minWidth().value() > 0) {
212         m_maxWidth = max(m_maxWidth, calcContentBoxWidth(style()->minWidth().value()));
213         m_minWidth = max(m_minWidth, calcContentBoxWidth(style()->minWidth().value()));
214     } else if (style()->width().isPercent() || (style()->width().isAuto() && style()->height().isPercent()))
215         m_minWidth = 0;
216     else
217         m_minWidth = m_maxWidth;
218
219     if (style()->maxWidth().isFixed() && style()->maxWidth().value() != undefinedLength) {
220         m_maxWidth = min(m_maxWidth, calcContentBoxWidth(style()->maxWidth().value()));
221         m_minWidth = min(m_minWidth, calcContentBoxWidth(style()->maxWidth().value()));
222     }
223
224     int toAdd = paddingLeft() + paddingRight() + borderLeft() + borderRight();
225     m_minWidth += toAdd;
226     m_maxWidth += toAdd;
227
228     setMinMaxKnown();
229 }
230
231 void RenderMenuList::showPopup()
232 {
233     if (m_popupIsVisible)
234         return;
235
236     // Create m_innerBlock here so it ends up as the first child.
237     // This is important because otherwise we might try to create m_innerBlock
238     // inside the showPopup call and it would fail.
239     createInnerBlock();
240     if (!m_popup)
241         m_popup = PopupMenu::create(this);
242     HTMLSelectElement* select = static_cast<HTMLSelectElement*>(node());
243     m_popupIsVisible = true;
244     m_popup->show(absoluteBoundingBoxRect(), document()->view(),
245         select->optionToListIndex(select->selectedIndex()));
246 }
247
248 void RenderMenuList::hidePopup()
249 {
250     if (m_popup)
251         m_popup->hide();
252     m_popupIsVisible = false;
253 }
254
255 void RenderMenuList::valueChanged(unsigned listIndex, bool fireOnChange)
256 {
257     HTMLSelectElement* select = static_cast<HTMLSelectElement*>(node());
258     select->setSelectedIndex(select->listToOptionIndex(listIndex), true, fireOnChange);
259 }
260
261 String RenderMenuList::itemText(unsigned listIndex) const
262 {
263     HTMLSelectElement* select = static_cast<HTMLSelectElement*>(node());
264     HTMLElement* element = select->listItems()[listIndex];
265     if (element->hasTagName(optgroupTag))
266         return static_cast<HTMLOptGroupElement*>(element)->groupLabelText();
267     else if (element->hasTagName(optionTag))
268         return static_cast<HTMLOptionElement*>(element)->optionText();
269     return String();
270 }
271
272 bool RenderMenuList::itemIsEnabled(unsigned listIndex) const
273 {
274     HTMLSelectElement* select = static_cast<HTMLSelectElement*>(node());
275     HTMLElement* element = select->listItems()[listIndex];
276     if (!element->hasTagName(optionTag))
277         return false;
278     bool groupEnabled = true;
279     if (element->parentNode() && element->parentNode()->hasTagName(optgroupTag))
280         groupEnabled = element->parentNode()->isEnabled();
281     return element->isEnabled() && groupEnabled;
282 }
283
284 RenderStyle* RenderMenuList::itemStyle(unsigned listIndex) const
285 {
286     HTMLSelectElement* select = static_cast<HTMLSelectElement*>(node());
287     HTMLElement* element = select->listItems()[listIndex];
288     
289     return element->renderStyle() ? element->renderStyle() : style();
290 }
291
292 RenderStyle* RenderMenuList::clientStyle() const
293 {
294     return style();
295 }
296
297 Document* RenderMenuList::clientDocument() const
298 {
299     return document();
300 }
301
302 int RenderMenuList::clientPaddingLeft() const
303 {
304     return paddingLeft();
305 }
306
307 int RenderMenuList::clientPaddingRight() const
308 {
309     return paddingRight();
310 }
311
312 unsigned RenderMenuList::listSize() const
313 {
314     HTMLSelectElement* select = static_cast<HTMLSelectElement*>(node());
315     return select->listItems().size();
316 }
317
318 int RenderMenuList::selectedIndex() const
319 {
320     HTMLSelectElement* select = static_cast<HTMLSelectElement*>(node());
321     return select->optionToListIndex(select->selectedIndex());
322 }
323
324 bool RenderMenuList::itemIsSeparator(unsigned listIndex) const
325 {
326     HTMLSelectElement* select = static_cast<HTMLSelectElement*>(node());
327     HTMLElement* element = select->listItems()[listIndex];
328     return element->hasTagName(hrTag);
329 }
330
331 bool RenderMenuList::itemIsLabel(unsigned listIndex) const
332 {
333     HTMLSelectElement* select = static_cast<HTMLSelectElement*>(node());
334     HTMLElement* element = select->listItems()[listIndex];
335     return element->hasTagName(optgroupTag);
336 }
337
338 bool RenderMenuList::itemIsSelected(unsigned listIndex) const
339 {
340     HTMLSelectElement* select = static_cast<HTMLSelectElement*>(node());
341     HTMLElement* element = select->listItems()[listIndex];
342     return element->hasTagName(optionTag)&& static_cast<HTMLOptionElement*>(element)->selected();
343 }
344
345 void RenderMenuList::setTextFromItem(unsigned listIndex)
346 {
347     HTMLSelectElement* select = static_cast<HTMLSelectElement*>(node());
348     setTextFromOption(select->listToOptionIndex(listIndex));
349 }
350
351 }