blob: 1c708bbf319ff726827b117589cf3db5816334c2 [file] [log] [blame]
/**
* This file is part of the select element renderer in WebCore.
*
* Copyright (C) 2006 Apple Computer, Inc.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Library General Public License for more details.
*
* You should have received a copy of the GNU Library General Public License
* along with this library; see the file COPYING.LIB. If not, write to
* the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
* Boston, MA 02111-1307, USA.
*
*/
#include "config.h"
#include "RenderListBox.h"
#include "Document.h"
#include "EventHandler.h"
#include "EventNames.h"
#include "Frame.h"
#include "FrameView.h"
#include "GraphicsContext.h"
#include "HTMLNames.h"
#include "HTMLOptGroupElement.h"
#include "HTMLOptionElement.h"
#include "HTMLSelectElement.h"
#include "HitTestResult.h"
#include "PlatformScrollBar.h"
#include "RenderBR.h"
#include "RenderText.h"
#include "RenderTheme.h"
#include "RenderView.h"
#include "TextStyle.h"
#include <math.h>
using namespace std;
namespace WebCore {
using namespace HTMLNames;
using namespace EventNames;
const int optionsSpacingMiddle = 1;
const int optionsSpacingLeft = 2;
RenderListBox::RenderListBox(HTMLSelectElement* element)
: RenderBlock(element)
, m_optionsChanged(true)
, m_optionsWidth(0)
, m_indexOffset(0)
, m_selectionChanged(true)
, m_vBar(0)
{
}
RenderListBox::~RenderListBox()
{
if (m_vBar && m_vBar->isWidget()) {
element()->document()->view()->removeChild(static_cast<PlatformScrollbar*>(m_vBar));
m_vBar->deref();
}
}
void RenderListBox::setStyle(RenderStyle* style)
{
RenderBlock::setStyle(style);
setReplaced(isInline());
}
void RenderListBox::updateFromElement()
{
HTMLSelectElement* select = static_cast<HTMLSelectElement*>(node());
if (m_optionsChanged) {
const Vector<HTMLElement*>& listItems = select->listItems();
int size = listItems.size();
float width = 0;
TextStyle textStyle(0, 0, 0, false, false, false, false);
for (int i = 0; i < size; ++i) {
HTMLElement* element = listItems[i];
String text;
Font itemFont = style()->font();
if (element->hasTagName(optionTag))
text = static_cast<HTMLOptionElement*>(element)->optionText();
else if (element->hasTagName(optgroupTag)) {
text = static_cast<HTMLOptGroupElement*>(element)->groupLabelText();
FontDescription d = itemFont.fontDescription();
d.setBold(true);
itemFont = Font(d, itemFont.letterSpacing(), itemFont.wordSpacing());
itemFont.update();
}
if (!text.isEmpty()) {
float textWidth = itemFont.floatWidth(TextRun(text.impl()), textStyle);
width = max(width, textWidth);
}
}
m_optionsWidth = static_cast<int>(ceilf(width));
m_optionsChanged = false;
setNeedsLayoutAndMinMaxRecalc();
}
int firstIndex = select->optionToListIndex(select->selectedIndex());
int lastIndex = select->lastSelectedListIndex();
if (firstIndex >= 0 && !listIndexIsVisible(firstIndex) && !listIndexIsVisible(lastIndex))
scrollToRevealElementAtListIndex(firstIndex);
}
void RenderListBox::calcMinMaxWidth()
{
if (!m_vBar) {
if (Scrollbar::hasPlatformScrollbars()) {
PlatformScrollbar* widget = new PlatformScrollbar(this, VerticalScrollbar, SmallScrollbar);
widget->ref();
node()->document()->view()->addChild(widget);
m_vBar = widget;
}
}
if (m_optionsChanged)
updateFromElement();
m_minWidth = 0;
m_maxWidth = 0;
if (style()->width().isFixed() && style()->width().value() > 0)
m_minWidth = m_maxWidth = calcContentBoxWidth(style()->width().value());
else {
m_maxWidth = m_optionsWidth;
if (m_vBar)
m_maxWidth += m_vBar->width();
}
if (style()->minWidth().isFixed() && style()->minWidth().value() > 0) {
m_maxWidth = max(m_maxWidth, calcContentBoxWidth(style()->minWidth().value()));
m_minWidth = max(m_minWidth, calcContentBoxWidth(style()->minWidth().value()));
} else if (style()->width().isPercent() || (style()->width().isAuto() && style()->height().isPercent()))
m_minWidth = 0;
else
m_minWidth = m_maxWidth;
if (style()->maxWidth().isFixed() && style()->maxWidth().value() != undefinedLength) {
m_maxWidth = min(m_maxWidth, calcContentBoxWidth(style()->maxWidth().value()));
m_minWidth = min(m_minWidth, calcContentBoxWidth(style()->maxWidth().value()));
}
int toAdd = paddingLeft() + paddingRight() + borderLeft() + borderRight();
m_minWidth += toAdd;
m_maxWidth += toAdd;
setMinMaxKnown();
}
const int minSize = 4;
const int minDefaultSize = 10;
int RenderListBox::size() const
{
HTMLSelectElement* select = static_cast<HTMLSelectElement*>(node());
int specifiedSize = select->size();
if (specifiedSize > 1)
return max(minSize, specifiedSize);
return min(max(numItems(), minSize), minDefaultSize);
}
int RenderListBox::numItems() const
{
HTMLSelectElement* select = static_cast<HTMLSelectElement*>(node());
return select->listItems().size();
}
void RenderListBox::calcHeight()
{
int toAdd = paddingTop() + paddingBottom() + borderTop() + borderBottom();
int itemHeight = style()->font().height() + optionsSpacingMiddle;
m_height = itemHeight * size() + toAdd;
RenderBlock::calcHeight();
if (m_vBar) {
m_vBar->setEnabled(size() < numItems());
m_vBar->setSteps(itemHeight, itemHeight);
m_vBar->setProportion(m_height - toAdd, itemHeight * numItems());
}
}
const int baselineAdjustment = 7;
short RenderListBox::baselinePosition(bool b, bool isRootLineBox) const
{
// FIXME: This hardcoded baselineAdjustment is what we used to do for the old widget, but I'm not sure this is right for the new control.
return height() + marginTop() + marginBottom() - baselineAdjustment;
}
IntRect RenderListBox::itemBoundingBoxRect(int tx, int ty, int index)
{
return IntRect (tx + borderLeft() + paddingLeft(),
ty + borderTop() + paddingTop() + ((style()->font().height() + optionsSpacingMiddle) * (index - m_indexOffset)),
absoluteBoundingBoxRect().width() - borderLeft() - borderRight() - paddingLeft() - paddingRight(),
style()->font().height() + optionsSpacingMiddle);
}
void RenderListBox::paintObject(PaintInfo& paintInfo, int tx, int ty)
{
// Push a clip.
IntRect clipRect(tx + borderLeft(), ty + borderTop(),
width() - borderLeft() - borderRight(), height() - borderBottom() - borderTop());
if (paintInfo.phase == PaintPhaseForeground || paintInfo.phase == PaintPhaseChildBlockBackgrounds) {
if (clipRect.width() == 0 || clipRect.height() == 0)
return;
paintInfo.context->save();
paintInfo.context->clip(clipRect);
}
HTMLSelectElement* select = static_cast<HTMLSelectElement*>(node());
int listItemsSize = select->listItems().size();
if (paintInfo.phase == PaintPhaseForeground) {
int index = m_indexOffset;
while (index < listItemsSize && index < m_indexOffset + size()) {
paintItemForeground(paintInfo, tx, ty, index);
index++;
}
}
// Paint the children.
RenderBlock::paintObject(paintInfo, tx, ty);
if (paintInfo.phase == PaintPhaseBlockBackground) {
int index = m_indexOffset;
while (index < listItemsSize && index < m_indexOffset + size()) {
paintItemBackground(paintInfo, tx, ty, index);
index++;
}
paintScrollbar(paintInfo);
}
// Pop the clip.
if (paintInfo.phase == PaintPhaseForeground || paintInfo.phase == PaintPhaseChildBlockBackgrounds)
paintInfo.context->restore();
}
void RenderListBox::paintScrollbar(PaintInfo& paintInfo)
{
if (m_vBar) {
IntRect absBounds = absoluteBoundingBoxRect();
IntRect scrollRect(absBounds.right() - borderRight() - m_vBar->width(),
absBounds.y() + borderTop(),
m_vBar->width(),
absBounds.height() - (borderTop() + borderBottom()));
m_vBar->setRect(scrollRect);
m_vBar->paint(paintInfo.context, scrollRect);
}
}
void RenderListBox::paintItemForeground(PaintInfo& paintInfo, int tx, int ty, int listIndex)
{
HTMLSelectElement* select = static_cast<HTMLSelectElement*>(node());
const Vector<HTMLElement*>& listItems = select->listItems();
HTMLElement* element = listItems[listIndex];
String itemText;
if (element->hasTagName(optionTag))
itemText = static_cast<HTMLOptionElement*>(element)->optionText();
else if (element->hasTagName(optgroupTag))
itemText = static_cast<HTMLOptGroupElement*>(element)->groupLabelText();
TextRun textRun(itemText.characters(), itemText.length());
// Determine where the item text should be placed
IntRect r = itemBoundingBoxRect(tx, ty, listIndex);
r.move(optionsSpacingLeft, style()->font().ascent());
RenderStyle* itemStyle = element->renderStyle();
if (!itemStyle)
itemStyle = style();
Color textColor = element->renderStyle() ? element->renderStyle()->color() : style()->color();
if (element->hasTagName(optionTag) && static_cast<HTMLOptionElement*>(element)->selected()) {
if (document()->frame()->isActive() && document()->focusedNode() == node())
textColor = theme()->activeListBoxSelectionForegroundColor();
// Honor the foreground color for disabled items
else if (!element->disabled())
textColor = theme()->inactiveListBoxSelectionForegroundColor();
}
paintInfo.context->setFillColor(textColor);
Font itemFont = style()->font();
if (element->hasTagName(optgroupTag)) {
FontDescription d = itemFont.fontDescription();
d.setBold(true);
itemFont = Font(d, itemFont.letterSpacing(), itemFont.wordSpacing());
itemFont.update();
}
paintInfo.context->setFont(itemFont);
// Draw the item text
if (itemStyle->visibility() != HIDDEN)
paintInfo.context->drawText(textRun, r.location());
}
void RenderListBox::paintItemBackground(PaintInfo& paintInfo, int tx, int ty, int listIndex)
{
HTMLSelectElement* select = static_cast<HTMLSelectElement*>(node());
const Vector<HTMLElement*>& listItems = select->listItems();
HTMLElement* element = listItems[listIndex];
Color backColor;
if (element->hasTagName(optionTag) && static_cast<HTMLOptionElement*>(element)->selected()) {
if (document()->frame()->isActive() && document()->focusedNode() == node())
backColor = theme()->activeListBoxSelectionBackgroundColor();
else
backColor = theme()->inactiveListBoxSelectionBackgroundColor();
} else
backColor = element->renderStyle() ? element->renderStyle()->backgroundColor() : style()->backgroundColor();
// Draw the background for this list box item
if (!element->renderStyle() || element->renderStyle()->visibility() != HIDDEN)
paintInfo.context->fillRect(itemBoundingBoxRect(tx, ty, listIndex), backColor);
}
bool RenderListBox::isPointInScrollbar(HitTestResult& result, int _x, int _y, int _tx, int _ty)
{
if (!m_vBar)
return false;
IntRect vertRect(_tx + width() - borderRight() - m_vBar->width(),
_ty + borderTop() - borderTopExtra(),
m_vBar->width(),
height() + borderTopExtra() + borderBottomExtra() - borderTop() - borderBottom());
if (vertRect.contains(_x, _y)) {
result.setScrollbar(m_vBar->isWidget() ? static_cast<PlatformScrollbar*>(m_vBar) : 0);
return true;
}
return false;
}
int RenderListBox::listIndexAtOffset(int offsetX, int offsetY)
{
HTMLSelectElement* select = static_cast<HTMLSelectElement*>(node());
const Vector<HTMLElement*>& listItems = select->listItems();
if ((int)listItems.size() > 0) {
int newOffset = max(0, offsetY / (style()->font().height() + optionsSpacingMiddle)) + m_indexOffset;
newOffset = max(0, min((int)listItems.size() - 1, newOffset));
int scrollbarWidth = m_vBar ? m_vBar->width() : 0;
if (offsetX >= borderLeft() + paddingLeft() && offsetX < absoluteBoundingBoxRect().width() - borderRight() - paddingRight() - scrollbarWidth)
return newOffset;
}
return -1;
}
void RenderListBox::autoscroll()
{
IntPoint pos = document()->frame()->view()->windowToContents(document()->frame()->eventHandler()->currentMousePosition());
int rx = 0;
int ry = 0;
absolutePosition(rx, ry);
int offsetX = pos.x() - rx;
int offsetY = pos.y() - ry;
int endIndex = -1;
int rows = size();
int offset = m_indexOffset;
if (offsetY < 0 && scrollToRevealElementAtListIndex(offset - 1))
endIndex = offset - 1;
else if (offsetY > absoluteBoundingBoxRect().height() && scrollToRevealElementAtListIndex(offset + rows))
endIndex = offset + rows - 1;
else
endIndex = listIndexAtOffset(offsetX, offsetY);
HTMLSelectElement* select = static_cast<HTMLSelectElement*>(node());
if (endIndex >= 0 && select) {
if (!select->multiple())
select->setActiveSelectionAnchorIndex(endIndex);
select->setActiveSelectionEndIndex(endIndex);
select->updateListBoxSelection(!select->multiple());
repaint();
}
}
void RenderListBox::stopAutoscroll()
{
if ( HTMLSelectElement* select = static_cast<HTMLSelectElement*>(node()))
select->listBoxOnChange();
}
bool RenderListBox::scrollToRevealElementAtListIndex(int index)
{
HTMLSelectElement* select = static_cast<HTMLSelectElement*>(node());
const Vector<HTMLElement*>& listItems = select->listItems();
if (index < 0 || index > (int)listItems.size() - 1 || listIndexIsVisible(index))
return false;
int newOffset;
if (index < m_indexOffset)
newOffset = index;
else
newOffset = index - size() + 1;
if (m_vBar) {
IntRect rect = absoluteBoundingBoxRect();
m_vBar->setValue(itemBoundingBoxRect(rect.x(), rect.y(), newOffset + m_indexOffset).y() - rect.y());
}
m_indexOffset = newOffset;
return true;
}
bool RenderListBox::listIndexIsVisible(int index)
{
return index >= m_indexOffset && index < m_indexOffset + size();
}
bool RenderListBox::scroll(ScrollDirection direction, ScrollGranularity granularity, float multiplier)
{
return m_vBar && m_vBar->scroll(direction, granularity, multiplier);
}
void RenderListBox::valueChanged(unsigned listIndex)
{
HTMLSelectElement* select = static_cast<HTMLSelectElement*>(node());
select->setSelectedIndex(select->listToOptionIndex(listIndex));
select->onChange();
}
void RenderListBox::valueChanged(Scrollbar*)
{
if (m_vBar) {
int newOffset = max(0, m_vBar->value() / (style()->font().height() + optionsSpacingMiddle));
if (newOffset != m_indexOffset) {
m_indexOffset = newOffset;
repaint();
// Fire the scroll DOM event.
EventTargetNodeCast(node())->dispatchHTMLEvent(scrollEvent, true, false);
}
}
}
IntRect RenderListBox::windowClipRect() const
{
return view()->frameView()->windowClipRectForLayer(enclosingLayer(), true);
}
} // namespace WebCore