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 "RenderTheme.h"
39#include "ScrollbarTheme.h"
40#include "WheelEvent.h"
41#include <wtf/IsoMallocInlines.h>
42#include <wtf/Ref.h>
43
44namespace WebCore {
45
46WTF_MAKE_ISO_ALLOCATED_IMPL(SpinButtonElement);
47
48using namespace HTMLNames;
49
50inline SpinButtonElement::SpinButtonElement(Document& document, SpinButtonOwner& spinButtonOwner)
51 : HTMLDivElement(divTag, document)
52 , m_spinButtonOwner(&spinButtonOwner)
53 , m_capturing(false)
54 , m_upDownState(Indeterminate)
55 , m_pressStartingState(Indeterminate)
56 , m_repeatingTimer(*this, &SpinButtonElement::repeatingTimerFired)
57{
58 setHasCustomStyleResolveCallbacks();
59 setPseudo(AtomicString("-webkit-inner-spin-button", AtomicString::ConstructFromLiteral));
60}
61
62Ref<SpinButtonElement> SpinButtonElement::create(Document& document, SpinButtonOwner& spinButtonOwner)
63{
64 return adoptRef(*new SpinButtonElement(document, spinButtonOwner));
65}
66
67void SpinButtonElement::willDetachRenderers()
68{
69 releaseCapture();
70}
71
72void SpinButtonElement::defaultEventHandler(Event& event)
73{
74 if (!is<MouseEvent>(event)) {
75 if (!event.defaultHandled())
76 HTMLDivElement::defaultEventHandler(event);
77 return;
78 }
79
80 RenderBox* box = renderBox();
81 if (!box) {
82 if (!event.defaultHandled())
83 HTMLDivElement::defaultEventHandler(event);
84 return;
85 }
86
87 if (!shouldRespondToMouseEvents()) {
88 if (!event.defaultHandled())
89 HTMLDivElement::defaultEventHandler(event);
90 return;
91 }
92
93 MouseEvent& mouseEvent = downcast<MouseEvent>(event);
94 IntPoint local = roundedIntPoint(box->absoluteToLocal(mouseEvent.absoluteLocation(), UseTransforms));
95 if (mouseEvent.type() == eventNames().mousedownEvent && mouseEvent.button() == LeftButton) {
96 if (box->borderBoxRect().contains(local)) {
97 // The following functions of HTMLInputElement may run JavaScript
98 // code which detaches this shadow node. We need to take a reference
99 // and check renderer() after such function calls.
100 Ref<SpinButtonElement> protectedThis(*this);
101 if (m_spinButtonOwner)
102 m_spinButtonOwner->focusAndSelectSpinButtonOwner();
103 if (renderer()) {
104 if (m_upDownState != Indeterminate) {
105 // A JavaScript event handler called in doStepAction() below
106 // might change the element state and we might need to
107 // cancel the repeating timer by the state change. If we
108 // started the timer after doStepAction(), we would have no
109 // chance to cancel the timer.
110 startRepeatingTimer();
111 doStepAction(m_upDownState == Up ? 1 : -1);
112 }
113 }
114 mouseEvent.setDefaultHandled();
115 }
116 } else if (mouseEvent.type() == eventNames().mouseupEvent && mouseEvent.button() == LeftButton)
117 stopRepeatingTimer();
118 else if (mouseEvent.type() == eventNames().mousemoveEvent) {
119 if (box->borderBoxRect().contains(local)) {
120 if (!m_capturing) {
121 if (RefPtr<Frame> frame = document().frame()) {
122 frame->eventHandler().setCapturingMouseEventsElement(this);
123 m_capturing = true;
124 if (Page* page = document().page())
125 page->chrome().registerPopupOpeningObserver(*this);
126 }
127 }
128 UpDownState oldUpDownState = m_upDownState;
129 switch (renderer()->theme().innerSpinButtonLayout(*renderer())) {
130 case RenderTheme::InnerSpinButtonLayout::Vertical:
131 m_upDownState = local.y() < box->height() / 2 ? Up : Down;
132 break;
133 case RenderTheme::InnerSpinButtonLayout::HorizontalUpLeft:
134 m_upDownState = local.x() < box->width() / 2 ? Up : Down;
135 break;
136 case RenderTheme::InnerSpinButtonLayout::HorizontalUpRight:
137 m_upDownState = local.x() > box->width() / 2 ? Up : Down;
138 break;
139 }
140 if (m_upDownState != oldUpDownState)
141 renderer()->repaint();
142 } else {
143 releaseCapture();
144 m_upDownState = Indeterminate;
145 }
146 }
147
148 if (!mouseEvent.defaultHandled())
149 HTMLDivElement::defaultEventHandler(mouseEvent);
150}
151
152void SpinButtonElement::willOpenPopup()
153{
154 releaseCapture();
155 m_upDownState = Indeterminate;
156}
157
158void SpinButtonElement::forwardEvent(Event& event)
159{
160 if (!renderBox())
161 return;
162
163 if (!is<WheelEvent>(event))
164 return;
165
166 if (!m_spinButtonOwner)
167 return;
168
169 if (!m_spinButtonOwner->shouldSpinButtonRespondToWheelEvents())
170 return;
171
172 doStepAction(downcast<WheelEvent>(event).wheelDeltaY());
173 event.setDefaultHandled();
174}
175
176bool SpinButtonElement::willRespondToMouseMoveEvents()
177{
178 if (renderBox() && shouldRespondToMouseEvents())
179 return true;
180
181 return HTMLDivElement::willRespondToMouseMoveEvents();
182}
183
184bool SpinButtonElement::willRespondToMouseClickEvents()
185{
186 if (renderBox() && shouldRespondToMouseEvents())
187 return true;
188
189 return HTMLDivElement::willRespondToMouseClickEvents();
190}
191
192void SpinButtonElement::doStepAction(int amount)
193{
194 if (!m_spinButtonOwner)
195 return;
196
197 if (amount > 0)
198 m_spinButtonOwner->spinButtonStepUp();
199 else if (amount < 0)
200 m_spinButtonOwner->spinButtonStepDown();
201}
202
203void SpinButtonElement::releaseCapture()
204{
205 stopRepeatingTimer();
206 if (m_capturing) {
207 if (RefPtr<Frame> frame = document().frame()) {
208 frame->eventHandler().setCapturingMouseEventsElement(nullptr);
209 m_capturing = false;
210 if (Page* page = document().page())
211 page->chrome().unregisterPopupOpeningObserver(*this);
212 }
213 }
214}
215
216bool SpinButtonElement::matchesReadWritePseudoClass() const
217{
218 return shadowHost()->matchesReadWritePseudoClass();
219}
220
221void SpinButtonElement::startRepeatingTimer()
222{
223 m_pressStartingState = m_upDownState;
224 ScrollbarTheme& theme = ScrollbarTheme::theme();
225 m_repeatingTimer.start(theme.initialAutoscrollTimerDelay(), theme.autoscrollTimerDelay());
226}
227
228void SpinButtonElement::stopRepeatingTimer()
229{
230 m_repeatingTimer.stop();
231}
232
233void SpinButtonElement::step(int amount)
234{
235 if (!shouldRespondToMouseEvents())
236 return;
237 // On Mac OS, NSStepper updates the value for the button under the mouse
238 // cursor regardless of the button pressed at the beginning. So the
239 // following check is not needed for Mac OS.
240#if !OS(MAC_OS_X)
241 if (m_upDownState != m_pressStartingState)
242 return;
243#endif
244 doStepAction(amount);
245}
246
247void SpinButtonElement::repeatingTimerFired()
248{
249 if (m_upDownState != Indeterminate)
250 step(m_upDownState == Up ? 1 : -1);
251}
252
253void SpinButtonElement::setHovered(bool flag)
254{
255 if (!flag)
256 m_upDownState = Indeterminate;
257 HTMLDivElement::setHovered(flag);
258}
259
260bool SpinButtonElement::shouldRespondToMouseEvents()
261{
262 return !m_spinButtonOwner || m_spinButtonOwner->shouldSpinButtonRespondToMouseEvents();
263}
264
265}
266