[gnome-shell] workspace: Split WindowPreview into a separate file



commit e4cbe5126a1c071bb9dad2be579f3e3f1d34acb7
Author: Jonas Dreßler <verdre v0yd nl>
Date:   Mon Jun 15 23:03:06 2020 +0200

    workspace: Split WindowPreview into a separate file
    
    The workspace.js file is quite large and is a bit confusing when it
    comes to the term "window" in there, because it can either refer to a
    WindowPreview of a complete window or to an individual window like an
    attached dialog.
    
    So try to avoid that confusion and split the new WindowPreview class and
    its WindowPreviewLayout layout manager out into a new windowPreview.js
    file.
    
    https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/1307

 js/js-resources.gresource.xml |   1 +
 js/ui/windowPreview.js        | 736 ++++++++++++++++++++++++++++++++++++++++++
 js/ui/workspace.js            | 733 +----------------------------------------
 3 files changed, 739 insertions(+), 731 deletions(-)
---
diff --git a/js/js-resources.gresource.xml b/js/js-resources.gresource.xml
index 6281eff7b2..c0a77eddc3 100644
--- a/js/js-resources.gresource.xml
+++ b/js/js-resources.gresource.xml
@@ -108,6 +108,7 @@
     <file>ui/windowAttentionHandler.js</file>
     <file>ui/windowMenu.js</file>
     <file>ui/windowManager.js</file>
+    <file>ui/windowPreview.js</file>
     <file>ui/workspace.js</file>
     <file>ui/workspaceSwitcherPopup.js</file>
     <file>ui/workspaceThumbnail.js</file>
