| /* |
| * Copyright (C) 2008, 2009, 2010, 2011 Apple Inc. All rights reserved. |
| * Copyright (C) 2012 Google Inc. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions |
| * are met: |
| * |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * 2. Redistributions in binary form must reproduce the above copyright |
| * notice, this list of conditions and the following disclaimer in the |
| * documentation and/or other materials provided with the distribution. |
| * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of |
| * its contributors may be used to endorse or promote products derived |
| * from this software without specific prior written permission. |
| * |
| * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY |
| * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
| * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
| * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY |
| * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES |
| * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; |
| * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND |
| * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF |
| * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| #include "core/html/shadow/MediaControlElements.h" |
| |
| #include "bindings/core/v8/ExceptionStatePlaceholder.h" |
| #include "core/InputTypeNames.h" |
| #include "core/dom/ClientRect.h" |
| #include "core/dom/Text.h" |
| #include "core/dom/shadow/ShadowRoot.h" |
| #include "core/events/MouseEvent.h" |
| #include "core/frame/LocalFrame.h" |
| #include "core/html/HTMLAnchorElement.h" |
| #include "core/html/HTMLLabelElement.h" |
| #include "core/html/HTMLMediaSource.h" |
| #include "core/html/HTMLSpanElement.h" |
| #include "core/html/HTMLVideoElement.h" |
| #include "core/html/TimeRanges.h" |
| #include "core/html/shadow/MediaControls.h" |
| #include "core/html/track/TextTrackList.h" |
| #include "core/input/EventHandler.h" |
| #include "core/layout/api/LayoutSliderItem.h" |
| #include "platform/EventDispatchForbiddenScope.h" |
| #include "platform/Histogram.h" |
| #include "platform/RuntimeEnabledFeatures.h" |
| #include "platform/text/PlatformLocale.h" |
| #include "public/platform/Platform.h" |
| #include "public/platform/UserMetricsAction.h" |
| |
| namespace blink { |
| |
| using namespace HTMLNames; |
| |
| namespace { |
| |
| // This is the duration from mediaControls.css |
| const double fadeOutDuration = 0.3; |
| |
| const QualifiedName& trackIndexAttrName() { |
| // Save the track index in an attribute to avoid holding a pointer to the text |
| // track. |
| DEFINE_STATIC_LOCAL(QualifiedName, trackIndexAttr, |
| (nullAtom, "data-track-index", nullAtom)); |
| return trackIndexAttr; |
| } |
| |
| // When specified as trackIndex, disable text tracks. |
| const int trackIndexOffValue = -1; |
| |
| bool isUserInteractionEvent(Event* event) { |
| const AtomicString& type = event->type(); |
| return type == EventTypeNames::mousedown || type == EventTypeNames::mouseup || |
| type == EventTypeNames::click || type == EventTypeNames::dblclick || |
| event->isKeyboardEvent() || event->isTouchEvent(); |
| } |
| |
| // Sliders (the volume control and timeline) need to capture some additional |
| // events used when dragging the thumb. |
| bool isUserInteractionEventForSlider(Event* event, LayoutObject* layoutObject) { |
| // It is unclear if this can be converted to isUserInteractionEvent(), since |
| // mouse* events seem to be eaten during a drag anyway. crbug.com/516416 . |
| if (isUserInteractionEvent(event)) |
| return true; |
| |
| // Some events are only captured during a slider drag. |
| LayoutSliderItem slider = LayoutSliderItem(toLayoutSlider(layoutObject)); |
| if (!slider.isNull() && !slider.inDragMode()) |
| return false; |
| |
| const AtomicString& type = event->type(); |
| return type == EventTypeNames::mouseover || |
| type == EventTypeNames::mouseout || type == EventTypeNames::mousemove; |
| } |
| |
| Element* elementFromCenter(Element& element) { |
| ClientRect* clientRect = element.getBoundingClientRect(); |
| int centerX = |
| static_cast<int>((clientRect->left() + clientRect->right()) / 2); |
| int centerY = |
| static_cast<int>((clientRect->top() + clientRect->bottom()) / 2); |
| |
| return element.document().elementFromPoint(centerX, centerY); |
| } |
| |
| bool hasDuplicateLabel(TextTrack* currentTrack) { |
| DCHECK(currentTrack); |
| TextTrackList* trackList = currentTrack->trackList(); |
| // The runtime of this method is quadratic but since there are usually very |
| // few text tracks it won't affect the performance much. |
| String currentTrackLabel = currentTrack->label(); |
| for (unsigned i = 0; i < trackList->length(); i++) { |
| TextTrack* track = trackList->anonymousIndexedGetter(i); |
| if (currentTrack != track && currentTrackLabel == track->label()) |
| return true; |
| } |
| return false; |
| } |
| |
| } // anonymous namespace |
| |
| MediaControlPanelElement::MediaControlPanelElement(MediaControls& mediaControls) |
| : MediaControlDivElement(mediaControls, MediaControlsPanel), |
| m_isDisplayed(false), |
| m_opaque(true), |
| m_transitionTimer(this, &MediaControlPanelElement::transitionTimerFired) { |
| } |
| |
| MediaControlPanelElement* MediaControlPanelElement::create( |
| MediaControls& mediaControls) { |
| MediaControlPanelElement* panel = new MediaControlPanelElement(mediaControls); |
| panel->setShadowPseudoId(AtomicString("-webkit-media-controls-panel")); |
| return panel; |
| } |
| |
| void MediaControlPanelElement::defaultEventHandler(Event* event) { |
| // Suppress the media element activation behavior (toggle play/pause) when |
| // any part of the control panel is clicked. |
| if (event->type() == EventTypeNames::click) { |
| event->setDefaultHandled(); |
| return; |
| } |
| HTMLDivElement::defaultEventHandler(event); |
| } |
| |
| void MediaControlPanelElement::startTimer() { |
| stopTimer(); |
| |
| // The timer is required to set the property display:'none' on the panel, |
| // such that captions are correctly displayed at the bottom of the video |
| // at the end of the fadeout transition. |
| // FIXME: Racing a transition with a setTimeout like this is wrong. |
| m_transitionTimer.startOneShot(fadeOutDuration, BLINK_FROM_HERE); |
| } |
| |
| void MediaControlPanelElement::stopTimer() { |
| if (m_transitionTimer.isActive()) |
| m_transitionTimer.stop(); |
| } |
| |
| void MediaControlPanelElement::transitionTimerFired(TimerBase*) { |
| if (!m_opaque) |
| setIsWanted(false); |
| |
| stopTimer(); |
| } |
| |
| void MediaControlPanelElement::didBecomeVisible() { |
| DCHECK(m_isDisplayed && m_opaque); |
| mediaElement().mediaControlsDidBecomeVisible(); |
| } |
| |
| void MediaControlPanelElement::makeOpaque() { |
| if (m_opaque) |
| return; |
| |
| setInlineStyleProperty(CSSPropertyOpacity, 1.0, |
| CSSPrimitiveValue::UnitType::Number); |
| m_opaque = true; |
| |
| if (m_isDisplayed) { |
| setIsWanted(true); |
| didBecomeVisible(); |
| } |
| } |
| |
| void MediaControlPanelElement::makeTransparent() { |
| if (!m_opaque) |
| return; |
| |
| setInlineStyleProperty(CSSPropertyOpacity, 0.0, |
| CSSPrimitiveValue::UnitType::Number); |
| |
| m_opaque = false; |
| startTimer(); |
| } |
| |
| void MediaControlPanelElement::setIsDisplayed(bool isDisplayed) { |
| if (m_isDisplayed == isDisplayed) |
| return; |
| |
| m_isDisplayed = isDisplayed; |
| if (m_isDisplayed && m_opaque) |
| didBecomeVisible(); |
| } |
| |
| bool MediaControlPanelElement::keepEventInNode(Event* event) { |
| return isUserInteractionEvent(event); |
| } |
| |
| // ---------------------------- |
| |
| MediaControlPanelEnclosureElement::MediaControlPanelEnclosureElement( |
| MediaControls& mediaControls) |
| // Mapping onto same MediaControlElementType as panel element, since it has |
| // similar properties. |
| : MediaControlDivElement(mediaControls, MediaControlsPanel) {} |
| |
| MediaControlPanelEnclosureElement* MediaControlPanelEnclosureElement::create( |
| MediaControls& mediaControls) { |
| MediaControlPanelEnclosureElement* enclosure = |
| new MediaControlPanelEnclosureElement(mediaControls); |
| enclosure->setShadowPseudoId( |
| AtomicString("-webkit-media-controls-enclosure")); |
| return enclosure; |
| } |
| |
| // ---------------------------- |
| |
| MediaControlOverlayEnclosureElement::MediaControlOverlayEnclosureElement( |
| MediaControls& mediaControls) |
| // Mapping onto same MediaControlElementType as panel element, since it has |
| // similar properties. |
| : MediaControlDivElement(mediaControls, MediaControlsPanel) {} |
| |
| MediaControlOverlayEnclosureElement* |
| MediaControlOverlayEnclosureElement::create(MediaControls& mediaControls) { |
| MediaControlOverlayEnclosureElement* enclosure = |
| new MediaControlOverlayEnclosureElement(mediaControls); |
| enclosure->setShadowPseudoId( |
| AtomicString("-webkit-media-controls-overlay-enclosure")); |
| return enclosure; |
| } |
| |
| EventDispatchHandlingState* |
| MediaControlOverlayEnclosureElement::preDispatchEventHandler(Event* event) { |
| // When the media element is clicked or touched we want to make the overlay |
| // cast button visible (if the other requirements are right) even if |
| // JavaScript is doing its own handling of the event. Doing it in |
| // preDispatchEventHandler prevents any interference from JavaScript. |
| // Note that we can't simply test for click, since JS handling of touch events |
| // can prevent their translation to click events. |
| if (event && (event->type() == EventTypeNames::click || |
| event->type() == EventTypeNames::touchstart)) |
| mediaControls().showOverlayCastButtonIfNeeded(); |
| return MediaControlDivElement::preDispatchEventHandler(event); |
| } |
| |
| // ---------------------------- |
| |
| MediaControlMuteButtonElement::MediaControlMuteButtonElement( |
| MediaControls& mediaControls) |
| : MediaControlInputElement(mediaControls, MediaMuteButton) {} |
| |
| MediaControlMuteButtonElement* MediaControlMuteButtonElement::create( |
| MediaControls& mediaControls) { |
| MediaControlMuteButtonElement* button = |
| new MediaControlMuteButtonElement(mediaControls); |
| button->ensureUserAgentShadowRoot(); |
| button->setType(InputTypeNames::button); |
| button->setShadowPseudoId(AtomicString("-webkit-media-controls-mute-button")); |
| return button; |
| } |
| |
| void MediaControlMuteButtonElement::defaultEventHandler(Event* event) { |
| if (event->type() == EventTypeNames::click) { |
| if (mediaElement().muted()) |
| Platform::current()->recordAction( |
| UserMetricsAction("Media.Controls.Unmute")); |
| else |
| Platform::current()->recordAction( |
| UserMetricsAction("Media.Controls.Mute")); |
| |
| mediaElement().setMuted(!mediaElement().muted()); |
| event->setDefaultHandled(); |
| } |
| |
| MediaControlInputElement::defaultEventHandler(event); |
| } |
| |
| void MediaControlMuteButtonElement::updateDisplayType() { |
| setDisplayType(mediaElement().muted() ? MediaUnMuteButton : MediaMuteButton); |
| updateOverflowString(); |
| } |
| |
| WebLocalizedString::Name |
| MediaControlMuteButtonElement::getOverflowStringName() { |
| if (mediaElement().muted()) |
| return WebLocalizedString::OverflowMenuUnmute; |
| return WebLocalizedString::OverflowMenuMute; |
| } |
| |
| // ---------------------------- |
| |
| MediaControlPlayButtonElement::MediaControlPlayButtonElement( |
| MediaControls& mediaControls) |
| : MediaControlInputElement(mediaControls, MediaPlayButton) {} |
| |
| MediaControlPlayButtonElement* MediaControlPlayButtonElement::create( |
| MediaControls& mediaControls) { |
| MediaControlPlayButtonElement* button = |
| new MediaControlPlayButtonElement(mediaControls); |
| button->ensureUserAgentShadowRoot(); |
| button->setType(InputTypeNames::button); |
| button->setShadowPseudoId(AtomicString("-webkit-media-controls-play-button")); |
| return button; |
| } |
| |
| void MediaControlPlayButtonElement::defaultEventHandler(Event* event) { |
| if (event->type() == EventTypeNames::click) { |
| if (mediaElement().paused()) |
| Platform::current()->recordAction( |
| UserMetricsAction("Media.Controls.Play")); |
| else |
| Platform::current()->recordAction( |
| UserMetricsAction("Media.Controls.Pause")); |
| |
| // Allow play attempts for plain src= media to force a reload in the error |
| // state. This allows potential recovery for transient network and decoder |
| // resource issues. |
| const String& url = mediaElement().currentSrc().getString(); |
| if (mediaElement().error() && !HTMLMediaElement::isMediaStreamURL(url) && |
| !HTMLMediaSource::lookup(url)) |
| mediaElement().load(); |
| |
| mediaElement().togglePlayState(); |
| updateDisplayType(); |
| event->setDefaultHandled(); |
| } |
| MediaControlInputElement::defaultEventHandler(event); |
| } |
| |
| void MediaControlPlayButtonElement::updateDisplayType() { |
| setDisplayType(mediaElement().paused() ? MediaPlayButton : MediaPauseButton); |
| updateOverflowString(); |
| } |
| |
| WebLocalizedString::Name |
| MediaControlPlayButtonElement::getOverflowStringName() { |
| if (mediaElement().paused()) |
| return WebLocalizedString::OverflowMenuPlay; |
| return WebLocalizedString::OverflowMenuPause; |
| } |
| |
| // ---------------------------- |
| |
| MediaControlOverlayPlayButtonElement::MediaControlOverlayPlayButtonElement( |
| MediaControls& mediaControls) |
| : MediaControlInputElement(mediaControls, MediaOverlayPlayButton) {} |
| |
| MediaControlOverlayPlayButtonElement* |
| MediaControlOverlayPlayButtonElement::create(MediaControls& mediaControls) { |
| MediaControlOverlayPlayButtonElement* button = |
| new MediaControlOverlayPlayButtonElement(mediaControls); |
| button->ensureUserAgentShadowRoot(); |
| button->setType(InputTypeNames::button); |
| button->setShadowPseudoId( |
| AtomicString("-webkit-media-controls-overlay-play-button")); |
| return button; |
| } |
| |
| void MediaControlOverlayPlayButtonElement::defaultEventHandler(Event* event) { |
| if (event->type() == EventTypeNames::click && mediaElement().paused()) { |
| Platform::current()->recordAction( |
| UserMetricsAction("Media.Controls.PlayOverlay")); |
| mediaElement().play(); |
| updateDisplayType(); |
| event->setDefaultHandled(); |
| } |
| } |
| |
| void MediaControlOverlayPlayButtonElement::updateDisplayType() { |
| setIsWanted(mediaElement().shouldShowControls() && mediaElement().paused()); |
| } |
| |
| bool MediaControlOverlayPlayButtonElement::keepEventInNode(Event* event) { |
| return isUserInteractionEvent(event); |
| } |
| |
| // ---------------------------- |
| |
| MediaControlToggleClosedCaptionsButtonElement:: |
| MediaControlToggleClosedCaptionsButtonElement(MediaControls& mediaControls) |
| : MediaControlInputElement(mediaControls, MediaShowClosedCaptionsButton) {} |
| |
| MediaControlToggleClosedCaptionsButtonElement* |
| MediaControlToggleClosedCaptionsButtonElement::create( |
| MediaControls& mediaControls) { |
| MediaControlToggleClosedCaptionsButtonElement* button = |
| new MediaControlToggleClosedCaptionsButtonElement(mediaControls); |
| button->ensureUserAgentShadowRoot(); |
| button->setType(InputTypeNames::button); |
| button->setShadowPseudoId( |
| AtomicString("-webkit-media-controls-toggle-closed-captions-button")); |
| button->setIsWanted(false); |
| return button; |
| } |
| |
| void MediaControlToggleClosedCaptionsButtonElement::updateDisplayType() { |
| bool captionsVisible = mediaElement().textTracksVisible(); |
| setDisplayType(captionsVisible ? MediaHideClosedCaptionsButton |
| : MediaShowClosedCaptionsButton); |
| } |
| |
| void MediaControlToggleClosedCaptionsButtonElement::defaultEventHandler( |
| Event* event) { |
| if (event->type() == EventTypeNames::click) { |
| mediaControls().toggleTextTrackList(); |
| updateDisplayType(); |
| event->setDefaultHandled(); |
| } |
| |
| MediaControlInputElement::defaultEventHandler(event); |
| } |
| |
| WebLocalizedString::Name |
| MediaControlToggleClosedCaptionsButtonElement::getOverflowStringName() { |
| return WebLocalizedString::OverflowMenuCaptions; |
| } |
| |
| // ---------------------------- |
| |
| MediaControlTextTrackListElement::MediaControlTextTrackListElement( |
| MediaControls& mediaControls) |
| : MediaControlDivElement(mediaControls, MediaTextTrackList) {} |
| |
| MediaControlTextTrackListElement* MediaControlTextTrackListElement::create( |
| MediaControls& mediaControls) { |
| MediaControlTextTrackListElement* element = |
| new MediaControlTextTrackListElement(mediaControls); |
| element->setShadowPseudoId( |
| AtomicString("-internal-media-controls-text-track-list")); |
| element->setIsWanted(false); |
| return element; |
| } |
| |
| void MediaControlTextTrackListElement::defaultEventHandler(Event* event) { |
| if (event->type() == EventTypeNames::change) { |
| // Identify which input element was selected and set track to showing |
| Node* target = event->target()->toNode(); |
| if (!target || !target->isElementNode()) |
| return; |
| |
| disableShowingTextTracks(); |
| int trackIndex = |
| toElement(target)->getIntegralAttribute(trackIndexAttrName()); |
| if (trackIndex != trackIndexOffValue) { |
| DCHECK_GE(trackIndex, 0); |
| showTextTrackAtIndex(trackIndex); |
| mediaElement().disableAutomaticTextTrackSelection(); |
| } |
| |
| event->setDefaultHandled(); |
| } |
| MediaControlDivElement::defaultEventHandler(event); |
| } |
| |
| void MediaControlTextTrackListElement::setVisible(bool visible) { |
| if (visible) { |
| setIsWanted(true); |
| refreshTextTrackListMenu(); |
| } else { |
| setIsWanted(false); |
| } |
| } |
| |
| void MediaControlTextTrackListElement::showTextTrackAtIndex( |
| unsigned indexToEnable) { |
| TextTrackList* trackList = mediaElement().textTracks(); |
| if (indexToEnable >= trackList->length()) |
| return; |
| TextTrack* track = trackList->anonymousIndexedGetter(indexToEnable); |
| if (track && track->canBeRendered()) |
| track->setMode(TextTrack::showingKeyword()); |
| } |
| |
| void MediaControlTextTrackListElement::disableShowingTextTracks() { |
| TextTrackList* trackList = mediaElement().textTracks(); |
| for (unsigned i = 0; i < trackList->length(); ++i) { |
| TextTrack* track = trackList->anonymousIndexedGetter(i); |
| if (track->mode() == TextTrack::showingKeyword()) |
| track->setMode(TextTrack::disabledKeyword()); |
| } |
| } |
| |
| String MediaControlTextTrackListElement::getTextTrackLabel(TextTrack* track) { |
| if (!track) |
| return mediaElement().locale().queryString( |
| WebLocalizedString::TextTracksOff); |
| |
| String trackLabel = track->label(); |
| |
| if (trackLabel.isEmpty()) |
| trackLabel = String(mediaElement().locale().queryString( |
| WebLocalizedString::TextTracksNoLabel)); |
| |
| return trackLabel; |
| } |
| |
| // TextTrack parameter when passed in as a nullptr, creates the "Off" list item |
| // in the track list. |
| Element* MediaControlTextTrackListElement::createTextTrackListItem( |
| TextTrack* track) { |
| int trackIndex = track ? track->trackIndex() : trackIndexOffValue; |
| HTMLLabelElement* trackItem = HTMLLabelElement::create(document()); |
| trackItem->setShadowPseudoId( |
| AtomicString("-internal-media-controls-text-track-list-item")); |
| HTMLInputElement* trackItemInput = |
| HTMLInputElement::create(document(), nullptr, false); |
| trackItemInput->setShadowPseudoId( |
| AtomicString("-internal-media-controls-text-track-list-item-input")); |
| trackItemInput->setType(InputTypeNames::checkbox); |
| trackItemInput->setIntegralAttribute(trackIndexAttrName(), trackIndex); |
| if (!mediaElement().textTracksVisible()) { |
| if (!track) |
| trackItemInput->setChecked(true); |
| } else { |
| // If there are multiple text tracks set to showing, they must all have |
| // checkmarks displayed. |
| if (track && track->mode() == TextTrack::showingKeyword()) |
| trackItemInput->setChecked(true); |
| } |
| |
| trackItem->appendChild(trackItemInput); |
| String trackLabel = getTextTrackLabel(track); |
| trackItem->appendChild(Text::create(document(), trackLabel)); |
| // Add a track kind marker icon if there are multiple tracks with the same |
| // label or if the track has no label. |
| if (track && (track->label().isEmpty() || hasDuplicateLabel(track))) { |
| HTMLSpanElement* trackKindMarker = HTMLSpanElement::create(document()); |
| if (track->kind() == track->captionsKeyword()) { |
| trackKindMarker->setShadowPseudoId(AtomicString( |
| "-internal-media-controls-text-track-list-kind-captions")); |
| } else { |
| DCHECK_EQ(track->kind(), track->subtitlesKeyword()); |
| trackKindMarker->setShadowPseudoId(AtomicString( |
| "-internal-media-controls-text-track-list-kind-subtitles")); |
| } |
| trackItem->appendChild(trackKindMarker); |
| } |
| return trackItem; |
| } |
| |
| void MediaControlTextTrackListElement::refreshTextTrackListMenu() { |
| if (!mediaElement().hasClosedCaptions() || |
| !mediaElement().textTracksAreReady()) |
| return; |
| |
| EventDispatchForbiddenScope::AllowUserAgentEvents allowEvents; |
| removeChildren(OmitSubtreeModifiedEvent); |
| |
| // Construct a menu for subtitles and captions. Pass in a nullptr to |
| // createTextTrackListItem to create the "Off" track item. |
| appendChild(createTextTrackListItem(nullptr)); |
| |
| TextTrackList* trackList = mediaElement().textTracks(); |
| for (unsigned i = 0; i < trackList->length(); i++) { |
| TextTrack* track = trackList->anonymousIndexedGetter(i); |
| if (!track->canBeRendered()) |
| continue; |
| appendChild(createTextTrackListItem(track)); |
| } |
| } |
| |
| // ---------------------------- |
| MediaControlOverflowMenuButtonElement::MediaControlOverflowMenuButtonElement( |
| MediaControls& mediaControls) |
| : MediaControlInputElement(mediaControls, MediaOverflowButton) {} |
| |
| MediaControlOverflowMenuButtonElement* |
| MediaControlOverflowMenuButtonElement::create(MediaControls& mediaControls) { |
| MediaControlOverflowMenuButtonElement* button = |
| new MediaControlOverflowMenuButtonElement(mediaControls); |
| button->ensureUserAgentShadowRoot(); |
| button->setType(InputTypeNames::button); |
| button->setShadowPseudoId( |
| AtomicString("-internal-media-controls-overflow-button")); |
| button->setIsWanted(false); |
| return button; |
| } |
| |
| void MediaControlOverflowMenuButtonElement::defaultEventHandler(Event* event) { |
| if (event->type() == EventTypeNames::click) { |
| if (mediaControls().overflowMenuVisible()) |
| Platform::current()->recordAction( |
| UserMetricsAction("Media.Controls.OverflowClose")); |
| else |
| Platform::current()->recordAction( |
| UserMetricsAction("Media.Controls.OverflowOpen")); |
| |
| mediaControls().toggleOverflowMenu(); |
| event->setDefaultHandled(); |
| } |
| |
| MediaControlInputElement::defaultEventHandler(event); |
| } |
| |
| // ---------------------------- |
| MediaControlOverflowMenuListElement::MediaControlOverflowMenuListElement( |
| MediaControls& mediaControls) |
| : MediaControlDivElement(mediaControls, MediaOverflowList) {} |
| |
| MediaControlOverflowMenuListElement* |
| MediaControlOverflowMenuListElement::create(MediaControls& mediaControls) { |
| MediaControlOverflowMenuListElement* element = |
| new MediaControlOverflowMenuListElement(mediaControls); |
| element->setIsWanted(false); |
| element->setShadowPseudoId( |
| AtomicString("-internal-media-controls-overflow-menu-list")); |
| return element; |
| } |
| |
| void MediaControlOverflowMenuListElement::defaultEventHandler(Event* event) { |
| if (event->type() == EventTypeNames::click) |
| event->setDefaultHandled(); |
| |
| MediaControlDivElement::defaultEventHandler(event); |
| } |
| |
| // ---------------------------- |
| MediaControlDownloadButtonElement::MediaControlDownloadButtonElement( |
| MediaControls& mediaControls) |
| : MediaControlInputElement(mediaControls, MediaDownloadButton) {} |
| |
| MediaControlDownloadButtonElement* MediaControlDownloadButtonElement::create( |
| MediaControls& mediaControls) { |
| MediaControlDownloadButtonElement* button = |
| new MediaControlDownloadButtonElement(mediaControls); |
| button->ensureUserAgentShadowRoot(); |
| button->setType(InputTypeNames::button); |
| button->setShadowPseudoId( |
| AtomicString("-internal-media-controls-download-button")); |
| button->setIsWanted(false); |
| return button; |
| } |
| |
| WebLocalizedString::Name |
| MediaControlDownloadButtonElement::getOverflowStringName() { |
| return WebLocalizedString::OverflowMenuDownload; |
| } |
| |
| bool MediaControlDownloadButtonElement::shouldDisplayDownloadButton() { |
| const KURL& url = mediaElement().currentSrc(); |
| |
| // URLs that lead to nowhere are ignored. |
| if (url.isNull() || url.isEmpty()) |
| return false; |
| |
| // Local files and blobs should not have a download button. |
| if (url.isLocalFile() || url.protocolIs("blob")) |
| return false; |
| |
| // MediaStream can't be downloaded. |
| if (HTMLMediaElement::isMediaStreamURL(url.getString())) |
| return false; |
| |
| // MediaSource can't be downloaded. |
| if (HTMLMediaSource::lookup(url)) |
| return false; |
| |
| return true; |
| } |
| |
| void MediaControlDownloadButtonElement::defaultEventHandler(Event* event) { |
| const KURL& url = mediaElement().currentSrc(); |
| if (event->type() == EventTypeNames::click && |
| !(url.isNull() || url.isEmpty())) { |
| Platform::current()->recordAction( |
| UserMetricsAction("Media.Controls.Download")); |
| if (!m_anchor) { |
| HTMLAnchorElement* anchor = HTMLAnchorElement::create(document()); |
| anchor->setAttribute(HTMLNames::downloadAttr, ""); |
| m_anchor = anchor; |
| } |
| m_anchor->setURL(url); |
| m_anchor->dispatchSimulatedClick(event); |
| } |
| MediaControlInputElement::defaultEventHandler(event); |
| } |
| |
| DEFINE_TRACE(MediaControlDownloadButtonElement) { |
| visitor->trace(m_anchor); |
| MediaControlInputElement::trace(visitor); |
| } |
| |
| // ---------------------------- |
| |
| MediaControlTimelineElement::MediaControlTimelineElement( |
| MediaControls& mediaControls) |
| : MediaControlInputElement(mediaControls, MediaSlider) {} |
| |
| MediaControlTimelineElement* MediaControlTimelineElement::create( |
| MediaControls& mediaControls) { |
| MediaControlTimelineElement* timeline = |
| new MediaControlTimelineElement(mediaControls); |
| timeline->ensureUserAgentShadowRoot(); |
| timeline->setType(InputTypeNames::range); |
| timeline->setAttribute(stepAttr, "any"); |
| timeline->setShadowPseudoId(AtomicString("-webkit-media-controls-timeline")); |
| return timeline; |
| } |
| |
| void MediaControlTimelineElement::defaultEventHandler(Event* event) { |
| if (event->isMouseEvent() && |
| toMouseEvent(event)->button() != |
| static_cast<short>(WebPointerProperties::Button::Left)) |
| return; |
| |
| if (!isConnected() || !document().isActive()) |
| return; |
| |
| if (event->type() == EventTypeNames::mousedown) { |
| Platform::current()->recordAction( |
| UserMetricsAction("Media.Controls.ScrubbingBegin")); |
| mediaControls().beginScrubbing(); |
| } |
| |
| if (event->type() == EventTypeNames::mouseup) { |
| Platform::current()->recordAction( |
| UserMetricsAction("Media.Controls.ScrubbingEnd")); |
| mediaControls().endScrubbing(); |
| } |
| |
| MediaControlInputElement::defaultEventHandler(event); |
| |
| if (event->type() == EventTypeNames::mouseover || |
| event->type() == EventTypeNames::mouseout || |
| event->type() == EventTypeNames::mousemove) |
| return; |
| |
| double time = value().toDouble(); |
| if (event->type() == EventTypeNames::input) { |
| // FIXME: This will need to take the timeline offset into consideration |
| // once that concept is supported, see https://crbug.com/312699 |
| if (mediaElement().seekable()->contain(time)) |
| mediaElement().setCurrentTime(time); |
| } |
| |
| LayoutSliderItem slider = LayoutSliderItem(toLayoutSlider(layoutObject())); |
| if (!slider.isNull() && slider.inDragMode()) |
| mediaControls().updateCurrentTimeDisplay(); |
| } |
| |
| bool MediaControlTimelineElement::willRespondToMouseClickEvents() { |
| return isConnected() && document().isActive(); |
| } |
| |
| void MediaControlTimelineElement::setPosition(double currentTime) { |
| setValue(String::number(currentTime)); |
| |
| if (LayoutObject* layoutObject = this->layoutObject()) |
| layoutObject->setShouldDoFullPaintInvalidation(); |
| } |
| |
| void MediaControlTimelineElement::setDuration(double duration) { |
| setFloatingPointAttribute(maxAttr, std::isfinite(duration) ? duration : 0); |
| |
| if (LayoutObject* layoutObject = this->layoutObject()) |
| layoutObject->setShouldDoFullPaintInvalidation(); |
| } |
| |
| bool MediaControlTimelineElement::keepEventInNode(Event* event) { |
| return isUserInteractionEventForSlider(event, layoutObject()); |
| } |
| |
| // ---------------------------- |
| |
| MediaControlVolumeSliderElement::MediaControlVolumeSliderElement( |
| MediaControls& mediaControls) |
| : MediaControlInputElement(mediaControls, MediaVolumeSlider) {} |
| |
| MediaControlVolumeSliderElement* MediaControlVolumeSliderElement::create( |
| MediaControls& mediaControls) { |
| MediaControlVolumeSliderElement* slider = |
| new MediaControlVolumeSliderElement(mediaControls); |
| slider->ensureUserAgentShadowRoot(); |
| slider->setType(InputTypeNames::range); |
| slider->setAttribute(stepAttr, "any"); |
| slider->setAttribute(maxAttr, "1"); |
| slider->setShadowPseudoId( |
| AtomicString("-webkit-media-controls-volume-slider")); |
| return slider; |
| } |
| |
| void MediaControlVolumeSliderElement::defaultEventHandler(Event* event) { |
| if (event->isMouseEvent() && |
| toMouseEvent(event)->button() != |
| static_cast<short>(WebPointerProperties::Button::Left)) |
| return; |
| |
| if (!isConnected() || !document().isActive()) |
| return; |
| |
| MediaControlInputElement::defaultEventHandler(event); |
| |
| if (event->type() == EventTypeNames::mouseover || |
| event->type() == EventTypeNames::mouseout || |
| event->type() == EventTypeNames::mousemove) |
| return; |
| |
| if (event->type() == EventTypeNames::mousedown) |
| Platform::current()->recordAction( |
| UserMetricsAction("Media.Controls.VolumeChangeBegin")); |
| |
| if (event->type() == EventTypeNames::mouseup) |
| Platform::current()->recordAction( |
| UserMetricsAction("Media.Controls.VolumeChangeEnd")); |
| |
| double volume = value().toDouble(); |
| mediaElement().setVolume(volume, ASSERT_NO_EXCEPTION); |
| mediaElement().setMuted(false); |
| } |
| |
| bool MediaControlVolumeSliderElement::willRespondToMouseMoveEvents() { |
| if (!isConnected() || !document().isActive()) |
| return false; |
| |
| return MediaControlInputElement::willRespondToMouseMoveEvents(); |
| } |
| |
| bool MediaControlVolumeSliderElement::willRespondToMouseClickEvents() { |
| if (!isConnected() || !document().isActive()) |
| return false; |
| |
| return MediaControlInputElement::willRespondToMouseClickEvents(); |
| } |
| |
| void MediaControlVolumeSliderElement::setVolume(double volume) { |
| if (value().toDouble() != volume) |
| setValue(String::number(volume)); |
| } |
| |
| bool MediaControlVolumeSliderElement::keepEventInNode(Event* event) { |
| return isUserInteractionEventForSlider(event, layoutObject()); |
| } |
| |
| // ---------------------------- |
| |
| MediaControlFullscreenButtonElement::MediaControlFullscreenButtonElement( |
| MediaControls& mediaControls) |
| : MediaControlInputElement(mediaControls, MediaEnterFullscreenButton) {} |
| |
| MediaControlFullscreenButtonElement* |
| MediaControlFullscreenButtonElement::create(MediaControls& mediaControls) { |
| MediaControlFullscreenButtonElement* button = |
| new MediaControlFullscreenButtonElement(mediaControls); |
| button->ensureUserAgentShadowRoot(); |
| button->setType(InputTypeNames::button); |
| button->setShadowPseudoId( |
| AtomicString("-webkit-media-controls-fullscreen-button")); |
| button->setIsWanted(false); |
| return button; |
| } |
| |
| void MediaControlFullscreenButtonElement::defaultEventHandler(Event* event) { |
| if (event->type() == EventTypeNames::click) { |
| if (mediaElement().isFullscreen()) { |
| Platform::current()->recordAction( |
| UserMetricsAction("Media.Controls.ExitFullscreen")); |
| mediaElement().exitFullscreen(); |
| } else { |
| Platform::current()->recordAction( |
| UserMetricsAction("Media.Controls.EnterFullscreen")); |
| mediaElement().enterFullscreen(); |
| } |
| event->setDefaultHandled(); |
| } |
| MediaControlInputElement::defaultEventHandler(event); |
| } |
| |
| void MediaControlFullscreenButtonElement::setIsFullscreen(bool isFullscreen) { |
| setDisplayType(isFullscreen ? MediaExitFullscreenButton |
| : MediaEnterFullscreenButton); |
| } |
| |
| WebLocalizedString::Name |
| MediaControlFullscreenButtonElement::getOverflowStringName() { |
| if (mediaElement().isFullscreen()) |
| return WebLocalizedString::OverflowMenuExitFullscreen; |
| return WebLocalizedString::OverflowMenuEnterFullscreen; |
| } |
| |
| // ---------------------------- |
| |
| MediaControlCastButtonElement::MediaControlCastButtonElement( |
| MediaControls& mediaControls, |
| bool isOverlayButton) |
| : MediaControlInputElement(mediaControls, MediaCastOnButton), |
| m_isOverlayButton(isOverlayButton) { |
| if (m_isOverlayButton) |
| recordMetrics(CastOverlayMetrics::Created); |
| setIsPlayingRemotely(false); |
| } |
| |
| MediaControlCastButtonElement* MediaControlCastButtonElement::create( |
| MediaControls& mediaControls, |
| bool isOverlayButton) { |
| MediaControlCastButtonElement* button = |
| new MediaControlCastButtonElement(mediaControls, isOverlayButton); |
| button->ensureUserAgentShadowRoot(); |
| button->setType(InputTypeNames::button); |
| return button; |
| } |
| |
| void MediaControlCastButtonElement::defaultEventHandler(Event* event) { |
| if (event->type() == EventTypeNames::click) { |
| if (m_isOverlayButton) |
| Platform::current()->recordAction( |
| UserMetricsAction("Media.Controls.CastOverlay")); |
| else |
| Platform::current()->recordAction( |
| UserMetricsAction("Media.Controls.Cast")); |
| |
| if (m_isOverlayButton && !m_clickUseCounted) { |
| m_clickUseCounted = true; |
| recordMetrics(CastOverlayMetrics::Clicked); |
| } |
| if (mediaElement().isPlayingRemotely()) { |
| mediaElement().requestRemotePlaybackControl(); |
| } else { |
| mediaElement().requestRemotePlayback(); |
| } |
| } |
| MediaControlInputElement::defaultEventHandler(event); |
| } |
| |
| const AtomicString& MediaControlCastButtonElement::shadowPseudoId() const { |
| DEFINE_STATIC_LOCAL(AtomicString, id_nonOverlay, |
| ("-internal-media-controls-cast-button")); |
| DEFINE_STATIC_LOCAL(AtomicString, id_overlay, |
| ("-internal-media-controls-overlay-cast-button")); |
| return m_isOverlayButton ? id_overlay : id_nonOverlay; |
| } |
| |
| void MediaControlCastButtonElement::setIsPlayingRemotely( |
| bool isPlayingRemotely) { |
| if (isPlayingRemotely) { |
| if (m_isOverlayButton) { |
| setDisplayType(MediaOverlayCastOnButton); |
| } else { |
| setDisplayType(MediaCastOnButton); |
| } |
| } else { |
| if (m_isOverlayButton) { |
| setDisplayType(MediaOverlayCastOffButton); |
| } else { |
| setDisplayType(MediaCastOffButton); |
| } |
| } |
| updateOverflowString(); |
| } |
| |
| WebLocalizedString::Name |
| MediaControlCastButtonElement::getOverflowStringName() { |
| if (mediaElement().isPlayingRemotely()) |
| return WebLocalizedString::OverflowMenuStopCast; |
| return WebLocalizedString::OverflowMenuCast; |
| } |
| |
| void MediaControlCastButtonElement::tryShowOverlay() { |
| DCHECK(m_isOverlayButton); |
| |
| setIsWanted(true); |
| if (elementFromCenter(*this) != &mediaElement()) { |
| setIsWanted(false); |
| return; |
| } |
| |
| DCHECK(isWanted()); |
| if (!m_showUseCounted) { |
| m_showUseCounted = true; |
| recordMetrics(CastOverlayMetrics::Shown); |
| } |
| } |
| |
| bool MediaControlCastButtonElement::keepEventInNode(Event* event) { |
| return isUserInteractionEvent(event); |
| } |
| |
| void MediaControlCastButtonElement::recordMetrics(CastOverlayMetrics metric) { |
| DCHECK(m_isOverlayButton); |
| DEFINE_STATIC_LOCAL( |
| EnumerationHistogram, overlayHistogram, |
| ("Cast.Sender.Overlay", static_cast<int>(CastOverlayMetrics::Count))); |
| overlayHistogram.count(static_cast<int>(metric)); |
| } |
| |
| // ---------------------------- |
| |
| MediaControlTimeRemainingDisplayElement:: |
| MediaControlTimeRemainingDisplayElement(MediaControls& mediaControls) |
| : MediaControlTimeDisplayElement(mediaControls, MediaTimeRemainingDisplay) { |
| } |
| |
| MediaControlTimeRemainingDisplayElement* |
| MediaControlTimeRemainingDisplayElement::create(MediaControls& mediaControls) { |
| MediaControlTimeRemainingDisplayElement* element = |
| new MediaControlTimeRemainingDisplayElement(mediaControls); |
| element->setShadowPseudoId( |
| AtomicString("-webkit-media-controls-time-remaining-display")); |
| return element; |
| } |
| |
| // ---------------------------- |
| |
| MediaControlCurrentTimeDisplayElement::MediaControlCurrentTimeDisplayElement( |
| MediaControls& mediaControls) |
| : MediaControlTimeDisplayElement(mediaControls, MediaCurrentTimeDisplay) {} |
| |
| MediaControlCurrentTimeDisplayElement* |
| MediaControlCurrentTimeDisplayElement::create(MediaControls& mediaControls) { |
| MediaControlCurrentTimeDisplayElement* element = |
| new MediaControlCurrentTimeDisplayElement(mediaControls); |
| element->setShadowPseudoId( |
| AtomicString("-webkit-media-controls-current-time-display")); |
| return element; |
| } |
| |
| } // namespace blink |