pdbe-molstar
Version:
Molstar implementation for PDBe
163 lines (162 loc) • 11.1 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.StateGalleryControls = void 0;
exports.StateGalleryManagerControls = StateGalleryManagerControls;
exports.StateGalleryTitleBox = StateGalleryTitleBox;
const tslib_1 = require("tslib");
const jsx_runtime_1 = require("react/jsx-runtime");
const base_1 = require("molstar/lib/mol-plugin-ui/base");
const common_1 = require("molstar/lib/mol-plugin-ui/controls/common");
const icons_1 = require("molstar/lib/mol-plugin-ui/controls/icons");
const parameters_1 = require("molstar/lib/mol-plugin-ui/controls/parameters");
const param_definition_1 = require("molstar/lib/mol-util/param-definition");
const react_1 = tslib_1.__importStar(require("react"));
const helpers_1 = require("../../helpers");
const icons_2 = require("../../ui/icons");
const behavior_1 = require("./behavior");
const manager_1 = require("./manager");
/** Parameter definition for ParameterControls part of "3D State Gallery" section */
const Params = {
entryId: param_definition_1.ParamDefinition.Text(undefined, { label: 'Entry ID' }),
};
/** "3D State Gallery" section in Structure Tools (right panel) */
class StateGalleryControls extends base_1.CollapsableControls {
constructor() {
super(...arguments);
this.onChangeValues = (values) => {
this.setState({ entryId: values.entryId });
};
/** Load entry given by `this.state.entryId` */
this.load = () => tslib_1.__awaiter(this, void 0, void 0, function* () {
if (this.loadDisabled())
return;
this.setState({ isLoading: true, description: undefined });
const manager = yield manager_1.StateGalleryManager.create(this.plugin, this.state.entryId);
this.setState({ manager, isLoading: false, description: this.state.entryId.toUpperCase() });
});
this.loadDisabled = () => { var _a; return !this.state.entryId || this.state.entryId === ((_a = this.state.manager) === null || _a === void 0 ? void 0 : _a.entryId) || this.state.isBusy || this.state.isLoading; };
}
defaultState() {
var _a, _b;
const existingManager = (_a = (0, behavior_1.StateGalleryCustomState)(this.plugin).manager) === null || _a === void 0 ? void 0 : _a.value;
return {
// CollapsableControls state
header: '3D State Gallery',
brand: { accent: 'green', svg: icons_2.CollectionsOutlinedSvg },
isCollapsed: false,
// Own state
entryId: (_b = existingManager === null || existingManager === void 0 ? void 0 : existingManager.entryId) !== null && _b !== void 0 ? _b : '',
manager: existingManager,
isLoading: false,
isBusy: false,
description: existingManager === null || existingManager === void 0 ? void 0 : existingManager.entryId.toUpperCase(),
};
}
componentDidMount() {
this.subscribe(this.plugin.managers.structure.hierarchy.behaviors.selection, sel => {
var _a;
if (this.state.entryId === '' && sel.structures.length > 0) {
const id = (_a = sel.structures[0].cell.obj) === null || _a === void 0 ? void 0 : _a.data.model.entryId;
if (id) {
this.setState({ entryId: id.toLowerCase() });
}
}
});
this.subscribe(this.plugin.behaviors.state.isBusy, isBusy => this.setState({ isBusy }));
}
get values() {
return {
entryId: this.state.entryId,
};
}
renderControls() {
return (0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(parameters_1.ParameterControls, { params: Params, values: this.values, onChangeValues: this.onChangeValues, onEnter: this.load }), (!this.state.manager || this.state.manager.entryId !== this.state.entryId) &&
(0, jsx_runtime_1.jsx)(common_1.Button, { icon: this.state.isLoading ? undefined : icons_1.CheckSvg, title: 'Load', disabled: this.loadDisabled(), onClick: this.load, className: 'msp-btn-block', children: this.state.isLoading ? 'Loading...' : 'Load' }), this.state.manager &&
(0, jsx_runtime_1.jsx)(StateGalleryManagerControls, { manager: this.state.manager })] });
}
}
exports.StateGalleryControls = StateGalleryControls;
/** Part of "3D State Gallery" section related to a specific entry */
function StateGalleryManagerControls(props) {
var _a;
const images = props.manager.images;
const nImages = images.length;
const categories = react_1.default.useMemo(() => (0, helpers_1.groupElements)(images, img => { var _a; return (_a = img.category) !== null && _a !== void 0 ? _a : 'Miscellaneous'; }), [images]);
const [selected, setSelected] = react_1.default.useState(undefined);
const [status, setStatus] = react_1.default.useState('ready');
const keyDownTargetRef = react_1.default.useRef(null);
const handleKeyDown = (e) => tslib_1.__awaiter(this, void 0, void 0, function* () {
if (e.code === 'ArrowLeft')
yield props.manager.loadPrevious();
if (e.code === 'ArrowRight')
yield props.manager.loadNext();
});
react_1.default.useEffect(() => {
var _a;
if (images.length > 0 && props.manager.events.requestedImage.value === undefined) {
props.manager.load(images[0]);
}
(_a = keyDownTargetRef.current) === null || _a === void 0 ? void 0 : _a.focus();
const subs = [
props.manager.events.status.subscribe(status => setStatus(status)),
props.manager.events.requestedImage.subscribe(img => setSelected(img)),
];
return () => subs.forEach(sub => sub.unsubscribe());
}, [props.manager]);
if (nImages === 0) {
return (0, jsx_runtime_1.jsxs)("div", { style: { margin: 8 }, children: ["No data available for ", props.manager.entryId, "."] });
}
return (0, jsx_runtime_1.jsxs)("div", { className: 'pdbemolstar-state-gallery-controls', onKeyDown: handleKeyDown, tabIndex: -1, ref: keyDownTargetRef, children: [(0, jsx_runtime_1.jsx)(common_1.ExpandGroup, { header: 'States', initiallyExpanded: true, children: (0, jsx_runtime_1.jsx)("div", { style: { marginBottom: 8 }, children: categories.groups.map(cat => {
var _a;
return (0, jsx_runtime_1.jsx)(common_1.ExpandGroup, { header: cat, initiallyExpanded: true, children: (_a = categories.members.get(cat)) === null || _a === void 0 ? void 0 : _a.map(img => (0, jsx_runtime_1.jsx)(ImageButton, { img: img, isSelected: img === selected, status: status, onClick: () => props.manager.load(img) }, img.filename)) }, cat);
}) }) }, 'states'), (0, jsx_runtime_1.jsx)(common_1.ExpandGroup, { header: 'Description', initiallyExpanded: true, children: (0, jsx_runtime_1.jsx)("div", { className: 'pdbemolstar-state-gallery-legend', children: (0, jsx_runtime_1.jsx)("div", { dangerouslySetInnerHTML: { __html: (_a = selected === null || selected === void 0 ? void 0 : selected.description) !== null && _a !== void 0 ? _a : '' } }) }) }, 'description')] });
}
/** Button with image title */
function ImageButton(props) {
var _a;
const { img, isSelected, status, onClick } = props;
const icon = !isSelected ? icons_2.EmptyIconSvg : (status === 'loading') ? icons_2.HourglassBottomSvg : (status === 'error') ? icons_1.ErrorSvg : icons_1.CheckSvg;
const tooltip = imageTooltip(img, isSelected ? status : 'ready');
return (0, jsx_runtime_1.jsxs)(common_1.Button, { className: 'msp-action-menu-button pdbemolstar-state-gallery-state-button', icon: icon, onClick: onClick, title: tooltip, style: { fontWeight: isSelected ? 'bold' : undefined }, children: [(_a = img.title) !== null && _a !== void 0 ? _a : img.filename, img.subtitle && (0, jsx_runtime_1.jsxs)("small", { children: ["\u2002 ", img.subtitle] })] });
}
/** Box in viewport with image title and arrows to move between images (3D states) */
function StateGalleryTitleBox() {
var _a;
const plugin = react_1.default.useContext(base_1.PluginReactContext);
const [image, setImage] = react_1.default.useState(undefined);
const [manager, setManager] = react_1.default.useState(undefined);
const [status, setStatus] = react_1.default.useState('ready');
const loadingCounter = (0, react_1.useRef)(0);
react_1.default.useEffect(() => {
var _a, _b, _c;
const customState = (0, behavior_1.StateGalleryCustomState)(plugin);
const subs = [
(_a = customState.requestedImage) === null || _a === void 0 ? void 0 : _a.subscribe(img => setImage(img)),
(_b = customState.manager) === null || _b === void 0 ? void 0 : _b.subscribe(mgr => setManager(mgr)),
(_c = customState.status) === null || _c === void 0 ? void 0 : _c.subscribe(status => {
const counter = ++loadingCounter.current;
if (status === 'loading') {
setTimeout(() => { if (loadingCounter.current === counter)
setStatus('loading'); }, 250);
}
else {
setStatus(status);
}
}),
];
return () => subs.forEach(sub => sub === null || sub === void 0 ? void 0 : sub.unsubscribe());
}, [plugin]);
if (image === undefined)
return null;
return (0, jsx_runtime_1.jsxs)("div", { className: 'pdbemolstar-state-gallery-title-box', children: [manager &&
(0, jsx_runtime_1.jsx)("div", { children: (0, jsx_runtime_1.jsx)(common_1.Button, { className: 'msp-btn-icon', title: 'Previous state', icon: icons_2.ChevronLeftSvg, onClick: () => manager.loadPrevious() }) }), (0, jsx_runtime_1.jsxs)("div", { className: 'pdbemolstar-state-gallery-title', title: imageTooltip(image, status), children: [(0, jsx_runtime_1.jsx)("div", { className: 'pdbemolstar-state-gallery-title-icon', children: (0, jsx_runtime_1.jsx)(icons_1.Icon, { svg: status === 'error' ? icons_1.ErrorSvg : status === 'loading' ? icons_2.HourglassBottomSvg : icons_2.EmptyIconSvg }) }), (0, jsx_runtime_1.jsxs)("div", { className: 'pdbemolstar-state-gallery-title-text', children: [(_a = image.title) !== null && _a !== void 0 ? _a : image.filename, (0, jsx_runtime_1.jsx)("br", {}), (0, jsx_runtime_1.jsx)("small", { children: image.subtitle })] })] }), manager &&
(0, jsx_runtime_1.jsx)("div", { children: (0, jsx_runtime_1.jsx)(common_1.Button, { className: 'msp-btn-icon', title: 'Next state', icon: icons_2.ChevronRightSvg, onClick: () => manager === null || manager === void 0 ? void 0 : manager.loadNext() }) })] });
}
/** Return tooltip text for an image */
function imageTooltip(img, status) {
var _a;
const tooltip = (status === 'error' ? '[Failed to load] \n' : status === 'loading' ? '[Loading] \n' : '')
+ ((_a = img.title) !== null && _a !== void 0 ? _a : img.filename)
+ (img.subtitle ? `: ${img.subtitle}` : '');
return tooltip;
}