molstar
Version:
A comprehensive macromolecular library.
322 lines (321 loc) • 21.3 kB
JavaScript
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
/**
* Copyright (c) 2019-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author David Sehnal <david.sehnal@gmail.com>
* @author Jason Pattle <jpattle.exscientia.co.uk>
* @author Ludovic Autin <ludovic.autin@gmail.com>
* @author Ventura Rivera <venturaxrivera@gmail.com>
*/
import * as React from 'react';
import { getElementQueries, getNonStandardResidueQueries, getPolymerAndBranchedEntityQueries, StructureSelectionQueries } from '../../mol-plugin-state/helpers/structure-selection-query';
import { InteractivityManager } from '../../mol-plugin-state/manager/interactivity';
import { StructureComponentManager } from '../../mol-plugin-state/manager/structure/component';
import { PluginConfig } from '../../mol-plugin/config';
import { compileIdListSelection } from '../../mol-script/util/id-list';
import { memoizeLatest } from '../../mol-util/memoize';
import { ParamDefinition } from '../../mol-util/param-definition';
import { capitalize, stripTags } from '../../mol-util/string';
import { PluginUIComponent, PurePluginUIComponent } from '../base';
import { ActionMenu } from '../controls/action-menu';
import { Button, ControlGroup, IconButton, ToggleButton } from '../controls/common';
import { BrushSvg, CancelOutlinedSvg, CloseSvg, CubeOutlineSvg, HelpOutlineSvg, Icon, IntersectSvg, RemoveSvg, RestoreSvg, SelectionModeSvg, SetSvg, SubtractSvg, UnionSvg } from '../controls/icons';
import { ParameterControls, PureSelectControl } from '../controls/parameters';
import { HelpGroup, HelpText, ViewportHelpContent } from '../viewport/help';
import { AddComponentControls } from './components';
export class ToggleSelectionModeButton extends PurePluginUIComponent {
constructor() {
super(...arguments);
this._toggleSelMode = () => {
this.plugin.selectionMode = !this.plugin.selectionMode;
};
}
componentDidMount() {
this.subscribe(this.plugin.events.canvas3d.settingsUpdated, () => this.forceUpdate());
this.subscribe(this.plugin.layout.events.updated, () => this.forceUpdate());
this.subscribe(this.plugin.behaviors.interaction.selectionMode, () => this.forceUpdate());
}
render() {
const style = this.props.inline
? { background: 'transparent', width: 'auto', height: 'auto', lineHeight: 'unset' }
: { background: 'transparent' };
return _jsx(IconButton, { svg: SelectionModeSvg, onClick: this._toggleSelMode, title: 'Toggle Selection Mode', style: style, toggleState: this.plugin.selectionMode });
}
}
const StructureSelectionParams = {
granularity: InteractivityManager.Params.granularity,
};
const ActionHeader = new Map([
['add', 'Add/Union Selection'],
['remove', 'Remove/Subtract Selection'],
['intersect', 'Intersect Selection'],
['set', 'Set Selection']
]);
export class StructureSelectionActionsControls extends PluginUIComponent {
constructor() {
super(...arguments);
this.state = {
action: void 0,
helper: void 0,
isEmpty: true,
isBusy: false,
canUndo: false,
structureSelectionParams: StructureSelectionParams,
};
this.set = (modifier, selectionQuery) => {
this.plugin.managers.structure.selection.fromSelectionQuery(modifier, selectionQuery, false);
};
this.selectQuery = (item, e) => {
if (!item || !this.state.action) {
this.setState({ action: void 0 });
return;
}
const q = this.state.action;
if (e === null || e === void 0 ? void 0 : e.shiftKey) {
this.set(q, item.value);
}
else {
this.setState({ action: void 0 }, () => {
this.set(q, item.value);
});
}
};
this.selectHelper = (item, e) => {
console.log(item);
if (!item || !this.state.action) {
this.setState({ action: void 0, helper: void 0 });
return;
}
this.setState({ helper: item.value.kind });
};
this.queriesItems = [];
this.queriesVersion = -1;
this.helpersItems = void 0;
this.toggleAdd = this.showAction('add');
this.toggleRemove = this.showAction('remove');
this.toggleIntersect = this.showAction('intersect');
this.toggleSet = this.showAction('set');
this.toggleTheme = this.showAction('theme');
this.toggleAddComponent = this.showAction('add-component');
this.toggleHelp = this.showAction('help');
this.setGranuality = ({ value }) => {
this.plugin.managers.interactivity.setProps({ granularity: value });
};
this.turnOff = () => this.plugin.selectionMode = false;
this.undo = () => {
const task = this.plugin.state.data.undo();
if (task)
this.plugin.runTask(task);
};
this.subtract = () => {
const sel = this.plugin.managers.structure.hierarchy.getStructuresWithSelection();
const components = [];
for (const s of sel)
components.push(...s.components);
if (components.length === 0)
return;
this.plugin.managers.structure.component.modifyByCurrentSelection(components, 'subtract');
};
}
componentDidMount() {
var _a, _b;
this.subscribe(this.plugin.managers.structure.hierarchy.behaviors.selection, c => {
const isEmpty = c.hierarchy.structures.length === 0;
if (this.state.isEmpty !== isEmpty) {
this.setState({ isEmpty });
}
// trigger elementQueries and nonStandardResidueQueries recalculation
this.queriesVersion = -1;
this.forceUpdate();
});
this.subscribe(this.plugin.behaviors.state.isBusy, v => {
this.setState({ isBusy: v, action: void 0 });
});
this.subscribe(this.plugin.managers.interactivity.events.propsUpdated, () => {
this.forceUpdate();
});
this.subscribe(this.plugin.state.data.events.historyUpdated, ({ state }) => {
this.setState({ canUndo: state.canUndo });
});
// Update structureSelectionParams state if there are custom-defined granularityOptions
const granularityOptions = (_b = (_a = this.plugin.spec.components) === null || _a === void 0 ? void 0 : _a.selectionTools) === null || _b === void 0 ? void 0 : _b.granularityOptions;
if (granularityOptions) {
const granularitySet = new Set((granularityOptions));
const structureSelectionParams = {
...StructureSelectionParams,
granularity: {
...StructureSelectionParams.granularity,
options: StructureSelectionParams.granularity.options.filter(([firstItem]) => granularitySet.has(firstItem)),
},
};
this.setState({ structureSelectionParams: structureSelectionParams });
}
}
get isDisabled() {
return this.state.isBusy || this.state.isEmpty;
}
get structures() {
var _a;
const structures = [];
for (const s of this.plugin.managers.structure.hierarchy.selection.structures) {
const structure = (_a = s.cell.obj) === null || _a === void 0 ? void 0 : _a.data;
if (structure)
structures.push(structure);
}
return structures;
}
get queries() {
const { registry } = this.plugin.query.structure;
if (registry.version !== this.queriesVersion) {
const structures = this.structures;
const queries = [
...registry.list,
...getPolymerAndBranchedEntityQueries(structures),
...getNonStandardResidueQueries(structures),
...getElementQueries(structures)
].sort((a, b) => b.priority - a.priority);
this.queriesItems = ActionMenu.createItems(queries, {
filter: q => q !== StructureSelectionQueries.current && !q.isHidden,
label: q => q.label,
category: q => q.category,
description: q => q.description
});
this.queriesVersion = registry.version;
}
return this.queriesItems;
}
get helpers() {
if (this.helpersItems)
return this.helpersItems;
// TODO: this is an initial implementation of the helper UI
// the plan is to add support to input queries in different languages
// after this has been implemented in mol-script
const helpers = [
{ kind: 'residue-list', category: 'Helpers', label: 'Atom/Residue Identifier List', description: 'Create a selection from a list of atom/residue ranges.' }
];
this.helpersItems = ActionMenu.createItems(helpers, {
label: q => q.label,
category: q => q.category,
description: q => q.description
});
return this.helpersItems;
}
showAction(q) {
return () => this.setState({ action: this.state.action === q ? void 0 : q, helper: void 0 });
}
render() {
var _a, _b;
const granularity = this.plugin.managers.interactivity.props.granularity;
const hide = (_b = (_a = this.plugin.spec.components) === null || _a === void 0 ? void 0 : _a.selectionTools) === null || _b === void 0 ? void 0 : _b.hide;
const undoTitle = this.state.canUndo
? `Undo ${this.plugin.state.data.latestUndoLabel}`
: 'Some mistakes of the past can be undone.';
let children = void 0;
if (this.state.action && !this.state.helper) {
children = _jsxs(_Fragment, { children: [(this.state.action && this.state.action !== 'theme' && this.state.action !== 'add-component' && this.state.action !== 'help') && _jsxs("div", { className: 'msp-selection-viewport-controls-actions', children: [_jsx(ActionMenu, { header: ActionHeader.get(this.state.action), title: 'Click to close.', items: this.queries, onSelect: this.selectQuery, noOffset: true }), _jsx(ActionMenu, { items: this.helpers, onSelect: this.selectHelper, noOffset: true })] }), this.state.action === 'theme' && _jsx("div", { className: 'msp-selection-viewport-controls-actions', children: _jsx(ControlGroup, { header: 'Theme', title: 'Click to close.', initialExpanded: true, hideExpander: true, hideOffset: true, onHeaderClick: this.toggleTheme, topRightIcon: CloseSvg, children: _jsx(ApplyThemeControls, { onApply: this.toggleTheme }) }) }), this.state.action === 'add-component' && _jsx("div", { className: 'msp-selection-viewport-controls-actions', children: _jsx(ControlGroup, { header: 'Add Component', title: 'Click to close.', initialExpanded: true, hideExpander: true, hideOffset: true, onHeaderClick: this.toggleAddComponent, topRightIcon: CloseSvg, children: _jsx(AddComponentControls, { onApply: this.toggleAddComponent, forSelection: true }) }) }), this.state.action === 'help' && _jsx("div", { className: 'msp-selection-viewport-controls-actions', children: _jsxs(ControlGroup, { header: 'Help', title: 'Click to close.', initialExpanded: true, hideExpander: true, hideOffset: true, onHeaderClick: this.toggleHelp, topRightIcon: CloseSvg, maxHeight: '300px', children: [_jsx(HelpGroup, { header: 'Selection Operations', children: _jsxs(HelpText, { children: ["Use ", _jsx(Icon, { svg: UnionSvg, inline: true }), " ", _jsx(Icon, { svg: SubtractSvg, inline: true }), " ", _jsx(Icon, { svg: IntersectSvg, inline: true }), " ", _jsx(Icon, { svg: SetSvg, inline: true }), " to modify the selection."] }) }), _jsx(HelpGroup, { header: 'Representation Operations', children: _jsxs(HelpText, { children: ["Use ", _jsx(Icon, { svg: BrushSvg, inline: true }), " ", _jsx(Icon, { svg: CubeOutlineSvg, inline: true }), " ", _jsx(Icon, { svg: RemoveSvg, inline: true }), " ", _jsx(Icon, { svg: RestoreSvg, inline: true }), " to color, create components, remove from components, or undo actions."] }) }), _jsx(ViewportHelpContent, { selectOnly: true })] }) })] });
}
else if (ActionHeader.has(this.state.action) && this.state.helper === 'residue-list') {
const close = () => this.setState({ action: void 0, helper: void 0 });
children = _jsx("div", { className: 'msp-selection-viewport-controls-actions', children: _jsx(ControlGroup, { header: 'Atom/Residue Identifier List', title: 'Click to close.', initialExpanded: true, hideExpander: true, hideOffset: true, onHeaderClick: close, topRightIcon: CloseSvg, children: _jsx(ResidueListSelectionHelper, { modifier: this.state.action, plugin: this.plugin, close: close }) }) });
}
return _jsxs(_Fragment, { children: [_jsxs("div", { className: 'msp-flex-row', style: { background: 'none' }, children: [(!(hide === null || hide === void 0 ? void 0 : hide.granularity)) && _jsx(PureSelectControl, { title: `Picking Level for selecting and highlighting`, param: this.state.structureSelectionParams.granularity, name: 'granularity', value: granularity, onChange: this.setGranuality, isDisabled: this.isDisabled }), (!(hide === null || hide === void 0 ? void 0 : hide.union)) && _jsx(ToggleButton, { icon: UnionSvg, title: `${ActionHeader.get('add')}. Hold shift key to keep menu open.`, toggle: this.toggleAdd, isSelected: this.state.action === 'add', disabled: this.isDisabled }), (!(hide === null || hide === void 0 ? void 0 : hide.subtract)) && _jsx(ToggleButton, { icon: SubtractSvg, title: `${ActionHeader.get('remove')}. Hold shift key to keep menu open.`, toggle: this.toggleRemove, isSelected: this.state.action === 'remove', disabled: this.isDisabled }), (!(hide === null || hide === void 0 ? void 0 : hide.intersect)) && _jsx(ToggleButton, { icon: IntersectSvg, title: `${ActionHeader.get('intersect')}. Hold shift key to keep menu open.`, toggle: this.toggleIntersect, isSelected: this.state.action === 'intersect', disabled: this.isDisabled }), (!(hide === null || hide === void 0 ? void 0 : hide.set)) && _jsx(ToggleButton, { icon: SetSvg, title: `${ActionHeader.get('set')}. Hold shift key to keep menu open.`, toggle: this.toggleSet, isSelected: this.state.action === 'set', disabled: this.isDisabled }), (!(hide === null || hide === void 0 ? void 0 : hide.theme)) && _jsx(ToggleButton, { icon: BrushSvg, title: 'Apply Theme to Selection', toggle: this.toggleTheme, isSelected: this.state.action === 'theme', disabled: this.isDisabled, style: { marginLeft: '10px' } }), (!(hide === null || hide === void 0 ? void 0 : hide.componentAdd)) && _jsx(ToggleButton, { icon: CubeOutlineSvg, title: 'Create Component of Selection with Representation', toggle: this.toggleAddComponent, isSelected: this.state.action === 'add-component', disabled: this.isDisabled }), (!(hide === null || hide === void 0 ? void 0 : hide.componentRemove)) && _jsx(IconButton, { svg: RemoveSvg, title: 'Remove/subtract Selection from all Components', onClick: this.subtract, disabled: this.isDisabled }), (!(hide === null || hide === void 0 ? void 0 : hide.undo)) && _jsx(IconButton, { svg: RestoreSvg, onClick: this.undo, disabled: !this.state.canUndo || this.isDisabled, title: undoTitle }), (!(hide === null || hide === void 0 ? void 0 : hide.help)) && _jsx(ToggleButton, { icon: HelpOutlineSvg, title: 'Show/hide help', toggle: this.toggleHelp, style: { marginLeft: '10px' }, isSelected: this.state.action === 'help' }), ((!(hide === null || hide === void 0 ? void 0 : hide.cancel)) && this.plugin.config.get(PluginConfig.Viewport.ShowSelectionMode)) && (_jsx(IconButton, { svg: CancelOutlinedSvg, title: 'Turn selection mode off', onClick: this.turnOff }))] }), children] });
}
}
export class StructureSelectionStatsControls extends PluginUIComponent {
constructor() {
super(...arguments);
this.state = {
isEmpty: true,
isBusy: false
};
this.clear = () => this.plugin.managers.interactivity.lociSelects.deselectAll();
this.focus = () => {
if (this.plugin.managers.structure.selection.stats.elementCount === 0)
return;
const { sphere } = this.plugin.managers.structure.selection.getBoundary();
this.plugin.managers.camera.focusSphere(sphere);
};
this.highlight = (e) => {
this.plugin.managers.interactivity.lociHighlights.clearHighlights();
this.plugin.managers.structure.selection.entries.forEach(e => {
this.plugin.managers.interactivity.lociHighlights.highlight({ loci: e.selection }, false);
});
};
this.clearHighlight = () => {
this.plugin.managers.interactivity.lociHighlights.clearHighlights();
};
}
componentDidMount() {
this.subscribe(this.plugin.managers.structure.selection.events.changed, () => {
this.forceUpdate();
});
this.subscribe(this.plugin.managers.structure.hierarchy.behaviors.selection, c => {
const isEmpty = c.structures.length === 0;
if (this.state.isEmpty !== isEmpty) {
this.setState({ isEmpty });
}
});
this.subscribe(this.plugin.behaviors.state.isBusy, v => {
this.setState({ isBusy: v });
});
}
get isDisabled() {
return this.state.isBusy || this.state.isEmpty;
}
get stats() {
const stats = this.plugin.managers.structure.selection.stats;
if (stats.structureCount === 0 || stats.elementCount === 0) {
return 'Nothing Selected';
}
else {
return `${stripTags(stats.label)} Selected`;
}
}
render() {
const stats = this.plugin.managers.structure.selection.stats;
const empty = stats.structureCount === 0 || stats.elementCount === 0;
if (empty && this.props.hideOnEmpty)
return null;
return _jsx(_Fragment, { children: _jsxs("div", { className: 'msp-flex-row', children: [_jsx(Button, { noOverflow: true, onClick: this.focus, title: 'Click to Focus Selection', disabled: empty, onMouseEnter: this.highlight, onMouseLeave: this.clearHighlight, style: { textAlignLast: !empty ? 'left' : void 0 }, children: this.stats }), !empty && _jsx(IconButton, { svg: CancelOutlinedSvg, onClick: this.clear, title: 'Clear', className: 'msp-form-control', flex: true })] }) });
}
}
class ApplyThemeControls extends PurePluginUIComponent {
constructor() {
super(...arguments);
this._params = memoizeLatest((pivot) => StructureComponentManager.getThemeParams(this.plugin, pivot));
this.state = { values: ParamDefinition.getDefaultValues(this.params) };
this.apply = () => {
var _a, _b;
this.plugin.managers.structure.component.applyTheme(this.state.values, this.plugin.managers.structure.hierarchy.current.structures);
(_b = (_a = this.props).onApply) === null || _b === void 0 ? void 0 : _b.call(_a);
};
this.paramsChanged = (values) => this.setState({ values });
}
get params() { return this._params(this.plugin.managers.structure.component.pivotStructure); }
render() {
return _jsxs(_Fragment, { children: [_jsx(ParameterControls, { params: this.params, values: this.state.values, onChangeValues: this.paramsChanged }), _jsx(Button, { icon: BrushSvg, className: 'msp-btn-commit msp-btn-commit-on', onClick: this.apply, style: { marginTop: '1px' }, children: "Apply Theme" })] });
}
}
const ResidueListIdTypeParams = {
idType: ParamDefinition.Select('auth', ParamDefinition.arrayToOptions(['auth', 'label', 'atom-id', 'element-symbol'])),
identifiers: ParamDefinition.Text('', { description: 'A comma separated list of atom identifiers (e.g. 10, 15-25), element symbols (e.g. N, C or 20-200) or residue ranges in given chain (e.g. A 10-15, B 25, C 30:i)' })
};
const DefaultResidueListIdTypeParams = ParamDefinition.getDefaultValues(ResidueListIdTypeParams);
function ResidueListSelectionHelper({ modifier, plugin, close }) {
const [state, setState] = React.useState(DefaultResidueListIdTypeParams);
const apply = () => {
if (state.identifiers.trim().length === 0)
return;
try {
close();
const query = compileIdListSelection(state.identifiers, state.idType);
plugin.managers.structure.selection.fromCompiledQuery(modifier, query, false);
}
catch (e) {
console.error(e);
plugin.log.error('Failed to create selection');
}
};
return _jsxs(_Fragment, { children: [_jsx(ParameterControls, { params: ResidueListIdTypeParams, values: state, onChangeValues: setState, onEnter: apply }), _jsxs(Button, { className: 'msp-btn-commit msp-btn-commit-on', disabled: state.identifiers.trim().length === 0, onClick: apply, style: { marginTop: '1px' }, children: [capitalize(modifier), " Selection"] })] });
}