diff --git a/src/plugins/layout/layout2/Inspector.js b/src/plugins/layout/Inspector.js similarity index 100% rename from src/plugins/layout/layout2/Inspector.js rename to src/plugins/layout/Inspector.js diff --git a/src/plugins/layout/layout2/InspectorSidebar.js b/src/plugins/layout/InspectorSidebar.js similarity index 97% rename from src/plugins/layout/layout2/InspectorSidebar.js rename to src/plugins/layout/InspectorSidebar.js index f3deebc1e..56f543544 100644 --- a/src/plugins/layout/layout2/InspectorSidebar.js +++ b/src/plugins/layout/InspectorSidebar.js @@ -7,8 +7,8 @@ import type {Element} from 'flipper'; import type {PluginClient} from 'flipper'; -import type Client from '../../../Client.js'; -import type {Logger} from '../../../fb-interfaces/Logger.js'; +import type Client from '../../Client.js'; +import type {Logger} from '../../fb-interfaces/Logger.js'; import { GK, diff --git a/src/plugins/layout/layout2/ProxyArchiveClient.js b/src/plugins/layout/ProxyArchiveClient.js similarity index 98% rename from src/plugins/layout/layout2/ProxyArchiveClient.js rename to src/plugins/layout/ProxyArchiveClient.js index a81f18907..681a9dc47 100644 --- a/src/plugins/layout/layout2/ProxyArchiveClient.js +++ b/src/plugins/layout/ProxyArchiveClient.js @@ -8,7 +8,8 @@ import type {Element, ElementID} from 'flipper'; import type {PersistedState} from './index'; import type {SearchResultTree} from './Search'; -import {cloneDeep} from 'lodash'; +// $FlowFixMe +import cloneDeep from 'lodash.clonedeep'; const propsForPersistedState = ( AXMode: boolean, diff --git a/src/plugins/layout/layout2/Search.js b/src/plugins/layout/Search.js similarity index 100% rename from src/plugins/layout/layout2/Search.js rename to src/plugins/layout/Search.js diff --git a/src/plugins/layout/layout2/ToolbarIcon.js b/src/plugins/layout/ToolbarIcon.js similarity index 100% rename from src/plugins/layout/layout2/ToolbarIcon.js rename to src/plugins/layout/ToolbarIcon.js diff --git a/src/plugins/layout/layout2/__tests__/ProxyArchiveClient.node.js b/src/plugins/layout/__tests__/ProxyArchiveClient.node.js similarity index 100% rename from src/plugins/layout/layout2/__tests__/ProxyArchiveClient.node.js rename to src/plugins/layout/__tests__/ProxyArchiveClient.node.js diff --git a/src/plugins/layout/index.js b/src/plugins/layout/index.js index fafe64504..d93975171 100644 --- a/src/plugins/layout/index.js +++ b/src/plugins/layout/index.js @@ -5,1238 +5,258 @@ * @format */ -import type {ElementID, Element, ElementSearchResultSet} from 'flipper'; -import { - colors, - Glyph, - FlexRow, - FlexColumn, - Toolbar, - FlipperPlugin, - ElementsInspector, - InspectorSidebar, - LoadingIndicator, - styled, - Component, - SearchBox, - SearchInput, - SearchIcon, - DetailSidebar, - VerticalRule, - Popover, - ToggleButton, - SidebarExtensions, - GK, +import type { + ElementID, + Element, + ElementSearchResultSet, + Store, + PluginClient, } from 'flipper'; -// $FlowFixMe perf_hooks is a new API in node -import {performance} from 'perf_hooks'; -import Layout2 from './layout2/index.js'; +import { + FlexColumn, + FlexRow, + FlipperPlugin, + Toolbar, + Sidebar, + Link, + Glyph, + DetailSidebar, + styled, +} from 'flipper'; +import Inspector from './Inspector'; +import ToolbarIcon from './ToolbarIcon'; +import InspectorSidebar from './InspectorSidebar'; +import Search from './Search'; +import ProxyArchiveClient from './ProxyArchiveClient'; -import type {TrackType} from '../../fb-interfaces/Logger.js'; - -import debounce from 'lodash.debounce'; - -export type InspectorState = {| - initialised: boolean, - selected: ?ElementID, - root: ?ElementID, - elements: {[key: ElementID]: Element}, - isSearchActive: boolean, - searchResults: ?ElementSearchResultSet, - outstandingSearchQuery: ?string, - // properties for ax mode - AXinitialised: boolean, - AXselected: ?ElementID, - AXfocused: ?ElementID, - AXroot: ?ElementID, - AXelements: {[key: ElementID]: Element}, +type State = {| + init: boolean, + inTargetMode: boolean, inAXMode: boolean, - forceLithoAXRender: boolean, - AXtoNonAXMapping: {[key: ElementID]: ElementID}, - accessibilitySettingsOpen: boolean, - showLithoAccessibilitySettings: boolean, - // - isAlignmentMode: boolean, - logCounter: number, + inAlignmentMode: boolean, + selectedElement: ?ElementID, + selectedAXElement: ?ElementID, + searchResults: ?ElementSearchResultSet, |}; -type SelectElementArgs = {| - key: ElementID, - AXkey: ElementID, +export type ElementMap = {[key: ElementID]: Element}; + +export type PersistedState = {| + rootElement: ?ElementID, + rootAXElement: ?ElementID, + elements: ElementMap, + AXelements: ElementMap, |}; -type ExpandElementArgs = {| - key: ElementID, - expand: boolean, -|}; - -type ExpandElementsArgs = {| - elements: Array, -|}; - -type UpdateElementsArgs = {| - elements: Array<$Shape>, -|}; - -type UpdateAXElementsArgs = {| - elements: Array<$Shape>, - forFocusEvent: boolean, -|}; - -type AXFocusEventResult = {| - isFocus: boolean, - isClick?: boolean, -|}; - -type SetRootArgs = {| - root: ElementID, -|}; - -type GetNodesResult = {| - elements: Array, -|}; - -type GetNodesOptions = {| - force: boolean, - ax: boolean, - forAccessibilityEvent?: boolean, -|}; - -type TrackArgs = {| - type: TrackType, - eventName: string, - data?: any, -|}; - -type SearchResultTree = {| - id: string, - isMatch: Boolean, - hasChildren: boolean, - children: ?Array, - element: Element, - axElement: Element, -|}; - -const LoadingSpinner = styled(LoadingIndicator)({ - marginRight: 4, - marginLeft: 3, - marginTop: -1, +const BetaBar = styled(Toolbar)({ + display: 'block', + overflow: 'hidden', + lineHeight: '15px', + whiteSpace: 'nowrap', + textOverflow: 'ellipsis', }); -const Center = styled(FlexRow)({ - alignItems: 'center', - justifyContent: 'center', -}); - -const SearchIconContainer = styled('div')({ - marginRight: 9, - marginTop: -3, - marginLeft: 4, - position: 'relative', // for settings popover positioning -}); - -const SettingsItem = styled('div')({ - display: 'flex', - flexDirection: 'row', - alignItems: 'center', -}); - -const SettingsLabel = styled('div')({ - marginLeft: 5, - marginRight: 15, -}); - -class LayoutSearchInput extends Component< - { - onSubmit: string => void, - }, - { - value: string, - }, -> { - static TextInput = styled('input')({ - width: '100%', - marginLeft: 6, - }); - - state = { - value: '', - }; - - timer: TimeoutID; - - onChange = (e: SyntheticInputEvent<>) => { - clearTimeout(this.timer); - this.setState({ - value: e.target.value, - }); - this.timer = setTimeout(() => this.props.onSubmit(this.state.value), 200); - }; - - onKeyDown = (e: SyntheticKeyboardEvent<>) => { - if (e.key === 'Enter') { - this.props.onSubmit(this.state.value); +export default class Layout extends FlipperPlugin { + static exportPersistedState = ( + callClient: (string, ?Object) => Promise, + persistedState: ?PersistedState, + store: ?Store, + ): Promise => { + const defaultPromise = Promise.resolve(persistedState); + if (!store) { + return defaultPromise; } + return callClient('getAllNodes').then(({allNodes}) => allNodes); }; - render() { - return ( - - ); - } -} - -class Layout extends FlipperPlugin { - state = { + static defaultPersistedState = { + rootElement: null, + rootAXElement: null, elements: {}, - initialised: false, - isSearchActive: false, - root: null, - selected: null, - searchResults: null, - outstandingSearchQuery: null, - // properties for ax mode - inAXMode: false, - forceLithoAXRender: true, AXelements: {}, - AXinitialised: false, - AXroot: null, - AXselected: null, - AXfocused: null, - accessibilitySettingsOpen: false, - AXtoNonAXMapping: {}, - showLithoAccessibilitySettings: false, - // - isAlignmentMode: false, - logCounter: 0, }; - reducers = { - SelectElement(state: InspectorState, {key, AXkey}: SelectElementArgs) { - return { - selected: key, - AXselected: AXkey, - }; - }, - - ExpandElement(state: InspectorState, {expand, key}: ExpandElementArgs) { - return { - elements: { - ...state.elements, - [key]: { - ...state.elements[key], - expanded: expand, - }, - }, - }; - }, - - ExpandAXElement(state: InspectorState, {expand, key}: ExpandElementArgs) { - return { - AXelements: { - ...state.AXelements, - [key]: { - ...state.AXelements[key], - expanded: expand, - }, - }, - }; - }, - - ExpandElements(state: InspectorState, {elements}: ExpandElementsArgs) { - const expandedSet = new Set(elements); - const newState = { - elements: { - ...state.elements, - }, - }; - for (const key of Object.keys(state.elements)) { - newState.elements[key] = { - ...newState.elements[key], - expanded: expandedSet.has(key), - }; - } - return newState; - }, - - ExpandAXElements(state: InspectorState, {elements}: ExpandElementsArgs) { - const expandedSet = new Set(elements); - const newState = { - AXelements: { - ...state.AXelements, - }, - }; - for (const key of Object.keys(state.AXelements)) { - newState.AXelements[key] = { - ...newState.AXelements[key], - expanded: expandedSet.has(key), - }; - } - return newState; - }, - - UpdateElements(state: InspectorState, {elements}: UpdateElementsArgs) { - const updatedElements = state.elements; - const updatedMapping = state.AXtoNonAXMapping; - - for (const element of elements) { - const current = updatedElements[element.id] || {}; - updatedElements[element.id] = { - ...current, - ...element, - }; - const linked = element.extraInfo && element.extraInfo.linkedAXNode; - if (linked && !updatedMapping[linked]) { - updatedMapping[linked] = element.id; - } - } - - return {elements: updatedElements, AXtoNonAXMapping: updatedMapping}; - }, - - UpdateAXElements( - state: InspectorState, - {elements, forFocusEvent}: UpdateAXElementsArgs, - ) { - const updatedElements = state.AXelements; - - // if focusEvent, previously focused element can be reset - let updatedFocus = forFocusEvent ? null : state.AXfocused; - - for (const element of elements) { - if (element.extraInfo && element.extraInfo.focused) { - updatedFocus = element.id; - } - const current = updatedElements[element.id] || {}; - updatedElements[element.id] = { - ...current, - ...element, - }; - } - - return { - AXelements: updatedElements, - AXfocused: updatedFocus, - }; - }, - - SetRoot(state: InspectorState, {root}: SetRootArgs) { - return {root}; - }, - - SetAXRoot(state: InspectorState, {root}: SetRootArgs) { - return {AXroot: root}; - }, - - SetSearchActive( - state: InspectorState, - {isSearchActive}: {isSearchActive: boolean}, - ) { - return {isSearchActive}; - }, - - SetAlignmentActive( - state: InspectorState, - {isAlignmentMode}: {isAlignmentMode: boolean}, - ) { - return {isAlignmentMode}; - }, - - SetAXMode(state: InspectorState, {inAXMode}: {inAXMode: boolean}) { - return {inAXMode}; - }, - - SetLithoRenderMode( - state: InspectorState, - {forceLithoAXRender}: {forceLithoAXRender: boolean}, - ) { - return {forceLithoAXRender}; - }, - - SetAccessibilitySettingsOpen( - state: InspectorState, - {accessibilitySettingsOpen}: {accessibilitySettingsOpen: boolean}, - ) { - return {accessibilitySettingsOpen}; - }, + state = { + init: false, + inTargetMode: false, + inAXMode: false, + inAlignmentMode: false, + selectedElement: null, + selectedAXElement: null, + searchResults: null, }; - search(query: string) { - this.setState({ - outstandingSearchQuery: query, - }); - - if (!query) { - this.displaySearchResults({query: '', results: null}); - } else { - this.client - .call('getSearchResults', {query: query, axEnabled: this.axEnabled()}) - .then(response => this.displaySearchResults(response)); - } - } - - executeCommand(command: string) { - return this.client.call('executeCommand', { - command: command, - context: this.state.inAXMode - ? this.state.AXselected - : this.state.selected, - }); - } - - /** - * When opening the inspector for the first time, expand all elements that contain only 1 child - * recursively. - */ - async performInitialExpand(element: Element, ax: boolean): Promise { - if (!element.children.length) { - // element has no children so we're as deep as we can be - return; - } - - this.dispatchAction({ - expand: true, - key: element.id, - type: ax ? 'ExpandAXElement' : 'ExpandElement', - }); - - return this.getChildren(element.id, ax).then((elements: Array) => { - this.dispatchAction({ - elements, - type: ax ? 'UpdateAXElements' : 'UpdateElements', - }); - - if (element.children.length >= 2) { - // element has two or more children so we can stop expanding - return; - } - - return this.performInitialExpand( - (ax ? this.state.AXelements : this.state.elements)[element.children[0]], - ax, - ); - }); - } - - displaySearchResults({ - results, - query, - }: { - results: ?SearchResultTree, - query: string, - }) { - const elements = this.getElementsFromSearchResultTree(results); - const idsToExpand = elements - .filter(x => x.hasChildren) - .map(x => x.element.id); - - const finishedSearching = query === this.state.outstandingSearchQuery; - - this.dispatchAction({ - elements: elements.map(x => x.element), - type: 'UpdateElements', - }); - - this.dispatchAction({ - elements: idsToExpand, - type: 'ExpandElements', - }); - - if (this.axEnabled()) { - const AXelements = elements.filter(x => x.axElement); - const AXidsToExpand = AXelements.filter(x => x.hasChildren).map( - x => x.axElement.id, - ); - - this.dispatchAction({ - elements: AXelements.map(x => x.axElement), - type: 'UpdateAXElements', - }); - - this.dispatchAction({ - elements: AXidsToExpand, - type: 'ExpandAXElements', - }); - } - - this.setState({ - searchResults: { - matches: new Set( - elements.filter(x => x.isMatch).map(x => x.element.id), - ), - query: query, - }, - outstandingSearchQuery: finishedSearching - ? null - : this.state.outstandingSearchQuery, - }); - } - - getElementsFromSearchResultTree( - tree: ?SearchResultTree, - ): Array { - if (!tree) { - return []; - } - let elements = [ - { - id: tree.id, - isMatch: tree.isMatch, - hasChildren: Boolean(tree.children), - element: tree.element, - axElement: tree.axElement, - }, - ]; - if (tree.children) { - for (const child of tree.children) { - elements = elements.concat(this.getElementsFromSearchResultTree(child)); - } - } - return elements; - } - - axEnabled(): boolean { - // only visible internally for Android clients - return this.realClient.query.os === 'Android'; - } - - // expand tree and highlight click-to-inspect node that was found - onSelectResultsRecieved(path: Array, ax: boolean) { - this.getNodesAndDirectChildren(path, ax).then( - (elements: Array) => { - const selected = path[path.length - 1]; - - this.dispatchAction({ - elements, - type: ax ? 'UpdateAXElements' : 'UpdateElements', - }); - - // select node from ax tree if in ax mode - // select node from main tree if not in ax mode - // (also selects corresponding node in other tree if it exists) - if ((ax && this.state.inAXMode) || (!ax && !this.state.inAXMode)) { - const {key, AXkey} = this.getKeysFromSelected(selected); - this.dispatchAction({key, AXkey, type: 'SelectElement'}); - } - - this.dispatchAction({ - isSearchActive: false, - type: 'SetSearchActive', - }); - - for (const key of path) { - this.dispatchAction({ - expand: true, - key, - type: ax ? 'ExpandAXElement' : 'ExpandElement', - }); - } - - this.client.call('setHighlighted', { - id: selected, - isAlignmentMode: this.state.isAlignmentMode, - }); - - this.client.call('setSearchActive', {active: false}); - }, - ); - } - - initAX() { - this.client - .call('shouldShowLithoAccessibilitySettings') - .then((showLithoAccessibilitySettings: boolean) => { - this.setState({ - showLithoAccessibilitySettings, - }); - }); - - performance.mark('InitAXRoot'); - this.client.call('getAXRoot').then((element: Element) => { - this.dispatchAction({elements: [element], type: 'UpdateAXElements'}); - this.dispatchAction({root: element.id, type: 'SetAXRoot'}); - this.performInitialExpand(element, true).then(() => { - this.props.logger.trackTimeSince('InitAXRoot', 'accessibility:getRoot'); - this.setState({AXinitialised: true}); - }); - }); - - this.client.subscribe( - 'axFocusEvent', - ({isFocus, isClick}: AXFocusEventResult) => { - this.props.logger.track('usage', 'accessibility:focusEvent', { - isFocus, - isClick, - inAXMode: this.state.inAXMode, - }); - - // if focusing, need to update all elements in the tree because - // we don't know which one now has focus - const keys = isFocus ? Object.keys(this.state.AXelements) : []; - - // if unfocusing, update only the focused and selected elements and - // only if they have been loaded into tree - if (!isFocus) { - if ( - this.state.AXfocused && - this.state.AXelements[this.state.AXfocused] - ) { - keys.push(this.state.AXfocused); - } - - // also update current selected element live, so data shown is not invalid - if ( - this.state.AXselected && - this.state.AXelements[this.state.AXselected] - ) { - keys.push(this.state.AXselected); - } - } - - this.getNodes(keys, { - force: true, - ax: true, - forAccessibilityEvent: true, - }).then((elements: Array) => { - this.dispatchAction({ - elements, - forFocusEvent: !isClick, - type: 'UpdateAXElements', - }); - }); - }, - ); - - this.client.subscribe( - 'invalidateAX', - ({nodes}: {nodes: Array<{id: ElementID}>}) => { - this.invalidate(nodes.map(node => node.id), true).then( - (elements: Array) => { - this.dispatchAction({elements, type: 'UpdateAXElements'}); - }, - ); - }, - ); - - this.client.subscribe('selectAX', ({path}: {path: Array}) => { - if (this.state.inAXMode) { - this.props.logger.track('usage', 'accessibility:clickToInspect'); - } - this.onSelectResultsRecieved(path, true); - }); - - this.client.subscribe('track', ({type, eventName, data}: TrackArgs) => { - this.props.logger.track(type, eventName, data); - }); - } - init() { + if (!this.props.persistedState) { + // If the selected plugin from the previous session was layout, then while importing the flipper trace, the redux store doesn't get updated in the first render, due to which the plugin crashes, as it has no persisted state + this.props.setPersistedState(this.constructor.defaultPersistedState); + } // persist searchActive state when moving between plugins to prevent multiple // TouchOverlayViews since we can't edit the view heirarchy in onDisconnect this.client.call('isSearchActive').then(({isSearchActive}) => { - this.dispatchAction({type: 'SetSearchActive', isSearchActive}); + this.setState({inTargetMode: isSearchActive}); }); - performance.mark('LayoutInspectorInitialize'); - this.client.call('getRoot').then((element: Element) => { - this.dispatchAction({elements: [element], type: 'UpdateElements'}); - this.dispatchAction({root: element.id, type: 'SetRoot'}); - this.performInitialExpand(element, false).then(() => { - this.props.logger.trackTimeSince('LayoutInspectorInitialize'); - this.setState({initialised: true}); - }); - }); - - this.client.subscribe( - 'invalidate', - ({nodes}: {nodes: Array<{id: ElementID}>}) => { - this.invalidate(nodes.map(node => node.id), false).then( - (elements: Array) => { - this.dispatchAction({elements, type: 'UpdateElements'}); - }, - ); - }, - ); - - this.client.subscribe('select', ({path}: {path: Array}) => { - this.onSelectResultsRecieved(path, false); - }); - - if (this.axEnabled()) { - this.props.logger.track('usage', 'accessibility:init'); - this.initAX(); - } - } - - invalidate(ids: Array, ax: boolean): Promise> { - if (ids.length === 0) { - return Promise.resolve([]); - } - - return this.getNodes(ids, {force: true, ax}).then( - (elements: Array) => { - const children = elements - .filter(element => { - const prev = (ax ? this.state.AXelements : this.state.elements)[ - element.id - ]; - return prev && prev.expanded; - }) - .map(element => element.children) - .reduce((acc, val) => acc.concat(val), []); - - return Promise.all([elements, this.invalidate(children, ax)]).then( - arr => { - return arr.reduce((acc, val) => acc.concat(val), []); - }, - ); - }, - ); - } - - getNodesAndDirectChildren( - ids: Array, - ax: boolean, - ): Promise> { - return this.getNodes(ids, {force: false, ax}).then( - (elements: Array) => { - const children = elements - .map(element => element.children) - .reduce((acc, val) => acc.concat(val), []); - - return Promise.all([ - elements, - this.getNodes(children, {force: false, ax}), - ]).then(arr => { - return arr.reduce((acc, val) => acc.concat(val), []); - }); - }, - ); - } - - getChildren(key: ElementID, ax: boolean): Promise> { - return this.getNodes( - (ax ? this.state.AXelements : this.state.elements)[key].children, - {force: false, ax}, - ); - } - - getNodes( - ids: Array = [], - options: GetNodesOptions, - ): Promise> { - const {force, ax, forAccessibilityEvent} = options; - if (!force) { - const elems = ax ? this.state.AXelements : this.state.elements; - // always force undefined elements and elements that need to be expanded - // over in the main tree (e.g. fragments) - ids = ids.filter(id => { - return ( - !elems[id] || - (elems[id].extraInfo && elems[id].extraInfo.nonAXWithAXChild) - ); - }); - } - - if (ids.length > 0) { - // prevents overlapping calls from interfering with each other's logging - const mark = 'LayoutInspectorGetNodes' + this.state.logCounter++; - const eventName = ax - ? 'accessibility:getNodes' - : 'LayoutInspectorGetNodes'; - - performance.mark(mark); - return this.client - .call(ax ? 'getAXNodes' : 'getNodes', { - ids, - forAccessibilityEvent, - selected: this.state.AXselected, - }) - .then(({elements}: GetNodesResult) => { - this.props.logger.trackTimeSince(mark, eventName); - return Promise.resolve(elements); - }); - } else { - return Promise.resolve([]); - } - } - - isExpanded(key: ElementID, ax: boolean): boolean { - return ax - ? this.state.AXelements[key].expanded - : this.state.elements[key].expanded; - } - - expandElement = (key: ElementID, ax: boolean): Promise> => { - const expand = !this.isExpanded(key, ax); - return this.setElementExpanded(key, expand, ax); - }; - - setElementExpanded = ( - key: ElementID, - expand: boolean, - ax: boolean, - ): Promise> => { - this.dispatchAction({ - expand, - key, - type: ax ? 'ExpandAXElement' : 'ExpandElement', - }); - - const mark = ax ? 'ExpandAXElement' : 'LayoutInspectorExpandElement'; - const eventName = ax - ? 'accessibility:expandElement' - : 'LayoutInspectorExpandElement'; - - performance.mark(mark); - if (expand) { - return this.getChildren(key, ax).then((elements: Array) => { - this.dispatchAction({ - elements, - type: ax ? 'UpdateAXElements' : 'UpdateElements', - }); - this.props.logger.trackTimeSince(mark, eventName); - - // only expand extra components in the main tree when in AX mode - if (this.state.inAXMode && !ax) { - // expand child wrapper elements that aren't in the AX tree (e.g. fragments) - for (const childElem of elements) { - if (childElem.extraInfo && childElem.extraInfo.nonAXWithAXChild) { - this.setElementExpanded(childElem.id, true, false); - } - } - } - return Promise.resolve(elements); - }); - } else { - return Promise.resolve([]); - } - }; - - deepExpandElement = async (key: ElementID, ax: boolean) => { - const expand = !this.isExpanded(key, ax); - if (!expand) { - // we never deep unexpand - return this.setElementExpanded(key, false, ax); - } - - // queue of keys to open - const keys = [key]; - - // amount of elements we've expanded, we stop at 100 just to be safe - let count = 0; - - while (keys.length && count < 100) { - const key = keys.shift(); - - // expand current element - const children = await this.setElementExpanded(key, true, ax); - - // and add its children to the queue - for (const child of children) { - keys.push(child.id); + // disable target mode after + this.client.subscribe('select', () => { + if (this.state.inTargetMode) { + this.onToggleTargetMode(); } - - count++; - } - }; - - onElementExpanded = (key: ElementID, deep: boolean) => { - if (this.state.elements[key]) { - if (deep) { - this.deepExpandElement(key, false); - } else { - this.expandElement(key, false); - } - this.props.logger.track('usage', 'layout:element-expanded', { - id: key, - deep: deep, - }); - } - - if (this.state.AXelements[key]) { - if (deep) { - this.deepExpandElement(key, true); - } else { - this.expandElement(key, true); - } - if (this.state.inAXMode) { - this.props.logger.track('usage', 'accessibility:elementExpanded', { - id: key, - deep: deep, - }); - } - } - }; - - onFindClick = () => { - const isSearchActive = !this.state.isSearchActive; - this.dispatchAction({isSearchActive, type: 'SetSearchActive'}); - this.client.call('setSearchActive', {active: isSearchActive}); - }; - - onToggleAccessibility = () => { - const inAXMode = !this.state.inAXMode; - const { - forceLithoAXRender, - AXroot, - showLithoAccessibilitySettings, - } = this.state; - this.props.logger.track('usage', 'accessibility:modeToggled', {inAXMode}); - this.dispatchAction({inAXMode, type: 'SetAXMode'}); - - // only force render if litho accessibility is included in app - if (showLithoAccessibilitySettings) { - this.client.call('forceLithoAXRender', { - forceLithoAXRender: inAXMode && forceLithoAXRender, - applicationId: AXroot, - }); - } - }; - - onToggleForceLithoAXRender = () => { - // only force render if litho accessibility is included in app - if (this.state.showLithoAccessibilitySettings) { - const forceLithoAXRender = !this.state.forceLithoAXRender; - const applicationId = this.state.AXroot; - this.dispatchAction({forceLithoAXRender, type: 'SetLithoRenderMode'}); - this.client.call('forceLithoAXRender', { - forceLithoAXRender: forceLithoAXRender, - applicationId, - }); - } - }; - - onOpenAccessibilitySettings = () => { - this.dispatchAction({ - accessibilitySettingsOpen: true, - type: 'SetAccessibilitySettingsOpen', }); - }; - onCloseAccessibilitySettings = () => { - this.dispatchAction({ - accessibilitySettingsOpen: false, - type: 'SetAccessibilitySettingsOpen', - }); - }; - - onToggleAlignment = () => { - const isAlignmentMode = !this.state.isAlignmentMode; - this.dispatchAction({isAlignmentMode, type: 'SetAlignmentActive'}); - }; - - getKeysFromSelected(selectedKey: ElementID) { - let key = selectedKey; - let AXkey = null; - - if (this.axEnabled()) { - const linkedAXNode = - this.state.elements[selectedKey] && - this.state.elements[selectedKey].extraInfo && - this.state.elements[selectedKey].extraInfo.linkedAXNode; - - // element only in main tree with linkedAXNode selected - if (linkedAXNode) { - AXkey = linkedAXNode; - - // element only in AX tree with linked nonAX (litho) element selected - } else if ( - !this.state.elements[selectedKey] || - this.state.elements[selectedKey].name === 'ComponentHost' - ) { - key = this.state.AXtoNonAXMapping[selectedKey] || null; - AXkey = selectedKey; - - // keys are same for both trees or 'linked' element does not exist - } else { - AXkey = selectedKey; - } - } - - return {key, AXkey}; + this.setState({init: true}); } - onElementSelected = debounce((selectedKey: ElementID) => { - const {key, AXkey} = this.getKeysFromSelected(selectedKey); - this.dispatchAction({key, AXkey, type: 'SelectElement'}); + onToggleTargetMode = () => { + const inTargetMode = !this.state.inTargetMode; + this.setState({inTargetMode}); + this.client.send('setSearchActive', {active: inTargetMode}); + }; - this.client.call('setHighlighted', { - id: selectedKey, - isAlignmentMode: this.state.isAlignmentMode, - }); + onToggleAXMode = () => { + this.setState({inAXMode: !this.state.inAXMode}); + }; - if (key) { - this.getNodes([key], {force: true, ax: false}).then( - (elements: Array) => { - this.dispatchAction({elements, type: 'UpdateElements'}); - }, - ); - } - - if (AXkey) { - this.getNodes([AXkey], {force: true, ax: true}).then( - (elements: Array) => { - this.dispatchAction({elements, type: 'UpdateAXElements'}); - }, - ); - } - - if (this.state.inAXMode) { - this.props.logger.track('usage', 'accessibility:selectElement'); - } - }); - - onElementHovered = debounce((key: ?ElementID) => { - this.client.call('setHighlighted', { - id: key, - isAlignmentMode: this.state.isAlignmentMode, - }); - }); - - getAXContextMenuExtensions() { - return [ - { - label: 'Focus', - click: (id: ElementID) => { - this.client.call('onRequestAXFocus', {id}); - }, - }, - ]; + getClient(): PluginClient { + return this.props.isArchivedDevice + ? new ProxyArchiveClient(this.props.persistedState) + : this.client; } + onToggleAlignmentMode = () => { + if (this.state.selectedElement) { + this.client.send('setHighlighted', { + id: this.state.selectedElement, + inAlignmentMode: !this.state.inAlignmentMode, + }); + } + this.setState({inAlignmentMode: !this.state.inAlignmentMode}); + }; onDataValueChanged = (path: Array, value: any) => { - const ax = this.state.inAXMode; - const id = ax ? this.state.AXselected : this.state.selected; - this.client - .call('setData', {id, path, value, ax}) - .then((element: Element) => { - if (ax) { - this.dispatchAction({ - elements: [element], - type: 'UpdateAXElements', - }); - } - }); - - const eventName = ax - ? 'accessibility:dataValueChanged' - : 'layout:value-changed'; - this.props.logger.track('usage', eventName, { + const id = this.state.inAXMode + ? this.state.selectedAXElement + : this.state.selectedElement; + this.client.call('setData', { id, - value, path, + value, + ax: this.state.inAXMode, }); }; - // returns object with all sidebar elements that should show more information - // on hover (needs to be kept up-to-date if names of properties change) - getAccessibilityTooltips() { - return { - 'accessibility-focused': - 'True if this element has the focus of an accessibility service', - 'content-description': - 'Text to label the content or functionality of this element ', - 'important-for-accessibility': - 'Marks this element as important to accessibility services; one of AUTO, YES, NO, NO_HIDE_DESCENDANTS', - 'talkback-focusable': 'True if Talkback can focus on this element', - 'talkback-focusable-reasons': 'Why Talkback can focus on this element', - 'talkback-ignored': 'True if Talkback cannot focus on this element', - 'talkback-ignored-reasons': 'Why Talkback cannot focus on the element', - 'talkback-output': - 'What Talkback will say when this element is focused (derived from role, content-description, and state of the element)', - 'talkback-hint': - 'What Talkback will say after output if hints are enabled', - }; - } - - renderSidebar = () => { - if (this.state.inAXMode) { - // empty if no element selected w/in AX node tree - return ( - this.state.AXselected && ( - - ) - ); - } else { - // empty if no element selected w/in view tree - return ( - this.state.selected != null && ( - - ) - ); - } - }; - - getAccessibilitySettingsPopover(forceLithoAXRender: boolean) { - return ( - - - - Force Litho Accessibility Rendering - - - ); - } - render() { - const { - initialised, - AXinitialised, - selected, - AXselected, - AXfocused, - root, - AXroot, - elements, - AXelements, - isSearchActive, - inAXMode, - forceLithoAXRender, - outstandingSearchQuery, - isAlignmentMode, - accessibilitySettingsOpen, - showLithoAccessibilitySettings, - } = this.state; + const inspectorProps = { + client: this.getClient(), + inAlignmentMode: this.state.inAlignmentMode, + selectedElement: this.state.selectedElement, + selectedAXElement: this.state.selectedAXElement, + setPersistedState: this.props.setPersistedState, + persistedState: this.props.persistedState, + onDataValueChanged: this.onDataValueChanged, + searchResults: this.state.searchResults, + }; + + let element; + if (this.state.inAXMode && this.state.selectedAXElement) { + element = this.props.persistedState.AXelements[ + this.state.selectedAXElement + ]; + } else if (this.state.selectedElement) { + element = this.props.persistedState.elements[this.state.selectedElement]; + } + + const inspector = ( + this.setState({selectedElement})} + showsSidebar={!this.state.inAXMode} + /> + ); return ( - - - - - {this.axEnabled() ? ( - - + + {!this.props.isArchivedDevice && ( + + )} + {this.realClient.query.os === 'Android' && ( + + )} + {!this.props.isArchivedDevice && ( + + )} + + + this.setState({searchResults}) } + inAXMode={this.state.inAXMode} /> - - ) : null} - - - - - - - {outstandingSearchQuery && } - - {inAXMode && showLithoAccessibilitySettings && ( - - + + + {this.state.inAXMode ? ( + <> + + {inspector} + + + this.setState({selectedAXElement}) + } + showsSidebar={true} + ax + /> + + ) : ( + inspector + )} + + + - {accessibilitySettingsOpen && - this.getAccessibilitySettingsPopover(forceLithoAXRender)} - - )} - - - {initialised ? ( - - ) : ( -
- -
- )} - {AXinitialised && inAXMode ? : null} - {AXinitialised && inAXMode ? ( - - ) : null} -
- {this.renderSidebar()} + + + )} + {/* TODO: Remove this when rolling out publicly */} + + +   + Version 2.0:  Provide feedback about this plugin + in our  + + feedback group + + . +
); } } - -export default (GK.get('flipper_layout_inspector_new') ? Layout2 : Layout); diff --git a/src/plugins/layout/layout2/index.js b/src/plugins/layout/layout2/index.js deleted file mode 100644 index d93975171..000000000 --- a/src/plugins/layout/layout2/index.js +++ /dev/null @@ -1,262 +0,0 @@ -/** - * 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 type { - ElementID, - Element, - ElementSearchResultSet, - Store, - PluginClient, -} from 'flipper'; - -import { - FlexColumn, - FlexRow, - FlipperPlugin, - Toolbar, - Sidebar, - Link, - Glyph, - DetailSidebar, - styled, -} from 'flipper'; -import Inspector from './Inspector'; -import ToolbarIcon from './ToolbarIcon'; -import InspectorSidebar from './InspectorSidebar'; -import Search from './Search'; -import ProxyArchiveClient from './ProxyArchiveClient'; - -type State = {| - init: boolean, - inTargetMode: boolean, - inAXMode: boolean, - inAlignmentMode: boolean, - selectedElement: ?ElementID, - selectedAXElement: ?ElementID, - searchResults: ?ElementSearchResultSet, -|}; - -export type ElementMap = {[key: ElementID]: Element}; - -export type PersistedState = {| - rootElement: ?ElementID, - rootAXElement: ?ElementID, - elements: ElementMap, - AXelements: ElementMap, -|}; - -const BetaBar = styled(Toolbar)({ - display: 'block', - overflow: 'hidden', - lineHeight: '15px', - whiteSpace: 'nowrap', - textOverflow: 'ellipsis', -}); - -export default class Layout extends FlipperPlugin { - static exportPersistedState = ( - callClient: (string, ?Object) => Promise, - persistedState: ?PersistedState, - store: ?Store, - ): Promise => { - const defaultPromise = Promise.resolve(persistedState); - if (!store) { - return defaultPromise; - } - return callClient('getAllNodes').then(({allNodes}) => allNodes); - }; - - static defaultPersistedState = { - rootElement: null, - rootAXElement: null, - elements: {}, - AXelements: {}, - }; - - state = { - init: false, - inTargetMode: false, - inAXMode: false, - inAlignmentMode: false, - selectedElement: null, - selectedAXElement: null, - searchResults: null, - }; - - init() { - if (!this.props.persistedState) { - // If the selected plugin from the previous session was layout, then while importing the flipper trace, the redux store doesn't get updated in the first render, due to which the plugin crashes, as it has no persisted state - this.props.setPersistedState(this.constructor.defaultPersistedState); - } - // persist searchActive state when moving between plugins to prevent multiple - // TouchOverlayViews since we can't edit the view heirarchy in onDisconnect - this.client.call('isSearchActive').then(({isSearchActive}) => { - this.setState({inTargetMode: isSearchActive}); - }); - - // disable target mode after - this.client.subscribe('select', () => { - if (this.state.inTargetMode) { - this.onToggleTargetMode(); - } - }); - - this.setState({init: true}); - } - - onToggleTargetMode = () => { - const inTargetMode = !this.state.inTargetMode; - this.setState({inTargetMode}); - this.client.send('setSearchActive', {active: inTargetMode}); - }; - - onToggleAXMode = () => { - this.setState({inAXMode: !this.state.inAXMode}); - }; - - getClient(): PluginClient { - return this.props.isArchivedDevice - ? new ProxyArchiveClient(this.props.persistedState) - : this.client; - } - onToggleAlignmentMode = () => { - if (this.state.selectedElement) { - this.client.send('setHighlighted', { - id: this.state.selectedElement, - inAlignmentMode: !this.state.inAlignmentMode, - }); - } - this.setState({inAlignmentMode: !this.state.inAlignmentMode}); - }; - - onDataValueChanged = (path: Array, value: any) => { - const id = this.state.inAXMode - ? this.state.selectedAXElement - : this.state.selectedElement; - this.client.call('setData', { - id, - path, - value, - ax: this.state.inAXMode, - }); - }; - - render() { - const inspectorProps = { - client: this.getClient(), - inAlignmentMode: this.state.inAlignmentMode, - selectedElement: this.state.selectedElement, - selectedAXElement: this.state.selectedAXElement, - setPersistedState: this.props.setPersistedState, - persistedState: this.props.persistedState, - onDataValueChanged: this.onDataValueChanged, - searchResults: this.state.searchResults, - }; - - let element; - if (this.state.inAXMode && this.state.selectedAXElement) { - element = this.props.persistedState.AXelements[ - this.state.selectedAXElement - ]; - } else if (this.state.selectedElement) { - element = this.props.persistedState.elements[this.state.selectedElement]; - } - - const inspector = ( - this.setState({selectedElement})} - showsSidebar={!this.state.inAXMode} - /> - ); - - return ( - - {this.state.init && ( - <> - - {!this.props.isArchivedDevice && ( - - )} - {this.realClient.query.os === 'Android' && ( - - )} - {!this.props.isArchivedDevice && ( - - )} - - - this.setState({searchResults}) - } - inAXMode={this.state.inAXMode} - /> - - - - {this.state.inAXMode ? ( - <> - - {inspector} - - - this.setState({selectedAXElement}) - } - showsSidebar={true} - ax - /> - - ) : ( - inspector - )} - - - - - - )} - {/* TODO: Remove this when rolling out publicly */} - - -   - Version 2.0:  Provide feedback about this plugin - in our  - - feedback group - - . - - - ); - } -} diff --git a/src/plugins/layout/package.json b/src/plugins/layout/package.json index 23def7cd0..1a1c9a610 100644 --- a/src/plugins/layout/package.json +++ b/src/plugins/layout/package.json @@ -5,7 +5,7 @@ "license": "MIT", "dependencies": { "deep-equal": "^1.0.1", - "lodash": "^4.17.11", + "lodash.clonedeep": "^4.5.0", "lodash.debounce": "^4.0.8" }, "title": "Layout", diff --git a/src/plugins/layout/yarn.lock b/src/plugins/layout/yarn.lock index f5830ed7a..78ed2dd38 100644 --- a/src/plugins/layout/yarn.lock +++ b/src/plugins/layout/yarn.lock @@ -7,11 +7,12 @@ deep-equal@^1.0.1: resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5" integrity sha1-9dJgKStmDghO/0zbyfCK0yR0SLU= +lodash.clonedeep@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" + integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8= + lodash.debounce@^4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" - -lodash@^4.17.11: - version "4.17.11" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" - integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg== + integrity sha1-gteb/zCmfEAF/9XiUVMArZyk168=