diff --git a/js/ui/windowPreview.js b/js/ui/windowPreview.js
new file mode 100644
index 0000000000..842a4c5155
--- /dev/null
+++ b/js/ui/windowPreview.js
@@ -0,0 +1,736 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+/* exported WindowPreview */
+
+const { Atk, Clutter, GLib, GObject,
+        Graphene, Meta, Pango, Shell, St } = imports.gi;
+
+const DND = imports.ui.dnd;
+
+var WINDOW_DND_SIZE = 256;
+
+var WINDOW_OVERLAY_IDLE_HIDE_TIMEOUT = 750;
+var WINDOW_OVERLAY_FADE_TIME = 200;
+
+var DRAGGING_WINDOW_OPACITY = 100;
+
+var WindowPreviewLayout = GObject.registerClass({
+    Properties: {
+        'bounding-box': GObject.ParamSpec.boxed(
+            'bounding-box', 'Bounding box', 'Bounding box',
+            GObject.ParamFlags.READABLE,
+            Clutter.ActorBox.$gtype),
+    },
+}, class WindowPreviewLayout extends Clutter.LayoutManager {
+    _init() {
+        super._init();
+
+        this._container = null;
+        this._boundingBox = new Clutter.ActorBox();
+        this._windows = new Map();
+    }
+
+    _layoutChanged() {
+        let frameRect;
+
+        for (const windowInfo of this._windows.values()) {
+            const frame = windowInfo.metaWindow.get_frame_rect();
+            frameRect = frameRect ? frameRect.union(frame) : frame;
+        }
+
+        if (!frameRect)
+            frameRect = new Meta.Rectangle();
+
+        const oldBox = this._boundingBox.copy();
+        this._boundingBox.set_origin(frameRect.x, frameRect.y);
+        this._boundingBox.set_size(frameRect.width, frameRect.height);
+
+        if (!this._boundingBox.equal(oldBox))
+            this.notify('bounding-box');
+
+        // Always call layout_changed(), a size or position change of an
+        // attached dialog might not affect the boundingBox
+        this.layout_changed();
+    }
+
+    vfunc_set_container(container) {
+        this._container = container;
+    }
+
+    vfunc_get_preferred_height(_container, _forWidth) {
+        return [0, this._boundingBox.get_height()];
+    }
+
+    vfunc_get_preferred_width(_container, _forHeight) {
+        return [0, this._boundingBox.get_width()];
+    }
+
+    vfunc_allocate(container, box) {
+        // If the scale isn't 1, we weren't allocated our preferred size
+        // and have to scale the children allocations accordingly.
+        const scaleX = box.get_width() / this._boundingBox.get_width();
+        const scaleY = box.get_height() / this._boundingBox.get_height();
+
+        const childBox = new Clutter.ActorBox();
+
+        for (const child of container) {
+            if (!child.visible)
+                continue;
+
+            const windowInfo = this._windows.get(child);
+            if (windowInfo) {
+                const bufferRect = windowInfo.metaWindow.get_buffer_rect();
+                childBox.set_origin(
+                    bufferRect.x - this._boundingBox.x1,
+                    bufferRect.y - this._boundingBox.y1);
+
+                const [, , natWidth, natHeight] = child.get_preferred_size();
+                childBox.set_size(natWidth, natHeight);
+
+                childBox.x1 *= scaleX;
+                childBox.x2 *= scaleX;
+                childBox.y1 *= scaleY;
+                childBox.y2 *= scaleY;
+
+                child.allocate(childBox);
+            } else {
+                child.allocate_preferred_size();
+            }
+        }
+    }
+
+    /**
+     * addWindow:
+     * @param {Meta.Window} window: the MetaWindow instance
+     *
+     * Creates a ClutterActor drawing the texture of @window and adds it
+     * to the container. If @window is already part of the preview, this
+     * function will do nothing.
+     *
+     * @returns {Clutter.Actor} The newly created actor drawing @window
+     */
+    addWindow(window) {
+        const index = [...this._windows.values()].findIndex(info =>
+            info.metaWindow === window);
+
+        if (index !== -1)
+            return null;
+
+        const windowActor = window.get_compositor_private();
+        const actor = new Clutter.Clone({ source: windowActor });
+
+        this._windows.set(actor, {
+            metaWindow: window,
+            windowActor,
+            sizeChangedId: window.connect('size-changed', () =>
+                this._layoutChanged()),
+            positionChangedId: window.connect('position-changed', () =>
+                this._layoutChanged()),
+            windowActorDestroyId: windowActor.connect('destroy', () =>
+                actor.destroy()),
+            destroyId: actor.connect('destroy', () =>
+                this.removeWindow(window)),
+        });
+
+        this._container.add_child(actor);
+
+        this._layoutChanged();
+
+        return actor;
+    }
+
+    /**
+     * removeWindow:
+     * @param {Meta.Window} window: the window to remove from the preview
+     *
+     * Removes a MetaWindow @window from the preview which has been added
+     * previously using addWindow(). If @window is not part of preview,
+     * this function will do nothing.
+     */
+    removeWindow(window) {
+        const entry = [...this._windows].find(
+            ([, i]) => i.metaWindow === window);
+
+        if (!entry)
+            return;
+
+        const [actor, windowInfo] = entry;
+
+        windowInfo.metaWindow.disconnect(windowInfo.sizeChangedId);
+        windowInfo.metaWindow.disconnect(windowInfo.positionChangedId);
+        windowInfo.windowActor.disconnect(windowInfo.windowActorDestroyId);
+        actor.disconnect(windowInfo.destroyId);
+
+        this._windows.delete(actor);
+        this._container.remove_child(actor);
+
+        this._layoutChanged();
+    }
+
+    /**
+     * getWindows:
+     *
+     * Gets an array of all MetaWindows that were added to the layout
+     * using addWindow(), ordered by the insertion order.
+     *
+     * @returns {Array} An array including all windows
+     */
+    getWindows() {
+        return [...this._windows.values()].map(i => i.metaWindow);
+    }
+
+    // eslint-disable-next-line camelcase
+    get bounding_box() {
+        return this._boundingBox;
+    }
+});
+
+var WindowPreview = GObject.registerClass({
+    Signals: {
+        'drag-begin': {},
+        'drag-cancelled': {},
+        'drag-end': {},
+        'selected': { param_types: [GObject.TYPE_UINT] },
+        'show-chrome': {},
+        'size-changed': {},
+    },
+}, class WindowPreview extends St.Widget {
+    _init(metaWindow, workspace) {
+        this.metaWindow = metaWindow;
+        this.metaWindow._delegate = this;
+        this._windowActor = metaWindow.get_compositor_private();
+        this._workspace = workspace;
+
+        super._init({
+            reactive: true,
+            can_focus: true,
+            accessible_role: Atk.Role.PUSH_BUTTON,
+            offscreen_redirect: Clutter.OffscreenRedirect.AUTOMATIC_FOR_OPACITY,
+        });
+
+        this._windowContainer = new Clutter.Actor();
+        // gjs currently can't handle setting an actors layout manager during
+        // the initialization of the actor if that layout manager keeps track
+        // of its container, so set the layout manager after creating the
+        // container
+        this._windowContainer.layout_manager = new WindowPreviewLayout();
+        this.add_child(this._windowContainer);
+
+        this._addWindow(metaWindow);
+
+        this._delegate = this;
+
+        this.slotId = 0;
+        this._stackAbove = null;
+
+        this._windowContainer.layout_manager.connect(
+            'notify::bounding-box', layout => {
+                // A bounding box of 0x0 means all windows were removed
+                if (layout.bounding_box.get_area() > 0)
+                    this.emit('size-changed');
+            });
+
+        this._windowDestroyId =
+            this._windowActor.connect('destroy', () => this.destroy());
+
+        this._updateAttachedDialogs();
+        this.x = this.boundingBox.x;
+        this.y = this.boundingBox.y;
+
+        let clickAction = new Clutter.ClickAction();
+        clickAction.connect('clicked', () => this._activate());
+        clickAction.connect('long-press', this._onLongPress.bind(this));
+        this.add_action(clickAction);
+        this.connect('destroy', this._onDestroy.bind(this));
+
+        this._draggable = DND.makeDraggable(this,
+                                            { restoreOnSuccess: true,
+                                              manualMode: true,
+                                              dragActorMaxSize: WINDOW_DND_SIZE,
+                                              dragActorOpacity: DRAGGING_WINDOW_OPACITY });
+        this._draggable.connect('drag-begin', this._onDragBegin.bind(this));
+        this._draggable.connect('drag-cancelled', this._onDragCancelled.bind(this));
+        this._draggable.connect('drag-end', this._onDragEnd.bind(this));
+        this.inDrag = false;
+
+        this._selected = false;
+        this._closeRequested = false;
+        this._idleHideOverlayId = 0;
+
+        this._border = new St.Widget({
+            visible: false,
+            style_class: 'window-clone-border',
+        });
+        this._borderConstraint = new Clutter.BindConstraint({
+            source: this._windowContainer,
+            coordinate: Clutter.BindCoordinate.SIZE,
+        });
+        this._border.add_constraint(this._borderConstraint);
+        this._border.add_constraint(new Clutter.AlignConstraint({
+            source: this._windowContainer,
+            align_axis: Clutter.AlignAxis.BOTH,
+            factor: 0.5,
+        }));
+        this._borderCenter = new Clutter.Actor();
+        this._border.bind_property('visible', this._borderCenter, 'visible',
+            GObject.BindingFlags.SYNC_CREATE);
+        this._borderCenterConstraint = new Clutter.BindConstraint({
+            source: this._windowContainer,
+            coordinate: Clutter.BindCoordinate.SIZE,
+        });
+        this._borderCenter.add_constraint(this._borderCenterConstraint);
+        this._borderCenter.add_constraint(new Clutter.AlignConstraint({
+            source: this._windowContainer,
+            align_axis: Clutter.AlignAxis.BOTH,
+            factor: 0.5,
+        }));
+        this._border.connect('style-changed',
+            this._onBorderStyleChanged.bind(this));
+
+        this._title = new St.Label({
+            visible: false,
+            style_class: 'window-caption',
+            text: this._getCaption(),
+            reactive: true,
+        });
+        this._title.add_constraint(new Clutter.BindConstraint({
+            source: this._borderCenter,
+            coordinate: Clutter.BindCoordinate.POSITION,
+        }));
+        this._title.add_constraint(new Clutter.AlignConstraint({
+            source: this._borderCenter,
+            align_axis: Clutter.AlignAxis.X_AXIS,
+            factor: 0.5,
+        }));
+        this._title.add_constraint(new Clutter.AlignConstraint({
+            source: this._borderCenter,
+            align_axis: Clutter.AlignAxis.Y_AXIS,
+            pivot_point: new Graphene.Point({ x: -1, y: 0.5 }),
+            factor: 1,
+        }));
+        this._title.clutter_text.ellipsize = Pango.EllipsizeMode.END;
+        this.label_actor = this._title;
+        this._updateCaptionId = this.metaWindow.connect('notify::title', () => {
+            this._title.text = this._getCaption();
+        });
+
+        const layout = Meta.prefs_get_button_layout();
+        this._closeButtonSide =
+            layout.left_buttons.includes(Meta.ButtonFunction.CLOSE)
+                ? St.Side.LEFT : St.Side.RIGHT;
+
+        this._closeButton = new St.Button({
+            visible: false,
+            style_class: 'window-close',
+            child: new St.Icon({ icon_name: 'window-close-symbolic' }),
+        });
+        this._closeButton.add_constraint(new Clutter.BindConstraint({
+            source: this._borderCenter,
+            coordinate: Clutter.BindCoordinate.POSITION,
+        }));
+        this._closeButton.add_constraint(new Clutter.AlignConstraint({
+            source: this._borderCenter,
+            align_axis: Clutter.AlignAxis.X_AXIS,
+            pivot_point: new Graphene.Point({ x: 0.5, y: -1 }),
+            factor: this._closeButtonSide === St.Side.LEFT ? 0 : 1,
+        }));
+        this._closeButton.add_constraint(new Clutter.AlignConstraint({
+            source: this._borderCenter,
+            align_axis: Clutter.AlignAxis.Y_AXIS,
+            pivot_point: new Graphene.Point({ x: -1, y: 0.5 }),
+            factor: 0,
+        }));
+        this._closeButton.connect('clicked', () => this._deleteAll());
+
+        this.add_child(this._borderCenter);
+        this.add_child(this._border);
+        this.add_child(this._title);
+        this.add_child(this._closeButton);
+    }
+
+    vfunc_get_preferred_width(forHeight) {
+        const themeNode = this.get_theme_node();
+
+        // Only include window previews in size request, not chrome
+        const [minWidth, natWidth] =
+            this._windowContainer.get_preferred_width(
+                themeNode.adjust_for_height(forHeight));
+
+        return themeNode.adjust_preferred_width(minWidth, natWidth);
+    }
+
+    vfunc_get_preferred_height(forWidth) {
+        const themeNode = this.get_theme_node();
+        const [minHeight, natHeight] =
+            this._windowContainer.get_preferred_height(
+                themeNode.adjust_for_width(forWidth));
+
+        return themeNode.adjust_preferred_height(minHeight, natHeight);
+    }
+
+    vfunc_allocate(box) {
+        this.set_allocation(box);
+
+        for (const child of this)
+            child.allocate_available_size(0, 0, box.get_width(), box.get_height());
+    }
+
+    _onBorderStyleChanged() {
+        let borderNode = this._border.get_theme_node();
+        this._borderSize = borderNode.get_border_width(St.Side.TOP);
+
+        // Increase the size of the border actor so the border outlines
+        // the bounding box
+        this._borderConstraint.offset = this._borderSize * 2;
+        this._borderCenterConstraint.offset = this._borderSize;
+    }
+
+    _windowCanClose() {
+        return this.metaWindow.can_close() &&
+               !this._hasAttachedDialogs();
+    }
+
+    _getCaption() {
+        if (this.metaWindow.title)
+            return this.metaWindow.title;
+
+        let tracker = Shell.WindowTracker.get_default();
+        let app = tracker.get_window_app(this.metaWindow);
+        return app.get_name();
+    }
+
+    chromeHeights() {
+        this._border.ensure_style();
+        this._title.ensure_style();
+        const [, closeButtonHeight] = this._closeButton.get_preferred_height(-1);
+        const [, titleHeight] = this._title.get_preferred_height(-1);
+
+        const topOversize = (this._borderSize / 2) + (closeButtonHeight / 2);
+        const bottomOversize = Math.max(
+            this._borderSize,
+            (titleHeight / 2) + (this._borderSize / 2));
+
+        return [topOversize, bottomOversize];
+    }
+
+    chromeWidths() {
+        this._border.ensure_style();
+        const [, closeButtonWidth] = this._closeButton.get_preferred_width(-1);
+
+        const leftOversize = this._closeButtonSide === St.Side.LEFT
+            ? (this._borderSize / 2) + (closeButtonWidth / 2)
+            : this._borderSize;
+        const rightOversize = this._closeButtonSide === St.Side.LEFT
+            ? this._borderSize
+            : (this._borderSize / 2) + (closeButtonWidth / 2);
+
+        return [leftOversize, rightOversize];
+    }
+
+    showOverlay(animate) {
+        const ongoingTransition = this._border.get_transition('opacity');
+
+        // Don't do anything if we're fully visible already
+        if (this._border.visible && !ongoingTransition)
+            return;
+
+        // If we're supposed to animate and an animation in our direction
+        // is already happening, let that one continue
+        if (animate &&
+            ongoingTransition &&
+            ongoingTransition.get_interval().peek_final_value() === 255)
+            return;
+
+        const toShow = this._windowCanClose()
+            ? [this._border, this._title, this._closeButton]
+            : [this._border, this._title];
+
+        toShow.forEach(a => {
+            a.opacity = 0;
+            a.show();
+            a.ease({
+                opacity: 255,
+                duration: animate ? WINDOW_OVERLAY_FADE_TIME : 0,
+                mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+            });
+        });
+
+        this.emit('show-chrome');
+    }
+
+    hideOverlay(animate) {
+        const ongoingTransition = this._border.get_transition('opacity');
+
+        // Don't do anything if we're fully hidden already
+        if (!this._border.visible && !ongoingTransition)
+            return;
+
+        // If we're supposed to animate and an animation in our direction
+        // is already happening, let that one continue
+        if (animate &&
+            ongoingTransition &&
+            ongoingTransition.get_interval().peek_final_value() === 0)
+            return;
+
+        [this._border, this._title, this._closeButton].forEach(a => {
+            a.opacity = 255;
+            a.ease({
+                opacity: 0,
+                duration: animate ? WINDOW_OVERLAY_FADE_TIME : 0,
+                mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+                onComplete: () => a.hide(),
+            });
+        });
+    }
+
+    _addWindow(metaWindow) {
+        const clone = this._windowContainer.layout_manager.addWindow(metaWindow);
+
+        // We expect this to be used for all interaction rather than
+        // the ClutterClone; as the former is reactive and the latter
+        // is not, this just works for most cases. However, for DND all
+        // actors are picked, so DND operations would operate on the clone.
+        // To avoid this, we hide it from pick.
+        Shell.util_set_hidden_from_pick(clone, true);
+    }
+
+    vfunc_has_overlaps() {
+        return this._hasAttachedDialogs();
+    }
+
+    _deleteAll() {
+        const windows = this._windowContainer.layout_manager.getWindows();
+
+        // Delete all windows, starting from the bottom-most (most-modal) one
+        for (const window of windows.reverse())
+            window.delete(global.get_current_time());
+
+        this._closeRequested = true;
+    }
+
+    addDialog(win) {
+        let parent = win.get_transient_for();
+        while (parent.is_attached_dialog())
+            parent = parent.get_transient_for();
+
+        // Display dialog if it is attached to our metaWindow
+        if (win.is_attached_dialog() && parent == this.metaWindow)
+            this._addWindow(win);
+
+        // The dialog popped up after the user tried to close the window,
+        // assume it's a close confirmation and leave the overview
+        if (this._closeRequested)
+            this._activate();
+    }
+
+    _hasAttachedDialogs() {
+        return this._windowContainer.layout_manager.getWindows().length > 1;
+    }
+
+    _updateAttachedDialogs() {
+        let iter = win => {
+            let actor = win.get_compositor_private();
+
+            if (!actor)
+                return false;
+            if (!win.is_attached_dialog())
+                return false;
+
+            this._addWindow(win);
+            win.foreach_transient(iter);
+            return true;
+        };
+        this.metaWindow.foreach_transient(iter);
+    }
+
+    get boundingBox() {
+        const box = this._windowContainer.layout_manager.bounding_box;
+
+        return {
+            x: box.x1,
+            y: box.y1,
+            width: box.get_width(),
+            height: box.get_height(),
+        };
+    }
+
+    get windowCenter() {
+        const box = this._windowContainer.layout_manager.bounding_box;
+
+        return new Graphene.Point({
+            x: box.get_x() + box.get_width() / 2,
+            y: box.get_y() + box.get_height() / 2,
+        });
+    }
+
+    // Find the actor just below us, respecting reparenting done by DND code
+    _getActualStackAbove() {
+        if (this._stackAbove == null)
+            return null;
+
+        if (this.inDrag) {
+            if (this._stackAbove._delegate)
+                return this._stackAbove._delegate._getActualStackAbove();
+            else
+                return null;
+        } else {
+            return this._stackAbove;
+        }
+    }
+
+    setStackAbove(actor) {
+        this._stackAbove = actor;
+        if (this.inDrag)
+            // We'll fix up the stack after the drag
+            return;
+
+        let parent = this.get_parent();
+        let actualAbove = this._getActualStackAbove();
+        if (actualAbove == null)
+            parent.set_child_below_sibling(this, null);
+        else
+            parent.set_child_above_sibling(this, actualAbove);
+    }
+
+    _onDestroy() {
+        this._windowActor.disconnect(this._windowDestroyId);
+
+        this.metaWindow._delegate = null;
+        this._delegate = null;
+
+        this.metaWindow.disconnect(this._updateCaptionId);
+
+        if (this._longPressLater) {
+            Meta.later_remove(this._longPressLater);
+            delete this._longPressLater;
+        }
+
+        if (this._idleHideOverlayId > 0) {
+            GLib.source_remove(this._idleHideOverlayId);
+            this._idleHideOverlayId = 0;
+        }
+
+        if (this.inDrag) {
+            this.emit('drag-end');
+            this.inDrag = false;
+        }
+    }
+
+    _activate() {
+        this._selected = true;
+        this.emit('selected', global.get_current_time());
+    }
+
+    vfunc_enter_event(crossingEvent) {
+        this.showOverlay(true);
+        return super.vfunc_enter_event(crossingEvent);
+    }
+
+    vfunc_leave_event(crossingEvent) {
+        if (this._idleHideOverlayId > 0)
+            GLib.source_remove(this._idleHideOverlayId);
+
+        this._idleHideOverlayId = GLib.timeout_add(
+            GLib.PRIORITY_DEFAULT,
+            WINDOW_OVERLAY_IDLE_HIDE_TIMEOUT, () => {
+                if (this._closeButton['has-pointer'] ||
+                    this._title['has-pointer'])
+                    return GLib.SOURCE_CONTINUE;
+
+                if (!this['has-pointer'])
+                    this.hideOverlay(true);
+
+                this._idleHideOverlayId = 0;
+                return GLib.SOURCE_REMOVE;
+            });
+
+        GLib.Source.set_name_by_id(this._idleHideOverlayId, '[gnome-shell] this._idleHideOverlayId');
+
+        return super.vfunc_leave_event(crossingEvent);
+    }
+
+    vfunc_key_focus_in() {
+        super.vfunc_key_focus_in();
+        this.showOverlay(true);
+    }
+
+    vfunc_key_focus_out() {
+        super.vfunc_key_focus_out();
+        this.hideOverlay(true);
+    }
+
+    vfunc_key_press_event(keyEvent) {
+        let symbol = keyEvent.keyval;
+        let isEnter = symbol == Clutter.KEY_Return || symbol == Clutter.KEY_KP_Enter;
+        if (isEnter) {
+            this._activate();
+            return true;
+        }
+
+        return super.vfunc_key_press_event(keyEvent);
+    }
+
+    _onLongPress(action, actor, state) {
+        // Take advantage of the Clutter policy to consider
+        // a long-press canceled when the pointer movement
+        // exceeds dnd-drag-threshold to manually start the drag
+        if (state == Clutter.LongPressState.CANCEL) {
+            let event = Clutter.get_current_event();
+            this._dragTouchSequence = event.get_event_sequence();
+
+            if (this._longPressLater)
+                return true;
+
+            // A click cancels a long-press before any click handler is
+            // run - make sure to not start a drag in that case
+            this._longPressLater = Meta.later_add(Meta.LaterType.BEFORE_REDRAW, () => {
+                delete this._longPressLater;
+                if (this._selected)
+                    return;
+                let [x, y] = action.get_coords();
+                action.release();
+                this._draggable.startDrag(x, y, global.get_current_time(), this._dragTouchSequence, 
event.get_device());
+            });
+        } else {
+            this.showOverlay(true);
+        }
+        return true;
+    }
+
+    _onDragBegin(_draggable, _time) {
+        this.inDrag = true;
+        this.hideOverlay(false);
+        this.emit('drag-begin');
+    }
+
+    handleDragOver(source, actor, x, y, time) {
+        return this._workspace.handleDragOver(source, actor, x, y, time);
+    }
+
+    acceptDrop(source, actor, x, y, time) {
+        return this._workspace.acceptDrop(source, actor, x, y, time);
+    }
+
+    _onDragCancelled(_draggable, _time) {
+        this.emit('drag-cancelled');
+    }
+
+    _onDragEnd(_draggable, _time, _snapback) {
+        this.inDrag = false;
+
+        // We may not have a parent if DnD completed successfully, in
+        // which case our clone will shortly be destroyed and replaced
+        // with a new one on the target workspace.
+        let parent = this.get_parent();
+        if (parent !== null) {
+            if (this._stackAbove == null)
+                parent.set_child_below_sibling(this, null);
+            else
+                parent.set_child_above_sibling(this, this._stackAbove);
+        }
+
+        if (this['has-pointer'])
+            this.showOverlay(true);
+
+        this.emit('drag-end');
+    }
+});
diff --git a/js/ui/workspace.js b/js/ui/workspace.js
index 6934148128..e28f67e61b 100644
--- a/js/ui/workspace.js
+++ b/js/ui/workspace.js
@@ -1,24 +1,17 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 /* exported Workspace */
 
