Explorar o código

drag and drop overlay

dsehnal %!s(int64=3) %!d(string=hai) anos
pai
achega
3cd3afb775

+ 2 - 0
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
 

+ 8 - 14
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;
 }

+ 66 - 3
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;

+ 15 - 0
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;
 }