1 /* 2 * This file is part of the select element renderer in WebCore. 3 * 4 * Copyright (C) 2006, 2007, 2008, 2009, 2010 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 "AXObjectCache.h" 28 #include "CSSStyleSelector.h" 29 #include "Frame.h" 30 #include "FrameView.h" 31 #include "HTMLNames.h" 32 #include "NodeRenderStyle.h" 33 #include "OptionElement.h" 34 #include "OptionGroupElement.h" 35 #include "PopupMenu.h" 36 #include "RenderBR.h" 37 #include "RenderScrollbar.h" 38 #include "RenderTheme.h" 39 #include "SelectElement.h" 40 #include <math.h> 41 42 using namespace std; 43 44 namespace WebCore { 45 46 using namespace HTMLNames; 47 48 RenderMenuList::RenderMenuList(Element* element) 49 : RenderFlexibleBox(element) 50 , m_buttonText(0) 51 , m_innerBlock(0) 52 , m_optionsChanged(true) 53 , m_optionsWidth(0) 54 , m_lastSelectedIndex(-1) 55 , m_popup(0) 56 , m_popupIsVisible(false) 57 { 58 } 59 60 RenderMenuList::~RenderMenuList() 61 { 62 if (m_popup) 63 m_popup->disconnectClient(); 64 m_popup = 0; 65 } 66 67 void RenderMenuList::createInnerBlock() 68 { 69 if (m_innerBlock) { 70 ASSERT(firstChild() == m_innerBlock); 71 ASSERT(!m_innerBlock->nextSibling()); 72 return; 73 } 74 75 // Create an anonymous block. 76 ASSERT(!firstChild()); 77 m_innerBlock = createAnonymousBlock(); 78 adjustInnerStyle(); 79 RenderFlexibleBox::addChild(m_innerBlock); 80 } 81 82 void RenderMenuList::adjustInnerStyle() 83 { 84 m_innerBlock->style()->setBoxFlex(1.0f); 85 86 m_innerBlock->style()->setPaddingLeft(Length(theme()->popupInternalPaddingLeft(style()), Fixed)); 87 m_innerBlock->style()->setPaddingRight(Length(theme()->popupInternalPaddingRight(style()), Fixed)); 88 m_innerBlock->style()->setPaddingTop(Length(theme()->popupInternalPaddingTop(style()), Fixed)); 89 m_innerBlock->style()->setPaddingBottom(Length(theme()->popupInternalPaddingBottom(style()), Fixed)); 90 91 if (PopupMenu::itemWritingDirectionIsNatural()) { 92 // Items in the popup will not respect the CSS text-align and direction properties, 93 // so we must adjust our own style to match. 94 m_innerBlock->style()->setTextAlign(LEFT); 95 TextDirection direction = (m_buttonText && m_buttonText->text()->defaultWritingDirection() == WTF::Unicode::RightToLeft) ? RTL : LTR; 96 m_innerBlock->style()->setDirection(direction); 97 } 98 } 99 100 void RenderMenuList::addChild(RenderObject* newChild, RenderObject* beforeChild) 101 { 102 createInnerBlock(); 103 m_innerBlock->addChild(newChild, beforeChild); 104 } 105 106 void RenderMenuList::removeChild(RenderObject* oldChild) 107 { 108 if (oldChild == m_innerBlock || !m_innerBlock) { 109 RenderFlexibleBox::removeChild(oldChild); 110 m_innerBlock = 0; 111 } else 112 m_innerBlock->removeChild(oldChild); 113 } 114 115 void RenderMenuList::styleDidChange(StyleDifference diff, const RenderStyle* oldStyle) 116 { 117 RenderBlock::styleDidChange(diff, oldStyle); 118 119 if (m_buttonText) 120 m_buttonText->setStyle(style()); 121 if (m_innerBlock) // RenderBlock handled updating the anonymous block's style. 122 adjustInnerStyle(); 123 124 setReplaced(isInline()); 125 126 bool fontChanged = !oldStyle || oldStyle->font() != style()->font(); 127 if (fontChanged) 128 updateOptionsWidth(); 129 } 130 131 void RenderMenuList::updateOptionsWidth() 132 { 133 float maxOptionWidth = 0; 134 const Vector<Element*>& listItems = toSelectElement(static_cast<Element*>(node()))->listItems(); 135 int size = listItems.size(); 136 for (int i = 0; i < size; ++i) { 137 Element* element = listItems[i]; 138 OptionElement* optionElement = toOptionElement(element); 139 if (!optionElement) 140 continue; 141 142 String text = optionElement->textIndentedToRespectGroupLabel(); 143 if (theme()->popupOptionSupportsTextIndent()) { 144 // Add in the option's text indent. We can't calculate percentage values for now. 145 float optionWidth = 0; 146 if (RenderStyle* optionStyle = element->renderStyle()) 147 optionWidth += optionStyle->textIndent().calcMinValue(0); 148 if (!text.isEmpty()) 149 optionWidth += style()->font().floatWidth(text); 150 maxOptionWidth = max(maxOptionWidth, optionWidth); 151 } else if (!text.isEmpty()) 152 maxOptionWidth = max(maxOptionWidth, style()->font().floatWidth(text)); 153 } 154 155 int width = static_cast<int>(ceilf(maxOptionWidth)); 156 if (m_optionsWidth == width) 157 return; 158 159 m_optionsWidth = width; 160 if (parent()) 161 setNeedsLayoutAndPrefWidthsRecalc(); 162 } 163 164 void RenderMenuList::updateFromElement() 165 { 166 if (m_optionsChanged) { 167 updateOptionsWidth(); 168 m_optionsChanged = false; 169 } 170 171 if (m_popupIsVisible) 172 m_popup->updateFromElement(); 173 else 174 setTextFromOption(toSelectElement(static_cast<Element*>(node()))->selectedIndex()); 175 } 176 177 void RenderMenuList::setTextFromOption(int optionIndex) 178 { 179 SelectElement* select = toSelectElement(static_cast<Element*>(node())); 180 const Vector<Element*>& listItems = select->listItems(); 181 int size = listItems.size(); 182 183 int i = select->optionToListIndex(optionIndex); 184 String text = ""; 185 if (i >= 0 && i < size) { 186 if (OptionElement* optionElement = toOptionElement(listItems[i])) 187 text = optionElement->textIndentedToRespectGroupLabel(); 188 } 189 190 setText(text.stripWhiteSpace()); 191 } 192 193 void RenderMenuList::setText(const String& s) 194 { 195 if (s.isEmpty()) { 196 if (!m_buttonText || !m_buttonText->isBR()) { 197 if (m_buttonText) 198 m_buttonText->destroy(); 199 m_buttonText = new (renderArena()) RenderBR(document()); 200 m_buttonText->setStyle(style()); 201 addChild(m_buttonText); 202 } 203 } else { 204 if (m_buttonText && !m_buttonText->isBR()) 205 m_buttonText->setText(s.impl()); 206 else { 207 if (m_buttonText) 208 m_buttonText->destroy(); 209 m_buttonText = new (renderArena()) RenderText(document(), s.impl()); 210 m_buttonText->setStyle(style()); 211 addChild(m_buttonText); 212 } 213 adjustInnerStyle(); 214 } 215 } 216 217 String RenderMenuList::text() const 218 { 219 return m_buttonText ? m_buttonText->text() : 0; 220 } 221 222 IntRect RenderMenuList::controlClipRect(int tx, int ty) const 223 { 224 // Clip to the intersection of the content box and the content box for the inner box 225 // This will leave room for the arrows which sit in the inner box padding, 226 // and if the inner box ever spills out of the outer box, that will get clipped too. 227 IntRect outerBox(tx + borderLeft() + paddingLeft(), 228 ty + borderTop() + paddingTop(), 229 contentWidth(), 230 contentHeight()); 231 232 IntRect innerBox(tx + m_innerBlock->x() + m_innerBlock->paddingLeft(), 233 ty + m_innerBlock->y() + m_innerBlock->paddingTop(), 234 m_innerBlock->contentWidth(), 235 m_innerBlock->contentHeight()); 236 237 return intersection(outerBox, innerBox); 238 } 239 240 void RenderMenuList::calcPrefWidths() 241 { 242 m_minPrefWidth = 0; 243 m_maxPrefWidth = 0; 244 245 if (style()->width().isFixed() && style()->width().value() > 0) 246 m_minPrefWidth = m_maxPrefWidth = calcContentBoxWidth(style()->width().value()); 247 else 248 m_maxPrefWidth = max(m_optionsWidth, theme()->minimumMenuListSize(style())) + m_innerBlock->paddingLeft() + m_innerBlock->paddingRight(); 249 250 if (style()->minWidth().isFixed() && style()->minWidth().value() > 0) { 251 m_maxPrefWidth = max(m_maxPrefWidth, calcContentBoxWidth(style()->minWidth().value())); 252 m_minPrefWidth = max(m_minPrefWidth, calcContentBoxWidth(style()->minWidth().value())); 253 } else if (style()->width().isPercent() || (style()->width().isAuto() && style()->height().isPercent())) 254 m_minPrefWidth = 0; 255 else 256 m_minPrefWidth = m_maxPrefWidth; 257 258 if (style()->maxWidth().isFixed() && style()->maxWidth().value() != undefinedLength) { 259 m_maxPrefWidth = min(m_maxPrefWidth, calcContentBoxWidth(style()->maxWidth().value())); 260 m_minPrefWidth = min(m_minPrefWidth, calcContentBoxWidth(style()->maxWidth().value())); 261 } 262 263 int toAdd = paddingLeft() + paddingRight() + borderLeft() + borderRight(); 264 m_minPrefWidth += toAdd; 265 m_maxPrefWidth += toAdd; 266 267 setPrefWidthsDirty(false); 268 } 269 270 void RenderMenuList::showPopup() 271 { 272 if (m_popupIsVisible) 273 return; 274 275 // Create m_innerBlock here so it ends up as the first child. 276 // This is important because otherwise we might try to create m_innerBlock 277 // inside the showPopup call and it would fail. 278 createInnerBlock(); 279 if (!m_popup) 280 m_popup = PopupMenu::create(this); 281 SelectElement* select = toSelectElement(static_cast<Element*>(node())); 282 m_popupIsVisible = true; 283 284 // Compute the top left taking transforms into account, but use 285 // the actual width of the element to size the popup. 286 FloatPoint absTopLeft = localToAbsolute(FloatPoint(), false, true); 287 IntRect absBounds = absoluteBoundingBoxRect(); 288 absBounds.setLocation(roundedIntPoint(absTopLeft)); 289 m_popup->show(absBounds, document()->view(), 290 select->optionToListIndex(select->selectedIndex())); 291 } 292 293 void RenderMenuList::hidePopup() 294 { 295 if (m_popup) 296 m_popup->hide(); 297 } 298 299 void RenderMenuList::valueChanged(unsigned listIndex, bool fireOnChange) 300 { 301 // Check to ensure a page navigation has not occurred while 302 // the popup was up. 303 Document* doc = static_cast<Element*>(node())->document(); 304 if (!doc || doc != doc->frame()->document()) 305 return; 306 307 SelectElement* select = toSelectElement(static_cast<Element*>(node())); 308 select->setSelectedIndexByUser(select->listToOptionIndex(listIndex), true, fireOnChange); 309 } 310 311 void RenderMenuList::didSetSelectedIndex() 312 { 313 int index = selectedIndex(); 314 if (m_lastSelectedIndex == index) 315 return; 316 317 m_lastSelectedIndex = index; 318 319 if (AXObjectCache::accessibilityEnabled()) 320 document()->axObjectCache()->postNotification(this, AXObjectCache::AXMenuListValueChanged, true, PostSynchronously); 321 } 322 323 String RenderMenuList::itemText(unsigned listIndex) const 324 { 325 SelectElement* select = toSelectElement(static_cast<Element*>(node())); 326 const Vector<Element*>& listItems = select->listItems(); 327 if (listIndex >= listItems.size()) 328 return String(); 329 Element* element = listItems[listIndex]; 330 if (OptionGroupElement* optionGroupElement = toOptionGroupElement(element)) 331 return optionGroupElement->groupLabelText(); 332 else if (OptionElement* optionElement = toOptionElement(element)) 333 return optionElement->textIndentedToRespectGroupLabel(); 334 return String(); 335 } 336 337 String RenderMenuList::itemToolTip(unsigned listIndex) const 338 { 339 SelectElement* select = toSelectElement(static_cast<Element*>(node())); 340 const Vector<Element*>& listItems = select->listItems(); 341 if (listIndex >= listItems.size()) 342 return String(); 343 Element* element = listItems[listIndex]; 344 return element->title(); 345 } 346 347 bool RenderMenuList::itemIsEnabled(unsigned listIndex) const 348 { 349 SelectElement* select = toSelectElement(static_cast<Element*>(node())); 350 const Vector<Element*>& listItems = select->listItems(); 351 if (listIndex >= listItems.size()) 352 return false; 353 Element* element = listItems[listIndex]; 354 if (!isOptionElement(element)) 355 return false; 356 357 bool groupEnabled = true; 358 if (Element* parentElement = element->parentElement()) { 359 if (isOptionGroupElement(parentElement)) 360 groupEnabled = parentElement->isEnabledFormControl(); 361 } 362 if (!groupEnabled) 363 return false; 364 365 return element->isEnabledFormControl(); 366 } 367 368 PopupMenuStyle RenderMenuList::itemStyle(unsigned listIndex) const 369 { 370 SelectElement* select = toSelectElement(static_cast<Element*>(node())); 371 const Vector<Element*>& listItems = select->listItems(); 372 if (listIndex >= listItems.size()) { 373 // If we are making an out of bounds access, then we want to use the style 374 // of a different option element (index 0). However, if there isn't an option element 375 // before at index 0, we fall back to the menu's style. 376 if (!listIndex) 377 return menuStyle(); 378 379 // Try to retrieve the style of an option element we know exists (index 0). 380 listIndex = 0; 381 } 382 Element* element = listItems[listIndex]; 383 384 RenderStyle* style = element->renderStyle() ? element->renderStyle() : element->computedStyle(); 385 return style ? PopupMenuStyle(style->color(), itemBackgroundColor(listIndex), style->font(), style->visibility() == VISIBLE, style->textIndent(), style->direction()) : menuStyle(); 386 } 387 388 Color RenderMenuList::itemBackgroundColor(unsigned listIndex) const 389 { 390 SelectElement* select = toSelectElement(static_cast<Element*>(node())); 391 const Vector<Element*>& listItems = select->listItems(); 392 if (listIndex >= listItems.size()) 393 return style()->backgroundColor(); 394 Element* element = listItems[listIndex]; 395 396 Color backgroundColor; 397 if (element->renderStyle()) 398 backgroundColor = element->renderStyle()->backgroundColor(); 399 // If the item has an opaque background color, return that. 400 if (!backgroundColor.hasAlpha()) 401 return backgroundColor; 402 403 // Otherwise, the item's background is overlayed on top of the menu background. 404 backgroundColor = style()->backgroundColor().blend(backgroundColor); 405 if (!backgroundColor.hasAlpha()) 406 return backgroundColor; 407 408 // If the menu background is not opaque, then add an opaque white background behind. 409 return Color(Color::white).blend(backgroundColor); 410 } 411 412 PopupMenuStyle RenderMenuList::menuStyle() const 413 { 414 RenderStyle* s = m_innerBlock ? m_innerBlock->style() : style(); 415 return PopupMenuStyle(s->color(), s->backgroundColor(), s->font(), s->visibility() == VISIBLE, s->textIndent(), s->direction()); 416 } 417 418 HostWindow* RenderMenuList::hostWindow() const 419 { 420 return document()->view()->hostWindow(); 421 } 422 423 PassRefPtr<Scrollbar> RenderMenuList::createScrollbar(ScrollbarClient* client, ScrollbarOrientation orientation, ScrollbarControlSize controlSize) 424 { 425 RefPtr<Scrollbar> widget; 426 bool hasCustomScrollbarStyle = style()->hasPseudoStyle(SCROLLBAR); 427 if (hasCustomScrollbarStyle) 428 widget = RenderScrollbar::createCustomScrollbar(client, orientation, this); 429 else 430 widget = Scrollbar::createNativeScrollbar(client, orientation, controlSize); 431 return widget.release(); 432 } 433 434 int RenderMenuList::clientInsetLeft() const 435 { 436 return 0; 437 } 438 439 int RenderMenuList::clientInsetRight() const 440 { 441 return 0; 442 } 443 444 int RenderMenuList::clientPaddingLeft() const 445 { 446 return paddingLeft(); 447 } 448 449 const int endOfLinePadding = 2; 450 int RenderMenuList::clientPaddingRight() const 451 { 452 if (style()->appearance() == MenulistPart || style()->appearance() == MenulistButtonPart) { 453 // For these appearance values, the theme applies padding to leave room for the 454 // drop-down button. But leaving room for the button inside the popup menu itself 455 // looks strange, so we return a small default padding to avoid having a large empty 456 // space appear on the side of the popup menu. 457 return endOfLinePadding; 458 } 459 460 // If the appearance isn't MenulistPart, then the select is styled (non-native), so 461 // we want to return the user specified padding. 462 return paddingRight(); 463 } 464 465 int RenderMenuList::listSize() const 466 { 467 SelectElement* select = toSelectElement(static_cast<Element*>(node())); 468 return select->listItems().size(); 469 } 470 471 int RenderMenuList::selectedIndex() const 472 { 473 SelectElement* select = toSelectElement(static_cast<Element*>(node())); 474 return select->optionToListIndex(select->selectedIndex()); 475 } 476 477 void RenderMenuList::popupDidHide() 478 { 479 m_popupIsVisible = false; 480 } 481 482 bool RenderMenuList::itemIsSeparator(unsigned listIndex) const 483 { 484 SelectElement* select = toSelectElement(static_cast<Element*>(node())); 485 const Vector<Element*>& listItems = select->listItems(); 486 if (listIndex >= listItems.size()) 487 return false; 488 Element* element = listItems[listIndex]; 489 return element->hasTagName(hrTag); 490 } 491 492 bool RenderMenuList::itemIsLabel(unsigned listIndex) const 493 { 494 SelectElement* select = toSelectElement(static_cast<Element*>(node())); 495 const Vector<Element*>& listItems = select->listItems(); 496 if (listIndex >= listItems.size()) 497 return false; 498 Element* element = listItems[listIndex]; 499 return isOptionGroupElement(element); 500 } 501 502 bool RenderMenuList::itemIsSelected(unsigned listIndex) const 503 { 504 SelectElement* select = toSelectElement(static_cast<Element*>(node())); 505 const Vector<Element*>& listItems = select->listItems(); 506 if (listIndex >= listItems.size()) 507 return false; 508 Element* element = listItems[listIndex]; 509 if (OptionElement* optionElement = toOptionElement(element)) 510 return optionElement->selected(); 511 return false; 512 } 513 514 void RenderMenuList::setTextFromItem(unsigned listIndex) 515 { 516 SelectElement* select = toSelectElement(static_cast<Element*>(node())); 517 setTextFromOption(select->listToOptionIndex(listIndex)); 518 } 519 520 FontSelector* RenderMenuList::fontSelector() const 521 { 522 return document()->styleSelector()->fontSelector(); 523 } 524 525 } 526