-const { Atk, Clutter, GLib, GObject,
-        Graphene, Meta, Pango, Shell, St } = imports.gi;
+const { Clutter, GLib, GObject, Meta, St } = imports.gi;
 
 const DND = imports.ui.dnd;
 const Main = imports.ui.main;
 const Overview = imports.ui.overview;
-
-var WINDOW_DND_SIZE = 256;
+const { WindowPreview } = imports.ui.windowPreview;
 
 var WINDOW_PREVIEW_MAXIMUM_SCALE = 1.0;
 
-var WINDOW_OVERLAY_IDLE_HIDE_TIMEOUT = 750;
-var WINDOW_OVERLAY_FADE_TIME = 200;
-
 var WINDOW_REPOSITIONING_DELAY = 750;
 
-var DRAGGING_WINDOW_OPACITY = 100;
-
 // When calculating a layout, we calculate the scale of windows and the percent
 // of the available area the new layout uses. If the values for the new layout,
 // when weighted with the values as below, are worse than the previous layout's,
@@ -33,728 +26,6 @@ function _interpolate(start, end, step) {
     return start + (end - start) * step;
 }
 
-var WindowPreviewLayout = GObject.registerClass({
-    Properties: {
-        'bounding-box': GObject.ParamSpec.boxed(
-            'bounding-box', 'Bounding box', 'Bounding box',
-            GObject.ParamFlags.READABLE,
-            Clutter.ActorBox.$gtype),
-    },
-}, class WindowPreviewLayout extends Clutter.LayoutManager {
-    _init() {
-        super._init();
-
-        this._container = null;
-        this._boundingBox = new Clutter.ActorBox();
-        this._windows = new Map();
-    }
-
-    _layoutChanged() {
-        let frameRect;
-
-        for (const windowInfo of this._windows.values()) {
-            const frame = windowInfo.metaWindow.get_frame_rect();
-            frameRect = frameRect ? frameRect.union(frame) : frame;
-        }
-
-        if (!frameRect)
-            frameRect = new Meta.Rectangle();
-
-        const oldBox = this._boundingBox.copy();
-        this._boundingBox.set_origin(frameRect.x, frameRect.y);
-        this._boundingBox.set_size(frameRect.width, frameRect.height);
-
-        if (!this._boundingBox.equal(oldBox))
-            this.notify('bounding-box');
-
-        // Always call layout_changed(), a size or position change of an
-        // attached dialog might not affect the boundingBox
-        this.layout_changed();
-    }
-
-    vfunc_set_container(container) {
-        this._container = container;
-    }
-
-    vfunc_get_preferred_height(_container, _forWidth) {
-        return [0, this._boundingBox.get_height()];
-    }
-
-    vfunc_get_preferred_width(_container, _forHeight) {
-        return [0, this._boundingBox.get_width()];
-    }
-
-    vfunc_allocate(container, box) {
-        // If the scale isn't 1, we weren't allocated our preferred size
-        // and have to scale the children allocations accordingly.
-        const scaleX = box.get_width() / this._boundingBox.get_width();
-        const scaleY = box.get_height() / this._boundingBox.get_height();
-
-        const childBox = new Clutter.ActorBox();
-
-        for (const child of container) {
-            if (!child.visible)
-                continue;
-
-            const windowInfo = this._windows.get(child);
-            if (windowInfo) {
-                const bufferRect = windowInfo.metaWindow.get_buffer_rect();
-                childBox.set_origin(
-                    bufferRect.x - this._boundingBox.x1,
-                    bufferRect.y - this._boundingBox.y1);
-
-                const [, , natWidth, natHeight] = child.get_preferred_size();
-                childBox.set_size(natWidth, natHeight);
-
-                childBox.x1 *= scaleX;
-                childBox.x2 *= scaleX;
-                childBox.y1 *= scaleY;
-                childBox.y2 *= scaleY;
-
-                child.allocate(childBox);
-            } else {
-                child.allocate_preferred_size();
-            }
-        }
-    }
-
-    /**
-     * addWindow:
-     * @param {Meta.Window} window: the MetaWindow instance
-     *
-     * Creates a ClutterActor drawing the texture of @window and adds it
-     * to the container. If @window is already part of the preview, this
-     * function will do nothing.
-     *
-     * @returns {Clutter.Actor} The newly created actor drawing @window
-     */
-    addWindow(window) {
-        const index = [...this._windows.values()].findIndex(info =>
-            info.metaWindow === window);
-
-        if (index !== -1)
-            return null;
-
-        const windowActor = window.get_compositor_private();
-        const actor = new Clutter.Clone({ source: windowActor });
-
-        this._windows.set(actor, {
-            metaWindow: window,
-            windowActor,
-            sizeChangedId: window.connect('size-changed', () =>
-                this._layoutChanged()),
-            positionChangedId: window.connect('position-changed', () =>
-                this._layoutChanged()),
-            windowActorDestroyId: windowActor.connect('destroy', () =>
-                actor.destroy()),
-            destroyId: actor.connect('destroy', () =>
-                this.removeWindow(window)),
-        });
-
-        this._container.add_child(actor);
-
-        this._layoutChanged();
-
-        return actor;
-    }
-
-    /**
-     * removeWindow:
-     * @param {Meta.Window} window: the window to remove from the preview
-     *
-     * Removes a MetaWindow @window from the preview which has been added
-     * previously using addWindow(). If @window is not part of preview,
-     * this function will do nothing.
-     */
-    removeWindow(window) {
-        const entry = [...this._windows].find(
-            ([, i]) => i.metaWindow === window);
-
-        if (!entry)
-            return;
-
-        const [actor, windowInfo] = entry;
-
-        windowInfo.metaWindow.disconnect(windowInfo.sizeChangedId);
-        windowInfo.metaWindow.disconnect(windowInfo.positionChangedId);
-        windowInfo.windowActor.disconnect(windowInfo.windowActorDestroyId);
-        actor.disconnect(windowInfo.destroyId);
-
-        this._windows.delete(actor);
-        this._container.remove_child(actor);
-
-        this._layoutChanged();
-    }
-
-    /**
-     * getWindows:
-     *
-     * Gets an array of all MetaWindows that were added to the layout
-     * using addWindow(), ordered by the insertion order.
-     *
-     * @returns {Array} An array including all windows
-     */
-    getWindows() {
-        return [...this._windows.values()].map(i => i.metaWindow);
-    }
-
-    // eslint-disable-next-line camelcase
-    get bounding_box() {
-        return this._boundingBox;
-    }
-});
-
-var WindowPreview = GObject.registerClass({
-    Signals: {
-        'drag-begin': {},
-        'drag-cancelled': {},
-        'drag-end': {},
-        'selected': { param_types: [GObject.TYPE_UINT] },
-        'show-chrome': {},
-        'size-changed': {},
-    },
-}, class WindowPreview extends St.Widget {
-    _init(metaWindow, workspace) {
-        this.metaWindow = metaWindow;
-        this.metaWindow._delegate = this;
-        this._windowActor = metaWindow.get_compositor_private();
-        this._workspace = workspace;
-
-        super._init({
-            reactive: true,
-            can_focus: true,
-            accessible_role: Atk.Role.PUSH_BUTTON,
-            offscreen_redirect: Clutter.OffscreenRedirect.AUTOMATIC_FOR_OPACITY,
-        });
-
-        this._windowContainer = new Clutter.Actor();
-        // gjs currently can't handle setting an actors layout manager during
-        // the initialization of the actor if that layout manager keeps track
-        // of its container, so set the layout manager after creating the
-        // container
-        this._windowContainer.layout_manager = new WindowPreviewLayout();
-        this.add_child(this._windowContainer);
-
-        this._addWindow(metaWindow);
-
-        this._delegate = this;
-
-        this.slotId = 0;
-        this._stackAbove = null;
-
-        this._windowContainer.layout_manager.connect(
-            'notify::bounding-box', layout => {
-                // A bounding box of 0x0 means all windows were removed
-                if (layout.bounding_box.get_area() > 0)
-                    this.emit('size-changed');
-            });
-
-        this._windowDestroyId =
-            this._windowActor.connect('destroy', () => this.destroy());
-
-        this._updateAttachedDialogs();
-        this.x = this.boundingBox.x;
-        this.y = this.boundingBox.y;
-
-        let clickAction = new Clutter.ClickAction();
-        clickAction.connect('clicked', () => this._activate());
-        clickAction.connect('long-press', this._onLongPress.bind(this));
-        this.add_action(clickAction);
-        this.connect('destroy', this._onDestroy.bind(this));
-
-        this._draggable = DND.makeDraggable(this,
-                                            { restoreOnSuccess: true,
-                                              manualMode: true,
-                                              dragActorMaxSize: WINDOW_DND_SIZE,
-                                              dragActorOpacity: DRAGGING_WINDOW_OPACITY });
-        this._draggable.connect('drag-begin', this._onDragBegin.bind(this));
-        this._draggable.connect('drag-cancelled', this._onDragCancelled.bind(this));
-        this._draggable.connect('drag-end', this._onDragEnd.bind(this));
-        this.inDrag = false;
-
-        this._selected = false;
-        this._closeRequested = false;
-        this._idleHideOverlayId = 0;
-
-        this._border = new St.Widget({
-            visible: false,
-            style_class: 'window-clone-border',
-        });
-        this._borderConstraint = new Clutter.BindConstraint({
-            source: this._windowContainer,
-            coordinate: Clutter.BindCoordinate.SIZE,
-        });
-        this._border.add_constraint(this._borderConstraint);
-        this._border.add_constraint(new Clutter.AlignConstraint({
-            source: this._windowContainer,
-            align_axis: Clutter.AlignAxis.BOTH,
-            factor: 0.5,
-        }));
-        this._borderCenter = new Clutter.Actor();
-        this._border.bind_property('visible', this._borderCenter, 'visible',
-            GObject.BindingFlags.SYNC_CREATE);
-        this._borderCenterConstraint = new Clutter.BindConstraint({
-            source: this._windowContainer,
-            coordinate: Clutter.BindCoordinate.SIZE,
-        });
-        this._borderCenter.add_constraint(this._borderCenterConstraint);
-        this._borderCenter.add_constraint(new Clutter.AlignConstraint({
-            source: this._windowContainer,
-            align_axis: Clutter.AlignAxis.BOTH,
-            factor: 0.5,
-        }));
-        this._border.connect('style-changed',
-            this._onBorderStyleChanged.bind(this));
-
-        this._title = new St.Label({
-            visible: false,
-            style_class: 'window-caption',
-            text: this._getCaption(),
-            reactive: true,
-        });
-        this._title.add_constraint(new Clutter.BindConstraint({
-            source: this._borderCenter,
-            coordinate: Clutter.BindCoordinate.POSITION,
-        }));
-        this._title.add_constraint(new Clutter.AlignConstraint({
-            source: this._borderCenter,
-            align_axis: Clutter.AlignAxis.X_AXIS,
-            factor: 0.5,
-        }));
-        this._title.add_constraint(new Clutter.AlignConstraint({
-            source: this._borderCenter,
-            align_axis: Clutter.AlignAxis.Y_AXIS,
-            pivot_point: new Graphene.Point({ x: -1, y: 0.5 }),
-            factor: 1,
-        }));
-        this._title.clutter_text.ellipsize = Pango.EllipsizeMode.END;
-        this.label_actor = this._title;
-        this._updateCaptionId = this.metaWindow.connect('notify::title', () => {
-            this._title.text = this._getCaption();
-        });
-
-        const layout = Meta.prefs_get_button_layout();
-        this._closeButtonSide =
-            layout.left_buttons.includes(Meta.ButtonFunction.CLOSE)
-                ? St.Side.LEFT : St.Side.RIGHT;
-
-        this._closeButton = new St.Button({
-            visible: false,
-            style_class: 'window-close',
-            child: new St.Icon({ icon_name: 'window-close-symbolic' }),
-        });
-        this._closeButton.add_constraint(new Clutter.BindConstraint({
-            source: this._borderCenter,
-            coordinate: Clutter.BindCoordinate.POSITION,
-        }));
-        this._closeButton.add_constraint(new Clutter.AlignConstraint({
-            source: this._borderCenter,
-            align_axis: Clutter.AlignAxis.X_AXIS,
-            pivot_point: new Graphene.Point({ x: 0.5, y: -1 }),
-            factor: this._closeButtonSide === St.Side.LEFT ? 0 : 1,
-        }));
-        this._closeButton.add_constraint(new Clutter.AlignConstraint({
-            source: this._borderCenter,
-            align_axis: Clutter.AlignAxis.Y_AXIS,
-            pivot_point: new Graphene.Point({ x: -1, y: 0.5 }),
-            factor: 0,
-        }));
-        this._closeButton.connect('clicked', () => this._deleteAll());
-
-        this.add_child(this._borderCenter);
-        this.add_child(this._border);
-        this.add_child(this._title);
-        this.add_child(this._closeButton);
-    }
-
-    vfunc_get_preferred_width(forHeight) {
-        const themeNode = this.get_theme_node();
-
-        // Only include window previews in size request, not chrome
-        const [minWidth, natWidth] =
-            this._windowContainer.get_preferred_width(
-                themeNode.adjust_for_height(forHeight));
-
-        return themeNode.adjust_preferred_width(minWidth, natWidth);
-    }
-
-    vfunc_get_preferred_height(forWidth) {
-        const themeNode = this.get_theme_node();
-        const [minHeight, natHeight] =
-            this._windowContainer.get_preferred_height(
-                themeNode.adjust_for_width(forWidth));
-
-        return themeNode.adjust_preferred_height(minHeight, natHeight);
-    }
-
-    vfunc_allocate(box) {
-        this.set_allocation(box);
-
-        for (const child of this)
-            child.allocate_available_size(0, 0, box.get_width(), box.get_height());
-    }
-
-    _onBorderStyleChanged() {
-        let borderNode = this._border.get_theme_node();
-        this._borderSize = borderNode.get_border_width(St.Side.TOP);
-
-        // Increase the size of the border actor so the border outlines
-        // the bounding box
-        this._borderConstraint.offset = this._borderSize * 2;
-        this._borderCenterConstraint.offset = this._borderSize;
-    }
-
-    _windowCanClose() {
-        return this.metaWindow.can_close() &&
-               !this._hasAttachedDialogs();
-    }
-
-    _getCaption() {
-        if (this.metaWindow.title)
-            return this.metaWindow.title;
-
-        let tracker = Shell.WindowTracker.get_default();
-        let app = tracker.get_window_app(this.metaWindow);
-        return app.get_name();
-    }
-
-    chromeHeights() {
-        this._border.ensure_style();
-        this._title.ensure_style();
-        const [, closeButtonHeight] = this._closeButton.get_preferred_height(-1);
-        const [, titleHeight] = this._title.get_preferred_height(-1);
-
-        const topOversize = (this._borderSize / 2) + (closeButtonHeight / 2);
-        const bottomOversize = Math.max(
-            this._borderSize,
-            (titleHeight / 2) + (this._borderSize / 2));
-
-        return [topOversize, bottomOversize];
-    }
-
-    chromeWidths() {
-        this._border.ensure_style();
-        const [, closeButtonWidth] = this._closeButton.get_preferred_width(-1);
-
-        const leftOversize = this._closeButtonSide === St.Side.LEFT
-            ? (this._borderSize / 2) + (closeButtonWidth / 2)
-            : this._borderSize;
-        const rightOversize = this._closeButtonSide === St.Side.LEFT
-            ? this._borderSize
-            : (this._borderSize / 2) + (closeButtonWidth / 2);
-
-        return [leftOversize, rightOversize];
-    }
-
-    showOverlay(animate) {
-        const ongoingTransition = this._border.get_transition('opacity');
-
-        // Don't do anything if we're fully visible already
-        if (this._border.visible && !ongoingTransition)
-            return;
-
-        // If we're supposed to animate and an animation in our direction
-        // is already happening, let that one continue
-        if (animate &&
-            ongoingTransition &&
-            ongoingTransition.get_interval().peek_final_value() === 255)
-            return;
-
-        const toShow = this._windowCanClose()
-            ? [this._border, this._title, this._closeButton]
-            : [this._border, this._title];
-
-        toShow.forEach(a => {
-            a.opacity = 0;
-            a.show();
-            a.ease({
-                opacity: 255,
-                duration: animate ? WINDOW_OVERLAY_FADE_TIME : 0,
-                mode: Clutter.AnimationMode.EASE_OUT_QUAD,
-            });
-        });
-
-        this.emit('show-chrome');
-    }
-
-    hideOverlay(animate) {
-        const ongoingTransition = this._border.get_transition('opacity');
-
-        // Don't do anything if we're fully hidden already
-        if (!this._border.visible && !ongoingTransition)
-            return;
-
-        // If we're supposed to animate and an animation in our direction
-        // is already happening, let that one continue
-        if (animate &&
-            ongoingTransition &&
-            ongoingTransition.get_interval().peek_final_value() === 0)
-            return;
-
-        [this._border, this._title, this._closeButton].forEach(a => {
-            a.opacity = 255;
-            a.ease({
-                opacity: 0,
-                duration: animate ? WINDOW_OVERLAY_FADE_TIME : 0,
-                mode: Clutter.AnimationMode.EASE_OUT_QUAD,
-                onComplete: () => a.hide(),
-            });
-        });
-    }
-
-    _addWindow(metaWindow) {
-        const clone = this._windowContainer.layout_manager.addWindow(metaWindow);
-
-        // We expect this to be used for all interaction rather than
-        // the ClutterClone; as the former is reactive and the latter
-        // is not, this just works for most cases. However, for DND all
-        // actors are picked, so DND operations would operate on the clone.
-        // To avoid this, we hide it from pick.
-        Shell.util_set_hidden_from_pick(clone, true);
-    }
-
-    vfunc_has_overlaps() {
-        return this._hasAttachedDialogs();
-    }
-
-    _deleteAll() {
-        const windows = this._windowContainer.layout_manager.getWindows();
-
-        // Delete all windows, starting from the bottom-most (most-modal) one
-        for (const window of windows.reverse())
-            window.delete(global.get_current_time());
-
-        this._closeRequested = true;
-    }
-
-    addDialog(win) {
-        let parent = win.get_transient_for();
-        while (parent.is_attached_dialog())
-            parent = parent.get_transient_for();
-
-        // Display dialog if it is attached to our metaWindow
-        if (win.is_attached_dialog() && parent == this.metaWindow)
-            this._addWindow(win);
-
-        // The dialog popped up after the user tried to close the window,
-        // assume it's a close confirmation and leave the overview
-        if (this._closeRequested)
-            this._activate();
-    }
-
-    _hasAttachedDialogs() {
-        return this._windowContainer.layout_manager.getWindows().length > 1;
-    }
-
-    _updateAttachedDialogs() {
-        let iter = win => {
-            let actor = win.get_compositor_private();
-
-            if (!actor)
-                return false;
-            if (!win.is_attached_dialog())
-                return false;
-
-            this._addWindow(win);
-            win.foreach_transient(iter);
-            return true;
-        };
-        this.metaWindow.foreach_transient(iter);
-    }
-
-    get boundingBox() {
-        const box = this._windowContainer.layout_manager.bounding_box;
-
-        return {
-            x: box.x1,
-            y: box.y1,
-            width: box.get_width(),
-            height: box.get_height(),
-        };
-    }
-
-    get windowCenter() {
-        const box = this._windowContainer.layout_manager.bounding_box;
-
-        return new Graphene.Point({
-            x: box.get_x() + box.get_width() / 2,
-            y: box.get_y() + box.get_height() / 2,
-        });
-    }
-
-    // Find the actor just below us, respecting reparenting done by DND code
-    _getActualStackAbove() {
-        if (this._stackAbove == null)
-            return null;
-
-        if (this.inDrag) {
-            if (this._stackAbove._delegate)
-                return this._stackAbove._delegate._getActualStackAbove();
-            else
-                return null;
-        } else {
-            return this._stackAbove;
-        }
-    }
-
-    setStackAbove(actor) {
-        this._stackAbove = actor;
-        if (this.inDrag)
-            // We'll fix up the stack after the drag
-            return;
-
-        let parent = this.get_parent();
-        let actualAbove = this._getActualStackAbove();
-        if (actualAbove == null)
-            parent.set_child_below_sibling(this, null);
-        else
-            parent.set_child_above_sibling(this, actualAbove);
-    }
-
-    _onDestroy() {
-        this._windowActor.disconnect(this._windowDestroyId);
-
-        this.metaWindow._delegate = null;
-        this._delegate = null;
-
-        this.metaWindow.disconnect(this._updateCaptionId);
-
-        if (this._longPressLater) {
-            Meta.later_remove(this._longPressLater);
-            delete this._longPressLater;
-        }
-
-        if (this._idleHideOverlayId > 0) {
-            GLib.source_remove(this._idleHideOverlayId);
-            this._idleHideOverlayId = 0;
-        }
-
-        if (this.inDrag) {
-            this.emit('drag-end');
-            this.inDrag = false;
-        }
-    }
-
-    _activate() {
-        this._selected = true;
-        this.emit('selected', global.get_current_time());
-    }
-
-    vfunc_enter_event(crossingEvent) {
-        this.showOverlay(true);
-        return super.vfunc_enter_event(crossingEvent);
-    }
-
-    vfunc_leave_event(crossingEvent) {
-        if (this._idleHideOverlayId > 0)
-            GLib.source_remove(this._idleHideOverlayId);
-
-        this._idleHideOverlayId = GLib.timeout_add(
-            GLib.PRIORITY_DEFAULT,
-            WINDOW_OVERLAY_IDLE_HIDE_TIMEOUT, () => {
-                if (this._closeButton['has-pointer'] ||
-                    this._title['has-pointer'])
-                    return GLib.SOURCE_CONTINUE;
-
-                if (!this['has-pointer'])
-                    this.hideOverlay(true);
-
-                this._idleHideOverlayId = 0;
-                return GLib.SOURCE_REMOVE;
-            });
-
-        GLib.Source.set_name_by_id(this._idleHideOverlayId, '[gnome-shell] this._idleHideOverlayId');
-
-        return super.vfunc_leave_event(crossingEvent);
-    }
-
-    vfunc_key_focus_in() {
-        super.vfunc_key_focus_in();
-        this.showOverlay(true);
-    }
-
-    vfunc_key_focus_out() {
-        super.vfunc_key_focus_out();
-        this.hideOverlay(true);
-    }
-
-    vfunc_key_press_event(keyEvent) {
-        let symbol = keyEvent.keyval;
-        let isEnter = symbol == Clutter.KEY_Return || symbol == Clutter.KEY_KP_Enter;
-        if (isEnter) {
-            this._activate();
-            return true;
-        }
-
-        return super.vfunc_key_press_event(keyEvent);
-    }
-
-    _onLongPress(action, actor, state) {
-        // Take advantage of the Clutter policy to consider
-        // a long-press canceled when the pointer movement
-        // exceeds dnd-drag-threshold to manually start the drag
-        if (state == Clutter.LongPressState.CANCEL) {
-            let event = Clutter.get_current_event();
-            this._dragTouchSequence = event.get_event_sequence();
-
-            if (this._longPressLater)
-                return true;
-
-            // A click cancels a long-press before any click handler is
-            // run - make sure to not start a drag in that case
-            this._longPressLater = Meta.later_add(Meta.LaterType.BEFORE_REDRAW, () => {
-                delete this._longPressLater;
-                if (this._selected)
-                    return;
-                let [x, y] = action.get_coords();
-                action.release();
-                this._draggable.startDrag(x, y, global.get_current_time(), this._dragTouchSequence, 
event.get_device());
-            });
-        } else {
-            this.showOverlay(true);
-        }
-        return true;
-    }
-
-    _onDragBegin(_draggable, _time) {
-        this.inDrag = true;
-        this.hideOverlay(false);
-        this.emit('drag-begin');
-    }
-
-    handleDragOver(source, actor, x, y, time) {
-        return this._workspace.handleDragOver(source, actor, x, y, time);
-    }
-
-    acceptDrop(source, actor, x, y, time) {
-        return this._workspace.acceptDrop(source, actor, x, y, time);
-    }
-
-    _onDragCancelled(_draggable, _time) {
-        this.emit('drag-cancelled');
-    }
-
-    _onDragEnd(_draggable, _time, _snapback) {
-        this.inDrag = false;
-
-        // We may not have a parent if DnD completed successfully, in
-        // which case our clone will shortly be destroyed and replaced
-        // with a new one on the target workspace.
-        let parent = this.get_parent();
-        if (parent !== null) {
-            if (this._stackAbove == null)
-                parent.set_child_below_sibling(this, null);
-            else
-                parent.set_child_above_sibling(this, this._stackAbove);
-        }
-
-        if (this['has-pointer'])
-            this.showOverlay(true);
-
-        this.emit('drag-end');
-    }
-});
-
 var WindowPositionFlags = {
     NONE: 0,
     INITIAL: 1 << 0,


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]