1/*
2 * Copyright (C) 2006, 2008, 2010 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
7 * are met:
8 * 1. Redistributions of source code must retain the above copyright
9 *    notice, this list of conditions and the following disclaimer.
10 * 2. Redistributions in binary form must reproduce the above copyright
11 *    notice, this list of conditions and the following disclaimer in the
12 *    documentation and/or other materials provided with the distribution.
13 *
14 * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
15 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
16 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
17 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE INC. OR
18 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
19 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
20 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
21 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
22 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
23 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
24 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
25 */
26
27#include "config.h"
28#include "SpinButtonElement.h"
29
30#include "Chrome.h"
31#include "EventHandler.h"
32#include "EventNames.h"
33#include "Frame.h"
34#include "HTMLNames.h"
35#include "MouseEvent.h"
36#include "Page.h"
37#include "RenderBox.h"
38#include "ScrollbarTheme.h"
39#include "WheelEvent.h"
40#include <wtf/Ref.h>
41
42namespace WebCore {
43
44using namespace HTMLNames;
45
46inline SpinButtonElement::SpinButtonElement(Document& document, SpinButtonOwner& spinButtonOwner)
47    : HTMLDivElement(divTag, document)
48    , m_spinButtonOwner(&spinButtonOwner)
49    , m_capturing(false)
50    , m_upDownState(Indeterminate)
51    , m_pressStartingState(Indeterminate)
52    , m_repeatingTimer(this, &SpinButtonElement::repeatingTimerFired)
53{
54    setHasCustomStyleResolveCallbacks();
55}
56
57PassRefPtr<SpinButtonElement> SpinButtonElement::create(Document& document, SpinButtonOwner& spinButtonOwner)
58{
59    return adoptRef(new SpinButtonElement(document, spinButtonOwner));
60}
61
62const AtomicString& SpinButtonElement::shadowPseudoId() const
63{
64    DEPRECATED_DEFINE_STATIC_LOCAL(AtomicString, innerPseudoId, ("-webkit-inner-spin-button", AtomicString::ConstructFromLiteral));
65    return innerPseudoId;
66}
67
68void SpinButtonElement::willDetachRenderers()
69{
70    releaseCapture();
71}
72
73void SpinButtonElement::defaultEventHandler(Event* event)
74{
75    if (!event->isMouseEvent()) {
76        if (!event->defaultHandled())
77            HTMLDivElement::defaultEventHandler(event);
78        return;
79    }
80
81    RenderBox* box = renderBox();
82    if (!box) {
83        if (!event->defaultHandled())
84            HTMLDivElement::defaultEventHandler(event);
85        return;
86    }
87
88    if (!shouldRespondToMouseEvents()) {
89        if (!event->defaultHandled())
90            HTMLDivElement::defaultEventHandler(event);
91        return;
92    }
93
94    MouseEvent* mouseEvent = toMouseEvent(event);
95    IntPoint local = roundedIntPoint(box->absoluteToLocal(mouseEvent->absoluteLocation(), UseTransforms));
96    if (mouseEvent->type() == eventNames().mousedownEvent && mouseEvent->button() == LeftButton) {
97        if (box->pixelSnappedBorderBoxRect().contains(local)) {
98            // The following functions of HTMLInputElement may run JavaScript
99            // code which detaches this shadow node. We need to take a reference
100            // and check renderer() after such function calls.
101            Ref<SpinButtonElement> protect(*this);
102            if (m_spinButtonOwner)
103                m_spinButtonOwner->focusAndSelectSpinButtonOwner();
104            if (renderer()) {
105                if (m_upDownState != Indeterminate) {
106                    // A JavaScript event handler called in doStepAction() below
107                    // might change the element state and we might need to
108                    // cancel the repeating timer by the state change. If we
109                    // started the timer after doStepAction(), we would have no
110                    // chance to cancel the timer.
111                    startRepeatingTimer();
112                    doStepAction(m_upDownState == Up ? 1 : -1);
113                }
114            }
115            event->setDefaultHandled();
116        }
117    } else if (mouseEvent->type() == eventNames().mouseupEvent && mouseEvent->button() == LeftButton)
118        stopRepeatingTimer();
119    else if (event->type() == eventNames().mousemoveEvent) {
120        if (box->pixelSnappedBorderBoxRect().contains(local)) {
121            if (!m_capturing) {
122                if (Frame* frame = document().frame()) {
123                    frame->eventHandler().setCapturingMouseEventsElement(this);
124                    m_capturing = true;
125                    if (Page* page = document().page())
126                        page->chrome().registerPopupOpeningObserver(this);
127                }
128            }
129            UpDownState oldUpDownState = m_upDownState;
130            m_upDownState = local.y() < box->height() / 2 ? Up : Down;
131            if (m_upDownState != oldUpDownState)
132                renderer()->repaint();
133        } else {
134            releaseCapture();
135            m_upDownState = Indeterminate;
136        }
137    }
138
139    if (!event->defaultHandled())
140        HTMLDivElement::defaultEventHandler(event);
141}
142
143void SpinButtonElement::willOpenPopup()
144{
145    releaseCapture();
146    m_upDownState = Indeterminate;
147}
148
149void SpinButtonElement::forwardEvent(Event* event)
150{
151    if (!renderBox())
152        return;
153
154    if (event->eventInterface() != WheelEventInterfaceType)
155        return;
156
157    if (!m_spinButtonOwner)
158        return;
159
160    if (!m_spinButtonOwner->shouldSpinButtonRespondToWheelEvents())
161        return;
162
163    doStepAction(toWheelEvent(event)->wheelDeltaY());
164    event->setDefaultHandled();
165}
166
167bool SpinButtonElement::willRespondToMouseMoveEvents()
168{
169    if (renderBox() && shouldRespondToMouseEvents())
170        return true;
171
172    return HTMLDivElement::willRespondToMouseMoveEvents();
173}
174
175bool SpinButtonElement::willRespondToMouseClickEvents()
176{
177    if (renderBox() && shouldRespondToMouseEvents())
178        return true;
179
180    return HTMLDivElement::willRespondToMouseClickEvents();
181}
182
183void SpinButtonElement::doStepAction(int amount)
184{
185    if (!m_spinButtonOwner)
186        return;
187
188    if (amount > 0)
189        m_spinButtonOwner->spinButtonStepUp();
190    else if (amount < 0)
191        m_spinButtonOwner->spinButtonStepDown();
192}
193
194void SpinButtonElement::releaseCapture()
195{
196    stopRepeatingTimer();
197    if (m_capturing) {
198        if (Frame* frame = document().frame()) {
199            frame->eventHandler().setCapturingMouseEventsElement(nullptr);
200            m_capturing = false;
201            if (Page* page = document().page())
202                page->chrome().unregisterPopupOpeningObserver(this);
203        }
204    }
205}
206
207bool SpinButtonElement::matchesReadOnlyPseudoClass() const
208{
209    return shadowHost()->matchesReadOnlyPseudoClass();
210}
211
212bool SpinButtonElement::matchesReadWritePseudoClass() const
213{
214    return shadowHost()->matchesReadWritePseudoClass();
215}
216
217void SpinButtonElement::startRepeatingTimer()
218{
219    m_pressStartingState = m_upDownState;
220    ScrollbarTheme* theme = ScrollbarTheme::theme();
221    m_repeatingTimer.start(theme->initialAutoscrollTimerDelay(), theme->autoscrollTimerDelay());
222}
223
224void SpinButtonElement::stopRepeatingTimer()
225{
226    m_repeatingTimer.stop();
227}
228
229void SpinButtonElement::step(int amount)
230{
231    if (!shouldRespondToMouseEvents())
232        return;
233    // On Mac OS, NSStepper updates the value for the button under the mouse
234    // cursor regardless of the button pressed at the beginning. So the
235    // following check is not needed for Mac OS.
236#if !OS(MAC_OS_X)
237    if (m_upDownState != m_pressStartingState)
238        return;
239#endif
240    doStepAction(amount);
241}
242
243void SpinButtonElement::repeatingTimerFired(Timer<SpinButtonElement>*)
244{
245    if (m_upDownState != Indeterminate)
246        step(m_upDownState == Up ? 1 : -1);
247}
248
249void SpinButtonElement::setHovered(bool flag)
250{
251    if (!flag)
252        m_upDownState = Indeterminate;
253    HTMLDivElement::setHovered(flag);
254}
255
256bool SpinButtonElement::shouldRespondToMouseEvents()
257{
258    return !m_spinButtonOwner || m_spinButtonOwner->shouldSpinButtonRespondToMouseEvents();
259}
260
261}
262