blob: 0de34b587b132a4a335d5199e9076f1536da9612 [file] [log] [blame]
/*
* Copyright (C) 2017 Apple 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.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. ``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 INC. OR
* 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.
*/
const InsideMargin = 6; // Minimum margin to guarantee around all controls, this constant needs to stay in sync with the --inline-controls-inside-margin CSS variable.
const BottomControlsBarHeight = 31; // This constant needs to stay in sync with the --inline-controls-bar-height CSS variable.
class InlineMediaControls extends MediaControls
{
constructor(options)
{
super(options);
this.element.classList.add("inline");
this.skipBackButton = new SkipBackButton(this);
this.skipForwardButton = new SkipForwardButton(this);
this.topLeftControlsBar = new ControlsBar("top-left");
this._topLeftControlsBarContainer = this.topLeftControlsBar.addChild(new ButtonsContainer);
this.topRightControlsBar = new ControlsBar("top-right");
this._topRightControlsBarContainer = this.topRightControlsBar.addChild(new ButtonsContainer);
this.leftContainer = new ButtonsContainer({ cssClassName: "left" });
this.rightContainer = new ButtonsContainer({ cssClassName: "right" });
this._shouldUseAudioLayout = false;
this._shouldUseSingleBarLayout = false;
this.showsStartButton = false;
this._updateBottomControlsBarLabel();
}
// Public
set shouldUseAudioLayout(flag)
{
if (this._shouldUseAudioLayout === flag)
return;
this._shouldUseAudioLayout = flag;
this.element.classList.toggle("audio", flag);
this.needsLayout = true;
this._updateBottomControlsBarLabel();
}
set shouldUseSingleBarLayout(flag)
{
if (this._shouldUseSingleBarLayout === flag)
return;
this._shouldUseSingleBarLayout = flag;
this.needsLayout = true;
}
get showsStartButton()
{
return !!this._showsStartButton;
}
set showsStartButton(flag)
{
if (this._showsStartButton === flag)
return;
this._showsStartButton = flag;
this.layout();
}
// Protected
layout()
{
super.layout();
const children = [];
if (this.placard) {
children.push(this.placard);
if (this.placardPreventsControlsBarDisplay()) {
this.children = children;
return;
}
}
if (!this.visible) {
this.children = children;
return;
}
// The controls might be too small to allow showing anything at all.
if (!this._shouldUseAudioLayout && (this.width < MinimumSizeToShowAnyControl || this.height < MinimumSizeToShowAnyControl)) {
this.children = children;
return;
}
// If we should show the start button, then only show that button.
if (this._showsStartButton) {
this.playPauseButton.style = this.width <= MaximumSizeToShowSmallProminentControl || this.height <= MaximumSizeToShowSmallProminentControl ? Button.Styles.SmallCenter : Button.Styles.Center;
this.children = [this.playPauseButton];
return;
}
if (!this.bottomControlsBar)
return;
// Ensure the tracks panel is a child if it were presented.
if (this.tracksPanel.presented)
children.push(this.tracksPanel);
// Update the top left controls bar.
this._topLeftControlsBarContainer.children = this._topLeftContainerButtons();
this._topLeftControlsBarContainer.layout();
this.topLeftControlsBar.width = this._topLeftControlsBarContainer.width;
this.topLeftControlsBar.visible = this._topLeftControlsBarContainer.children.some(button => button.visible);
// Compute the visible size for the controls bar.
this.bottomControlsBar.width = this._shouldUseAudioLayout ? this.width : (this.width - 2 * InsideMargin);
// Compute the absolute minimum width to display the center control (status label or time control).
const centerControl = this.statusLabel.enabled ? this.statusLabel : this.timeControl;
let minimumCenterControlWidth = centerControl.minimumWidth;
// Worst case scenario is that we can't fit the center control with the required margins. In this case,
// we need to make the play/pause button display as a corner button.
const minimumControlsBarWidthForCenterControl = minimumCenterControlWidth + this.leftContainer.leftMargin + this.rightContainer.rightMargin;
if (this.bottomControlsBar.width < minimumControlsBarWidthForCenterControl) {
this.playPauseButton.style = Button.Styles.Corner;
if (!this._shouldUseSingleBarLayout && this.height >= 82) {
children.push(this.topLeftControlsBar);
this._addTopRightBarWithMuteButtonToChildren(children);
}
this.children = children.concat(this.playPauseButton);
return;
}
// Now allow the minimum center element to display with fewer constraints.
minimumCenterControlWidth = centerControl.idealMinimumWidth;
// Iterate through controls to see if we need to drop any of them. Reset all default states before we proceed.
this.bottomControlsBar.visible = true;
this.playPauseButton.style = Button.Styles.Bar;
this.leftContainer.children = this._leftContainerButtons();
this.rightContainer.children = this._rightContainerButtons();
this.rightContainer.children.concat(this.leftContainer.children).forEach(button => delete button.dropped);
this.muteButton.style = this.preferredMuteButtonStyle;
this.muteButton.usesRTLIconVariant = false;
for (let button of this._droppableButtons()) {
// If the button is not enabled, we can skip it.
if (!button.enabled)
continue;
// Ensure button containers are laid out with latest constraints.
this.leftContainer.layout();
this.rightContainer.layout();
// Nothing left to do if the combined width of both containers and the time control is shorter than the available width.
if (this.leftContainer.width + minimumCenterControlWidth + this.rightContainer.width < this.bottomControlsBar.width)
break;
// This button must now be dropped.
button.dropped = true;
}
// Update layouts once more.
this.leftContainer.layout();
this.rightContainer.layout();
const widthLeftOfTimeControl = this.leftContainer.children.length > 0 ? this.leftContainer.width : this.leftContainer.leftMargin;
const widthRightOfTimeControl = this.rightContainer.children.length > 0 ? this.rightContainer.width : this.rightContainer.rightMargin;
centerControl.x = widthLeftOfTimeControl;
centerControl.width = this.bottomControlsBar.width - widthLeftOfTimeControl - widthRightOfTimeControl;
centerControl.layout();
// Add visible children.
const controlsBarChildren = [];
if (this.leftContainer.children.length)
controlsBarChildren.push(this.leftContainer);
controlsBarChildren.push(centerControl);
if (this.rightContainer.children.length) {
controlsBarChildren.push(this.rightContainer);
this.rightContainer.x = this.bottomControlsBar.width - this.rightContainer.width;
}
// Ensure we position the bottom controls bar at the bottom of the frame, accounting for
// the inside margin, unless this would yield a position outside of the frame.
this.bottomControlsBar.y = Math.max(0, this.height - BottomControlsBarHeight - InsideMargin);
this.bottomControlsBar.children = controlsBarChildren;
if (!this._shouldUseAudioLayout && !this._shouldUseSingleBarLayout)
children.push(this.topLeftControlsBar);
children.push(this.bottomControlsBar);
if (this.muteButton.style === Button.Styles.Corner || (this.muteButton.dropped && !this._shouldUseAudioLayout && !this._shouldUseSingleBarLayout))
this._addTopRightBarWithMuteButtonToChildren(children);
this.children = children;
}
commitProperty(propertyName)
{
// We override the default behavior of the "visible" property, which usually means the node
// will not be displayed if false, but we want to allow placards to be visible, even when
// controls are supposed to be hidden.
if (propertyName !== "visible")
super.commitProperty(propertyName);
}
get preferredMuteButtonStyle()
{
return (this._shouldUseAudioLayout || this._shouldUseSingleBarLayout) ? Button.Styles.Bar : Button.Styles.Corner;
}
// Private
_updateBottomControlsBarLabel()
{
this.bottomControlsBar.element.setAttribute("aria-label", this._shouldUseAudioLayout ? UIString("Audio Controls") : UIString("Video Controls"));
}
_topLeftContainerButtons()
{
if (this._shouldUseSingleBarLayout)
return [];
if (this.usesLTRUserInterfaceLayoutDirection)
return [this.fullscreenButton, this.pipButton];
return [this.pipButton, this.fullscreenButton];
}
_leftContainerButtons()
{
return [this.skipBackButton, this.playPauseButton, this.skipForwardButton];
}
_rightContainerButtons()
{
if (this._shouldUseAudioLayout)
return [this.muteButton, this.airplayButton];
if (this._shouldUseSingleBarLayout)
return [this.muteButton, this.airplayButton, this.pipButton, this.tracksButton, this.fullscreenButton];
const buttons = [];
if (this.preferredMuteButtonStyle === Button.Styles.Bar)
buttons.push(this.muteButton);
buttons.push(this.airplayButton, this.tracksButton);
return buttons;
}
_droppableButtons()
{
if (this._shouldUseSingleBarLayout)
return [this.skipForwardButton, this.skipBackButton, this.airplayButton, this.tracksButton, this.pipButton, this.fullscreenButton, this.muteButton];
const buttons = [this.skipForwardButton, this.skipBackButton, this.airplayButton, this.tracksButton];
if (this.preferredMuteButtonStyle === Button.Styles.Bar)
buttons.push(this.muteButton);
return buttons;
}
_addTopRightBarWithMuteButtonToChildren(children)
{
if (!this.muteButton.enabled)
return;
delete this.muteButton.dropped;
this.muteButton.style = Button.Styles.Bar;
this.muteButton.usesRTLIconVariant = !this.usesLTRUserInterfaceLayoutDirection;
this._topRightControlsBarContainer.children = [this.muteButton];
this._topRightControlsBarContainer.layout();
this.topRightControlsBar.width = this._topRightControlsBarContainer.width;
children.push(this.topRightControlsBar);
}
}