diff --git a/CHANGELOG.md b/CHANGELOG.md index 235642fa915b3a74fe6ccd501b7d7559eeebfc6f..d64713c6656bc017b5dc910d690fec2be1753fde 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ Note that since we don't clearly distinguish between a public and private interf - Fix false positives in Model.isFromPdbArchive - Add drag and drop support for loading any file, including multiple at once - If there are session files (.molx or .molj) among the dropped files, only the first session will be loaded +- Add drag and drop overlay +- Safari 15.1 - 15.3 WebGL 2 support workaround ## [v3.0.0-dev.3] - 2021-12-4 diff --git a/src/mol-plugin-ui/hooks/use-behavior.ts b/src/mol-plugin-ui/hooks/use-behavior.ts index 7ba6aa432475cf3e15c2e85aa099b5a48c99e44a..8b57207f7c7b2ead44f28323056cb76f236e4573 100644 --- a/src/mol-plugin-ui/hooks/use-behavior.ts +++ b/src/mol-plugin-ui/hooks/use-behavior.ts @@ -1,10 +1,10 @@ /** - * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2020-21 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author David Sehnal <david.sehnal@gmail.com> */ -import { useEffect, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; interface Behavior<T> { value: T; @@ -16,26 +16,20 @@ export function useBehavior<T>(s: Behavior<T>): T; export function useBehavior<T>(s: Behavior<T> | undefined): T | undefined; // eslint-disable-next-line export function useBehavior<T>(s: Behavior<T> | undefined): T | undefined { - const [value, setValue] = useState(s?.value); + const [, next] = useState({}); + const current = useRef<T>(); + current.current = s?.value; useEffect(() => { if (!s) { - if (value !== void 0) setValue(void 0); return; } - let fst = true; const sub = s.subscribe((v) => { - if (fst) { - fst = false; - if (v !== value) setValue(v); - } else setValue(v); + if (current.current !== v) next({}); }); - return () => { - sub.unsubscribe(); - }; - // eslint-disable-next-line + return () => sub.unsubscribe(); }, [s]); - return value; + return s?.value; } \ No newline at end of file diff --git a/src/mol-plugin-ui/plugin.tsx b/src/mol-plugin-ui/plugin.tsx index f8835c46b3ee3bd36543db00f98fa6151787e66b..72c4602e32887e701f814aa3715949eb504e09d3 100644 --- a/src/mol-plugin-ui/plugin.tsx +++ b/src/mol-plugin-ui/plugin.tsx @@ -1,5 +1,5 @@ /** - * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author David Sehnal <david.sehnal@gmail.com> * @author Alexander Rose <alexander.rose@weirdbyte.de> @@ -20,6 +20,8 @@ import { PluginCommands } from '../mol-plugin/commands'; import { PluginUIContext } from './context'; import { OpenFiles } from '../mol-plugin-state/actions/file'; import { Asset } from '../mol-util/assets'; +import { BehaviorSubject } from 'rxjs'; +import { useBehavior } from './hooks/use-behavior'; export class Plugin extends React.Component<{ plugin: PluginUIContext }, {}> { region(kind: 'left' | 'right' | 'bottom' | 'main', element: JSX.Element) { @@ -139,13 +141,16 @@ class Layout extends PluginUIComponent { ev.preventDefault(); } + private showDragOverlay = new BehaviorSubject(false); + onDragEnter = () => this.showDragOverlay.next(true); + render() { const layout = this.plugin.layout.state; const controls = this.plugin.spec.components?.controls || {}; const viewport = this.plugin.spec.components?.viewport?.view || DefaultViewport; - return <div className='msp-plugin' onDrop={this.onDrop} onDragOver={this.onDragOver}> - <div className={this.layoutClassName}> + return <div className='msp-plugin'> + <div className={this.layoutClassName} onDragEnter={this.onDragEnter}> <div className={this.layoutVisibilityClassName}> {this.region('main', viewport)} {layout.showControls && controls.top !== 'none' && this.region('top', controls.top || SequenceView)} @@ -154,11 +159,69 @@ class Layout extends PluginUIComponent { {layout.showControls && controls.bottom !== 'none' && this.region('bottom', controls.bottom || Log)} </div> {!this.plugin.spec.components?.hideTaskOverlay && <OverlayTaskProgress />} + <DragOverlay plugin={this.plugin} showDragOverlay={this.showDragOverlay} /> </div> </div>; } } +function dropFiles(ev: React.DragEvent<HTMLDivElement>, plugin: PluginUIContext, showDragOverlay: BehaviorSubject<boolean>) { + ev.preventDefault(); + ev.stopPropagation(); + showDragOverlay.next(false); + + const files: File[] = []; + if (ev.dataTransfer.items) { + // Use DataTransferItemList interface to access the file(s) + for (let i = 0; i < ev.dataTransfer.items.length; i++) { + if (ev.dataTransfer.items[i].kind !== 'file') continue; + const file = ev.dataTransfer.items[i].getAsFile(); + if (file) files.push(file); + } + } else { + for (let i = 0; i < ev.dataTransfer.files.length; i++) { + const file = ev.dataTransfer.files[0]; + if (file) files.push(file); + } + } + + const sessions = files.filter(f => { + const fn = f.name.toLowerCase(); + return fn.endsWith('.molx') || fn.endsWith('.molj'); + }); + + if (sessions.length > 0) { + PluginCommands.State.Snapshots.OpenFile(plugin, { file: sessions[0] }); + } else { + plugin.runTask(plugin.state.data.applyAction(OpenFiles, { + files: files.map(f => Asset.File(f)), + format: { name: 'auto', params: {} }, + visuals: true + })); + } +} + +function DragOverlay({ plugin, showDragOverlay }: { plugin: PluginUIContext, showDragOverlay: BehaviorSubject<boolean> }) { + const show = useBehavior(showDragOverlay); + + const preventDrag = (e: React.DragEvent) => { + e.dataTransfer.dropEffect = 'copy'; + e.preventDefault(); + e.stopPropagation(); + }; + + return <div + className='msp-drag-drop-overlay' + style={{ display: show ? 'flex' : 'none' }} + onDragEnter={preventDrag} + onDragOver={preventDrag} + onDragLeave={() => showDragOverlay.next(false)} + onDrop={e => dropFiles(e, plugin, showDragOverlay)} + > + Upload File(s) + </div>; +} + export class ControlsWrapper extends PluginUIComponent { render() { const StructureTools = this.plugin.spec.components?.structureTools || DefaultStructureTools; diff --git a/src/mol-plugin-ui/skin/base/components/misc.scss b/src/mol-plugin-ui/skin/base/components/misc.scss index 8f29d8ad18bd19ea1bbabddeabacf50956fabf8e..72bfea0bf4ec814290f24f41cb56b19bbe53c9f0 100644 --- a/src/mol-plugin-ui/skin/base/components/misc.scss +++ b/src/mol-plugin-ui/skin/base/components/misc.scss @@ -626,4 +626,19 @@ .msp-list-unstyled { padding-left: 0; list-style: none; +} + +.msp-drag-drop-overlay { + border: 12px dashed $font-color; + background: rgba(0, 0, 0, 0.36); + display: flex; + align-items: center; + justify-content: center; + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 0; + font-size: 48px; + font-weight: bold; } \ No newline at end of file