Summary: Refactors the plugin architecture of Sonar: - Before plugin rendering had it's own implementation of the react lifecycle. This means the `render`-function was not called by react, but rather by the application it self. In this diff, the render method is now called from react, which enables better debugging and allows react to do optimizations. - Business logic for querying emulators is moved away from the view components into its own dispatcher - All plugin handling is moved from `App.js` to `PluginContainer`. - The sidebar only shows one selected device. This allows us to add the screenshot feature as part of the Sonar main app and not a plugin. - This also fixes the inconsistency between the devices button and the sidebar Reviewed By: jknoxville Differential Revision: D8186933 fbshipit-source-id: 46404443025bcf18d6eeba0679e098d5440822d5
365 lines
9.2 KiB
JavaScript
365 lines
9.2 KiB
JavaScript
/**
|
|
* Copyright 2018-present Facebook.
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
* @format
|
|
*/
|
|
|
|
import Glyph from './Glyph.js';
|
|
import styled from '../styled/index.js';
|
|
import type {StyledComponent} from '../styled/index.js';
|
|
import {findDOMNode} from 'react-dom';
|
|
import PropTypes from 'prop-types';
|
|
import {colors} from './colors.js';
|
|
import {connect} from 'react-redux';
|
|
import electron from 'electron';
|
|
|
|
const borderColor = props => {
|
|
if (!props.windowIsFocused) {
|
|
return colors.macOSTitleBarButtonBorderBlur;
|
|
} else if (props.type === 'danger') {
|
|
return colors.red;
|
|
} else {
|
|
return colors.macOSTitleBarButtonBorder;
|
|
}
|
|
};
|
|
const borderBottomColor = props => {
|
|
if (!props.windowIsFocused) {
|
|
return colors.macOSTitleBarButtonBorderBlur;
|
|
} else if (props.type === 'danger') {
|
|
return colors.red;
|
|
} else {
|
|
return colors.macOSTitleBarButtonBorderBottom;
|
|
}
|
|
};
|
|
|
|
const StyledButton = styled.view(
|
|
{
|
|
backgroundColor: props => {
|
|
if (!props.windowIsFocused) {
|
|
return colors.macOSTitleBarButtonBackgroundBlur;
|
|
} else {
|
|
return colors.white;
|
|
}
|
|
},
|
|
backgroundImage: props =>
|
|
props.windowIsFocused
|
|
? `linear-gradient(to bottom, transparent 0%,${
|
|
colors.macOSTitleBarButtonBackground
|
|
} 100%)`
|
|
: 'none',
|
|
borderStyle: 'solid',
|
|
borderWidth: 1,
|
|
borderColor,
|
|
borderBottomColor,
|
|
fontSize: props => (props.compact === true ? 11 : '1em'),
|
|
color: props => {
|
|
if (props.type === 'danger' && props.windowIsFocused) {
|
|
return colors.red;
|
|
} else if (props.disabled) {
|
|
return colors.macOSTitleBarIconBlur;
|
|
} else {
|
|
return colors.light50;
|
|
}
|
|
},
|
|
borderRadius: 4,
|
|
position: 'relative',
|
|
padding: '0 6px',
|
|
height: props => (props.compact === true ? 24 : 28),
|
|
margin: 0,
|
|
marginLeft: props => (props.inButtonGroup === true ? 0 : 10),
|
|
minWidth: 34,
|
|
display: 'inline-flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
flexShrink: 0,
|
|
|
|
boxShadow: props =>
|
|
props.pulse && props.windowIsFocused
|
|
? `0 0 0 ${colors.macOSTitleBarIconSelected}`
|
|
: '',
|
|
animation: props =>
|
|
props.pulse && props.windowIsFocused ? 'pulse 1s infinite' : '',
|
|
|
|
'&:not(:first-child)': {
|
|
borderTopLeftRadius: props => (props.inButtonGroup === true ? 0 : 4),
|
|
borderBottomLeftRadius: props => (props.inButtonGroup === true ? 0 : 4),
|
|
},
|
|
|
|
'&:not(:last-child)': {
|
|
borderTopRightRadius: props => (props.inButtonGroup === true ? 0 : 4),
|
|
borderBottomRightRadius: props => (props.inButtonGroup === true ? 0 : 4),
|
|
borderRight: props => (props.inButtonGroup === true ? 0 : ''),
|
|
},
|
|
|
|
'&:first-of-type': {
|
|
marginLeft: 0,
|
|
},
|
|
|
|
'&:active': {
|
|
borderColor: colors.macOSTitleBarButtonBorder,
|
|
borderBottomColor: colors.macOSTitleBarButtonBorderBottom,
|
|
background: `linear-gradient(to bottom, ${
|
|
colors.macOSTitleBarButtonBackgroundActiveHighlight
|
|
} 1px, ${colors.macOSTitleBarButtonBackgroundActive} 0%, ${
|
|
colors.macOSTitleBarButtonBorderBlur
|
|
} 100%)`,
|
|
},
|
|
|
|
'&:disabled': {
|
|
borderColor,
|
|
borderBottomColor,
|
|
pointerEvents: 'none',
|
|
},
|
|
|
|
'&:hover::before': {
|
|
content: props => (props.dropdown ? "''" : ''),
|
|
position: 'absolute',
|
|
bottom: 1,
|
|
right: 2,
|
|
borderStyle: 'solid',
|
|
borderWidth: '4px 3px 0 3px',
|
|
borderColor: props =>
|
|
`${colors.macOSTitleBarIcon} transparent transparent transparent`,
|
|
},
|
|
},
|
|
{
|
|
ignoreAttributes: [
|
|
'dropdown',
|
|
'dispatch',
|
|
'compact',
|
|
'large',
|
|
'windowIsFocused',
|
|
'inButtonGroup',
|
|
'danger',
|
|
'pulse',
|
|
],
|
|
},
|
|
);
|
|
|
|
const Icon = Glyph.extends(
|
|
{
|
|
marginRight: props => (props.hasText ? 3 : 0),
|
|
},
|
|
{
|
|
ignoreAttributes: ['hasText', 'type'],
|
|
},
|
|
);
|
|
|
|
type Props = {
|
|
/**
|
|
* onClick handler.
|
|
*/
|
|
onClick?: (event: SyntheticMouseEvent<>) => void,
|
|
/**
|
|
* Whether this button is disabled.
|
|
*/
|
|
disabled?: boolean,
|
|
/**
|
|
* Whether this button is large. Increases padding and line-height.
|
|
*/
|
|
large?: boolean,
|
|
/**
|
|
* Whether this button is compact. Decreases padding and line-height.
|
|
*/
|
|
compact?: boolean,
|
|
/**
|
|
* Type of button.
|
|
*/
|
|
type?: 'primary' | 'success' | 'warning' | 'danger',
|
|
/**
|
|
* Children.
|
|
*/
|
|
children?: React$Node,
|
|
/**
|
|
* Dropdown menu template shown on click.
|
|
*/
|
|
dropdown?: Array<Electron$MenuItemOptions>,
|
|
/**
|
|
* Name of the icon dispalyed next to the text
|
|
*/
|
|
icon?: string,
|
|
iconSize?: number,
|
|
/**
|
|
* For toggle buttons, if the button is selected
|
|
*/
|
|
selected?: boolean,
|
|
/**
|
|
* Button is pulsing
|
|
*/
|
|
pulse?: boolean,
|
|
/**
|
|
* URL to open in the browser on click
|
|
*/
|
|
href?: string,
|
|
};
|
|
|
|
type State = {
|
|
active: boolean,
|
|
};
|
|
|
|
/**
|
|
* Simple button.
|
|
*
|
|
* **Usage**
|
|
*
|
|
* ```jsx
|
|
* import {Button} from 'sonar';
|
|
* <Button onClick={handler}>Click me</Button>
|
|
* ```
|
|
*
|
|
* @example Default button
|
|
* <Button>Click me</Button>
|
|
* @example Primary button
|
|
* <Button type="primary">Click me</Button>
|
|
* @example Success button
|
|
* <Button type="success">Click me</Button>
|
|
* @example Warning button
|
|
* <Button type="warning">Click me</Button>
|
|
* @example Danger button
|
|
* <Button type="danger">Click me</Button>
|
|
* @example Default solid button
|
|
* <Button solid={true}>Click me</Button>
|
|
* @example Primary solid button
|
|
* <Button type="primary" solid={true}>Click me</Button>
|
|
* @example Success solid button
|
|
* <Button type="success" solid={true}>Click me</Button>
|
|
* @example Warning solid button
|
|
* <Button type="warning" solid={true}>Click me</Button>
|
|
* @example Danger solid button
|
|
* <Button type="danger" solid={true}>Click me</Button>
|
|
* @example Compact button
|
|
* <Button compact={true}>Click me</Button>
|
|
* @example Large button
|
|
* <Button large={true}>Click me</Button>
|
|
* @example Disabled button
|
|
* <Button disabled={true}>Click me</Button>
|
|
*/
|
|
class Button extends styled.StylableComponent<
|
|
Props & {windowIsFocused: boolean},
|
|
State,
|
|
> {
|
|
static contextTypes = {
|
|
inButtonGroup: PropTypes.bool,
|
|
};
|
|
|
|
state = {
|
|
active: false,
|
|
};
|
|
|
|
_ref: ?Element | ?Text;
|
|
|
|
onMouseDown = () => this.setState({active: true});
|
|
onMouseUp = () => this.setState({active: false});
|
|
|
|
onClick = (e: SyntheticMouseEvent<>) => {
|
|
if (this.props.disabled === true) {
|
|
return;
|
|
}
|
|
if (this.props.dropdown) {
|
|
const menu = electron.remote.Menu.buildFromTemplate(this.props.dropdown);
|
|
const position = {};
|
|
if (this._ref != null && this._ref instanceof Element) {
|
|
const {left, bottom} = this._ref.getBoundingClientRect();
|
|
position.x = parseInt(left, 10);
|
|
position.y = parseInt(bottom + 6, 10);
|
|
}
|
|
menu.popup(electron.remote.getCurrentWindow(), {
|
|
async: true,
|
|
...position,
|
|
});
|
|
}
|
|
if (this.props.onClick) {
|
|
this.props.onClick(e);
|
|
}
|
|
if (this.props.href != null) {
|
|
electron.shell.openExternal(this.props.href);
|
|
}
|
|
};
|
|
|
|
setRef = (ref: ?React.ElementRef<any>) => {
|
|
this._ref = findDOMNode(ref);
|
|
};
|
|
|
|
render() {
|
|
const {
|
|
icon,
|
|
children,
|
|
selected,
|
|
iconSize,
|
|
windowIsFocused,
|
|
...props
|
|
} = this.props;
|
|
const {active} = this.state;
|
|
|
|
let color = colors.macOSTitleBarIcon;
|
|
if (props.disabled === true) {
|
|
color = colors.macOSTitleBarIconBlur;
|
|
} else if (windowIsFocused && selected === true) {
|
|
color = colors.macOSTitleBarIconSelected;
|
|
} else if (!windowIsFocused && (selected == null || selected === false)) {
|
|
color = colors.macOSTitleBarIconBlur;
|
|
} else if (!windowIsFocused && selected === true) {
|
|
color = colors.macOSTitleBarIconSelectedBlur;
|
|
} else if (selected == null && active) {
|
|
color = colors.macOSTitleBarIconActive;
|
|
} else if (props.type === 'danger') {
|
|
color = colors.red;
|
|
}
|
|
|
|
let iconComponent;
|
|
if (icon != null) {
|
|
iconComponent = (
|
|
<Icon
|
|
name={icon}
|
|
size={
|
|
iconSize != null ? iconSize : this.props.compact === true ? 12 : 16
|
|
}
|
|
color={color}
|
|
hasText={Boolean(children)}
|
|
/>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<StyledButton
|
|
{...props}
|
|
ref={this.setRef}
|
|
windowIsFocused={windowIsFocused}
|
|
onClick={this.onClick}
|
|
onMouseDown={this.onMouseDown}
|
|
onMouseUp={this.onMouseUp}
|
|
inButtonGroup={this.context.inButtonGroup}>
|
|
{iconComponent}
|
|
{children}
|
|
{this.props.pulse === true && (
|
|
<style
|
|
dangerouslySetInnerHTML={{
|
|
__html: `
|
|
@keyframes pulse {
|
|
0% {
|
|
box-shadow: 0 0 4px 0 ${colors.macOSTitleBarIconSelected};
|
|
}
|
|
70% {
|
|
box-shadow: 0 0 4px 6px transparent;
|
|
}
|
|
100% {
|
|
box-shadow: 0 0 4px 0 transparent;
|
|
}
|
|
}
|
|
`,
|
|
}}
|
|
/>
|
|
)}
|
|
</StyledButton>
|
|
);
|
|
}
|
|
}
|
|
|
|
const ConnectedButton = connect(({application: {windowIsFocused}}) => ({
|
|
windowIsFocused,
|
|
}))(Button);
|
|
|
|
// $FlowFixMe
|
|
export default (ConnectedButton: StyledComponent<Props>);
|