Refactoring: Replace Element::disabled and isEnabledFormControl with isDisabledFormCo...
[WebKit-https.git] / Source / WebCore / html / shadow / SliderThumbElement.cpp
1 /*
2  * Copyright (C) 2006, 2007, 2008, 2009 Apple Inc. All rights reserved.
3  * Copyright (C) 2010 Google Inc. All rights reserved.
4  *
5  * Redistribution and use in source and binary forms, with or without
6  * modification, are permitted provided that the following conditions are
7  * met:
8  *
9  *     * Redistributions of source code must retain the above copyright
10  * notice, this list of conditions and the following disclaimer.
11  *     * Redistributions in binary form must reproduce the above
12  * copyright notice, this list of conditions and the following disclaimer
13  * in the documentation and/or other materials provided with the
14  * distribution.
15  *     * Neither the name of Google Inc. nor the names of its
16  * contributors may be used to endorse or promote products derived from
17  * this software without specific prior written permission.
18  *
19  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
22  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23  * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
26  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
27  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30  */
31
32
33 #include "config.h"
34 #include "SliderThumbElement.h"
35
36 #include "CSSValueKeywords.h"
37 #include "ElementShadow.h"
38 #include "Event.h"
39 #include "Frame.h"
40 #include "HTMLInputElement.h"
41 #include "HTMLParserIdioms.h"
42 #include "MouseEvent.h"
43 #include "RenderFlexibleBox.h"
44 #include "RenderSlider.h"
45 #include "RenderTheme.h"
46 #include "ShadowRoot.h"
47 #include "StepRange.h"
48 #include <wtf/MathExtras.h>
49
50 using namespace std;
51
52 namespace WebCore {
53
54 using namespace HTMLNames;
55
56 inline static Decimal sliderPosition(HTMLInputElement* element)
57 {
58     const StepRange stepRange(element->createStepRange(RejectAny));
59     const Decimal oldValue = parseToDecimalForNumberType(element->value(), stepRange.defaultValue());
60     return stepRange.proportionFromValue(stepRange.clampValue(oldValue));
61 }
62
63 inline static bool hasVerticalAppearance(HTMLInputElement* input)
64 {
65     ASSERT(input->renderer());
66     RenderStyle* sliderStyle = input->renderer()->style();
67
68 #if ENABLE(VIDEO)
69     if (sliderStyle->appearance() == MediaVolumeSliderPart && input->renderer()->theme()->usesVerticalVolumeSlider())
70         return true;
71 #endif
72
73     return sliderStyle->appearance() == SliderVerticalPart;
74 }
75
76 SliderThumbElement* sliderThumbElementOf(Node* node)
77 {
78     ASSERT(node);
79     ShadowRoot* shadow = node->toInputElement()->userAgentShadowRoot();
80     ASSERT(shadow);
81     Node* thumb = shadow->firstChild()->firstChild()->firstChild();
82     ASSERT(thumb);
83     return toSliderThumbElement(thumb);
84 }
85
86 HTMLElement* sliderTrackElementOf(Node* node)
87 {
88     ASSERT(node);
89     ShadowRoot* shadow = node->toInputElement()->userAgentShadowRoot();
90     ASSERT(shadow);
91     Node* track = shadow->firstChild()->firstChild();
92     ASSERT(track);
93     return toHTMLElement(track);
94 }
95
96 // --------------------------------
97
98 RenderSliderThumb::RenderSliderThumb(SliderThumbElement* element)
99     : RenderBlock(element)
100 {
101 }
102
103 void RenderSliderThumb::updateAppearance(RenderStyle* parentStyle)
104 {
105     if (parentStyle->appearance() == SliderVerticalPart)
106         style()->setAppearance(SliderThumbVerticalPart);
107     else if (parentStyle->appearance() == SliderHorizontalPart)
108         style()->setAppearance(SliderThumbHorizontalPart);
109     else if (parentStyle->appearance() == MediaSliderPart)
110         style()->setAppearance(MediaSliderThumbPart);
111     else if (parentStyle->appearance() == MediaVolumeSliderPart)
112         style()->setAppearance(MediaVolumeSliderThumbPart);
113     else if (parentStyle->appearance() == MediaFullScreenVolumeSliderPart)
114         style()->setAppearance(MediaFullScreenVolumeSliderThumbPart);
115     if (style()->hasAppearance())
116         theme()->adjustSliderThumbSize(style(), toElement(node()));
117 }
118
119 bool RenderSliderThumb::isSliderThumb() const
120 {
121     return true;
122 }
123
124 // --------------------------------
125
126 // FIXME: Find a way to cascade appearance and adjust heights, and get rid of this class.
127 // http://webkit.org/b/62535
128 class RenderSliderContainer : public RenderFlexibleBox {
129 public:
130     RenderSliderContainer(SliderContainerElement* element)
131         : RenderFlexibleBox(element) { }
132 public:
133     virtual void computeLogicalHeight(LayoutUnit logicalHeight, LayoutUnit logicalTop, LogicalExtentComputedValues&) const OVERRIDE;
134
135 private:
136     virtual void layout() OVERRIDE;
137 };
138
139 void RenderSliderContainer::computeLogicalHeight(LayoutUnit logicalHeight, LayoutUnit logicalTop, LogicalExtentComputedValues& computedValues) const
140 {
141     HTMLInputElement* input = node()->shadowHost()->toInputElement();
142     bool isVertical = hasVerticalAppearance(input);
143
144 #if ENABLE(DATALIST_ELEMENT)
145     if (input->renderer()->isSlider() && !isVertical && input->list()) {
146         int offsetFromCenter = theme()->sliderTickOffsetFromTrackCenter();
147         LayoutUnit trackHeight = 0;
148         if (offsetFromCenter < 0)
149             trackHeight = -2 * offsetFromCenter;
150         else {
151             int tickLength = theme()->sliderTickSize().height();
152             trackHeight = 2 * (offsetFromCenter + tickLength);
153         }
154         float zoomFactor = style()->effectiveZoom();
155         if (zoomFactor != 1.0)
156             trackHeight *= zoomFactor;
157
158         RenderBox::computeLogicalHeight(trackHeight, logicalTop, computedValues);
159         return;
160     }
161 #endif
162     if (isVertical)
163         logicalHeight = RenderSlider::defaultTrackLength;
164     RenderBox::computeLogicalHeight(logicalHeight, logicalTop, computedValues);
165 }
166
167 void RenderSliderContainer::layout()
168 {
169     HTMLInputElement* input = node()->shadowHost()->toInputElement();
170     bool isVertical = hasVerticalAppearance(input);
171     style()->setFlexDirection(isVertical ? FlowColumn : FlowRow);
172     TextDirection oldTextDirection = style()->direction();
173     if (isVertical) {
174         // FIXME: Work around rounding issues in RTL vertical sliders. We want them to
175         // render identically to LTR vertical sliders. We can remove this work around when
176         // subpixel rendering is enabled on all ports.
177         style()->setDirection(LTR);
178     }
179
180     RenderBox* thumb = input->sliderThumbElement() ? input->sliderThumbElement()->renderBox() : 0;
181     RenderBox* track = input->sliderTrackElement() ? input->sliderTrackElement()->renderBox() : 0;
182     // Force a layout to reset the position of the thumb so the code below doesn't move the thumb to the wrong place.
183     // FIXME: Make a custom Render class for the track and move the thumb positioning code there.
184     if (track)
185         track->setChildNeedsLayout(true, MarkOnlyThis);
186
187     RenderFlexibleBox::layout();
188
189     style()->setDirection(oldTextDirection);
190     // These should always exist, unless someone mutates the shadow DOM (e.g., in the inspector).
191     if (!thumb || !track)
192         return;
193
194     double percentageOffset = sliderPosition(input).toDouble();
195     LayoutUnit availableExtent = isVertical ? track->contentHeight() : track->contentWidth();
196     availableExtent -= isVertical ? thumb->height() : thumb->width();
197     LayoutUnit offset = percentageOffset * availableExtent;
198     LayoutPoint thumbLocation = thumb->location();
199     if (isVertical)
200         thumbLocation.setY(thumbLocation.y() + track->contentHeight() - thumb->height() - offset);
201     else if (style()->isLeftToRightDirection())
202         thumbLocation.setX(thumbLocation.x() + offset);
203     else
204         thumbLocation.setX(thumbLocation.x() - offset);
205     thumb->setLocation(thumbLocation);
206 }
207
208 // --------------------------------
209
210 void SliderThumbElement::setPositionFromValue()
211 {
212     // Since the code to calculate position is in the RenderSliderThumb layout
213     // path, we don't actually update the value here. Instead, we poke at the
214     // renderer directly to trigger layout.
215     if (renderer())
216         renderer()->setNeedsLayout(true);
217 }
218
219 RenderObject* SliderThumbElement::createRenderer(RenderArena* arena, RenderStyle*)
220 {
221     return new (arena) RenderSliderThumb(this);
222 }
223
224 bool SliderThumbElement::isDisabledFormControl() const
225 {
226     return hostInput()->isDisabledFormControl();
227 }
228
229 bool SliderThumbElement::matchesReadOnlyPseudoClass() const
230 {
231     return hostInput()->matchesReadOnlyPseudoClass();
232 }
233
234 bool SliderThumbElement::matchesReadWritePseudoClass() const
235 {
236     return hostInput()->matchesReadWritePseudoClass();
237 }
238
239 Node* SliderThumbElement::focusDelegate()
240 {
241     return hostInput();
242 }
243
244 void SliderThumbElement::dragFrom(const LayoutPoint& point)
245 {
246     setPositionFromPoint(point);
247     startDragging();
248 }
249
250 void SliderThumbElement::setPositionFromPoint(const LayoutPoint& point)
251 {
252     HTMLInputElement* input = hostInput();
253     HTMLElement* trackElement = sliderTrackElementOf(input);
254
255     if (!input->renderer() || !renderBox() || !trackElement->renderBox())
256         return;
257
258     input->setTextAsOfLastFormControlChangeEvent(input->value());
259     LayoutPoint offset = roundedLayoutPoint(input->renderer()->absoluteToLocal(point, UseTransforms));
260     bool isVertical = hasVerticalAppearance(input);
261     bool isLeftToRightDirection = renderBox()->style()->isLeftToRightDirection();
262     LayoutUnit trackSize;
263     LayoutUnit position;
264     LayoutUnit currentPosition;
265     // We need to calculate currentPosition from absolute points becaue the
266     // renderer for this node is usually on a layer and renderBox()->x() and
267     // y() are unusable.
268     // FIXME: This should probably respect transforms.
269     LayoutPoint absoluteThumbOrigin = renderBox()->absoluteBoundingBoxRectIgnoringTransforms().location();
270     LayoutPoint absoluteSliderContentOrigin = roundedLayoutPoint(input->renderer()->localToAbsolute());
271     IntRect trackBoundingBox = trackElement->renderer()->absoluteBoundingBoxRectIgnoringTransforms();
272     IntRect inputBoundingBox = input->renderer()->absoluteBoundingBoxRectIgnoringTransforms();
273     if (isVertical) {
274         trackSize = trackElement->renderBox()->contentHeight() - renderBox()->height();
275         position = offset.y() - renderBox()->height() / 2 - trackBoundingBox.y() + inputBoundingBox.y() - renderBox()->marginBottom();
276         currentPosition = absoluteThumbOrigin.y() - absoluteSliderContentOrigin.y();
277     } else {
278         trackSize = trackElement->renderBox()->contentWidth() - renderBox()->width();
279         position = offset.x() - renderBox()->width() / 2 - trackBoundingBox.x() + inputBoundingBox.x();
280         position -= isLeftToRightDirection ? renderBox()->marginLeft() : renderBox()->marginRight();
281         currentPosition = absoluteThumbOrigin.x() - absoluteSliderContentOrigin.x();
282     }
283     position = max<LayoutUnit>(0, min(position, trackSize));
284     const Decimal ratio = Decimal::fromDouble(static_cast<double>(position) / trackSize);
285     const Decimal fraction = isVertical || !isLeftToRightDirection ? Decimal(1) - ratio : ratio;
286     StepRange stepRange(input->createStepRange(RejectAny));
287     Decimal value = stepRange.clampValue(stepRange.valueFromProportion(fraction));
288
289 #if ENABLE(DATALIST_ELEMENT)
290     const LayoutUnit snappingThreshold = renderer()->theme()->sliderTickSnappingThreshold();
291     if (snappingThreshold > 0) {
292         Decimal closest = input->findClosestTickMarkValue(value);
293         if (closest.isFinite()) {
294             double closestFraction = stepRange.proportionFromValue(closest).toDouble();
295             double closestRatio = isVertical || !isLeftToRightDirection ? 1.0 - closestFraction : closestFraction;
296             LayoutUnit closestPosition = trackSize * closestRatio;
297             if ((closestPosition - position).abs() <= snappingThreshold)
298                 value = closest;
299         }
300     }
301 #endif
302
303     String valueString = serializeForNumberType(value);
304     if (valueString == input->value())
305         return;
306
307     // FIXME: This is no longer being set from renderer. Consider updating the method name.
308     input->setValueFromRenderer(valueString);
309     renderer()->setNeedsLayout(true);
310     input->dispatchFormControlChangeEvent();
311 }
312
313 void SliderThumbElement::startDragging()
314 {
315     if (Frame* frame = document()->frame()) {
316         frame->eventHandler()->setCapturingMouseEventsNode(this);
317         m_inDragMode = true;
318     }
319 }
320
321 void SliderThumbElement::stopDragging()
322 {
323     if (!m_inDragMode)
324         return;
325
326     if (Frame* frame = document()->frame())
327         frame->eventHandler()->setCapturingMouseEventsNode(0);
328     m_inDragMode = false;
329     if (renderer())
330         renderer()->setNeedsLayout(true);
331 }
332
333 void SliderThumbElement::defaultEventHandler(Event* event)
334 {
335     if (!event->isMouseEvent()) {
336         HTMLDivElement::defaultEventHandler(event);
337         return;
338     }
339
340     // FIXME: Should handle this readonly/disabled check in more general way.
341     // Missing this kind of check is likely to occur elsewhere if adding it in each shadow element.
342     HTMLInputElement* input = hostInput();
343     if (!input || input->isDisabledOrReadOnly()) {
344         stopDragging();
345         HTMLDivElement::defaultEventHandler(event);
346         return;
347     }
348
349     MouseEvent* mouseEvent = static_cast<MouseEvent*>(event);
350     bool isLeftButton = mouseEvent->button() == LeftButton;
351     const AtomicString& eventType = event->type();
352
353     // We intentionally do not call event->setDefaultHandled() here because
354     // MediaControlTimelineElement::defaultEventHandler() wants to handle these
355     // mouse events.
356     if (eventType == eventNames().mousedownEvent && isLeftButton) {
357         startDragging();
358         return;
359     } else if (eventType == eventNames().mouseupEvent && isLeftButton) {
360         stopDragging();
361         return;
362     } else if (eventType == eventNames().mousemoveEvent) {
363         if (m_inDragMode)
364             setPositionFromPoint(mouseEvent->absoluteLocation());
365         return;
366     }
367
368     HTMLDivElement::defaultEventHandler(event);
369 }
370
371 bool SliderThumbElement::willRespondToMouseMoveEvents()
372 {
373     const HTMLInputElement* input = hostInput();
374     if (input && !input->isDisabledOrReadOnly() && m_inDragMode)
375         return true;
376
377     return HTMLDivElement::willRespondToMouseMoveEvents();
378 }
379
380 bool SliderThumbElement::willRespondToMouseClickEvents()
381 {
382     const HTMLInputElement* input = hostInput();
383     if (input && !input->isDisabledOrReadOnly())
384         return true;
385
386     return HTMLDivElement::willRespondToMouseClickEvents();
387 }
388
389 void SliderThumbElement::detach()
390 {
391     if (m_inDragMode) {
392         if (Frame* frame = document()->frame())
393             frame->eventHandler()->setCapturingMouseEventsNode(0);
394     }
395     HTMLDivElement::detach();
396 }
397
398 HTMLInputElement* SliderThumbElement::hostInput() const
399 {
400     // Only HTMLInputElement creates SliderThumbElement instances as its shadow nodes.
401     // So, shadowHost() must be an HTMLInputElement.
402     return shadowHost()->toInputElement();
403 }
404
405 static const AtomicString& sliderThumbShadowPseudoId()
406 {
407     DEFINE_STATIC_LOCAL(const AtomicString, sliderThumb, ("-webkit-slider-thumb", AtomicString::ConstructFromLiteral));
408     return sliderThumb;
409 }
410
411 static const AtomicString& mediaSliderThumbShadowPseudoId()
412 {
413     DEFINE_STATIC_LOCAL(const AtomicString, mediaSliderThumb, ("-webkit-media-slider-thumb", AtomicString::ConstructFromLiteral));
414     return mediaSliderThumb;
415 }
416
417 const AtomicString& SliderThumbElement::shadowPseudoId() const
418 {
419     HTMLInputElement* input = hostInput();
420     if (!input)
421         return sliderThumbShadowPseudoId();
422
423     RenderStyle* sliderStyle = input->renderer()->style();
424     switch (sliderStyle->appearance()) {
425     case MediaSliderPart:
426     case MediaSliderThumbPart:
427     case MediaVolumeSliderPart:
428     case MediaVolumeSliderThumbPart:
429     case MediaFullScreenVolumeSliderPart:
430     case MediaFullScreenVolumeSliderThumbPart:
431         return mediaSliderThumbShadowPseudoId();
432     default:
433         return sliderThumbShadowPseudoId();
434     }
435 }
436
437 // --------------------------------
438
439 inline SliderContainerElement::SliderContainerElement(Document* document)
440     : HTMLDivElement(HTMLNames::divTag, document)
441 {
442 }
443
444 PassRefPtr<SliderContainerElement> SliderContainerElement::create(Document* document)
445 {
446     return adoptRef(new SliderContainerElement(document));
447 }
448
449 RenderObject* SliderContainerElement::createRenderer(RenderArena* arena, RenderStyle*)
450 {
451     return new (arena) RenderSliderContainer(this);
452 }
453
454 const AtomicString& SliderContainerElement::shadowPseudoId() const
455 {
456     DEFINE_STATIC_LOCAL(const AtomicString, mediaSliderContainer, ("-webkit-media-slider-container", AtomicString::ConstructFromLiteral));
457     DEFINE_STATIC_LOCAL(const AtomicString, sliderContainer, ("-webkit-slider-container", AtomicString::ConstructFromLiteral));
458
459     HTMLInputElement* input = shadowHost()->toInputElement();
460     if (!input)
461         return sliderContainer;
462
463     RenderStyle* sliderStyle = input->renderer()->style();
464     switch (sliderStyle->appearance()) {
465     case MediaSliderPart:
466     case MediaSliderThumbPart:
467     case MediaVolumeSliderPart:
468     case MediaVolumeSliderThumbPart:
469     case MediaFullScreenVolumeSliderPart:
470     case MediaFullScreenVolumeSliderThumbPart:
471         return mediaSliderContainer;
472     default:
473         return sliderContainer;
474     }
475 }
476
477 }