summaryrefslogtreecommitdiffstats
path: root/js/ui/workspace.js
diff options
context:
space:
mode:
Diffstat (limited to 'js/ui/workspace.js')
-rw-r--r--js/ui/workspace.js1457
1 files changed, 1457 insertions, 0 deletions
diff --git a/js/ui/workspace.js b/js/ui/workspace.js
new file mode 100644
index 0000000..bf631a5
--- /dev/null
+++ b/js/ui/workspace.js
@@ -0,0 +1,1457 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+/* exported Workspace */
+
+const {Clutter, GLib, GObject, Graphene, Meta, Shell, St} = imports.gi;
+
+const Background = imports.ui.background;
+const DND = imports.ui.dnd;
+const Main = imports.ui.main;
+const OverviewControls = imports.ui.overviewControls;
+const Params = imports.misc.params;
+const Util = imports.misc.util;
+const { WindowPreview } = imports.ui.windowPreview;
+
+var WINDOW_PREVIEW_MAXIMUM_SCALE = 0.95;
+
+var WINDOW_REPOSITIONING_DELAY = 750;
+
+// 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,
+// we stop looking for a new layout and use the previous layout.
+// Otherwise, we keep looking for a new layout.
+var LAYOUT_SCALE_WEIGHT = 1;
+var LAYOUT_SPACE_WEIGHT = 0.1;
+
+const BACKGROUND_CORNER_RADIUS_PIXELS = 30;
+
+// Window Thumbnail Layout Algorithm
+// =================================
+//
+// General overview
+// ----------------
+//
+// The window thumbnail layout algorithm calculates some optimal layout
+// by computing layouts with some number of rows, calculating how good
+// each layout is, and stopping iterating when it finds one that is worse
+// than the previous layout. A layout consists of which windows are in
+// which rows, row sizes and other general state tracking that would make
+// calculating window positions from this information fairly easy.
+//
+// After a layout is computed that's considered the best layout, we
+// compute the layout scale to fit it in the area, and then compute
+// slots (sizes and positions) for each thumbnail.
+//
+// Layout generation
+// -----------------
+//
+// Layout generation is naive and simple: we simply add windows to a row
+// until we've added too many windows to a row, and then make a new row,
+// until we have our required N rows. The potential issue with this strategy
+// is that we may have too many windows at the bottom in some pathological
+// cases, which tends to make the thumbnails have the shape of a pile of
+// sand with a peak, with one window at the top.
+//
+// Scaling factors
+// ---------------
+//
+// Thumbnail position is mostly straightforward -- the main issue is
+// computing an optimal scale for each window that fits the constraints,
+// and doesn't make the thumbnail too small to see. There are two factors
+// involved in thumbnail scale to make sure that these two goals are met:
+// the window scale (calculated by _computeWindowScale) and the layout
+// scale (calculated by computeSizeAndScale).
+//
+// The calculation logic becomes slightly more complicated because row
+// and column spacing are not scaled, they're constant, so we can't
+// simply generate a bunch of window positions and then scale it. In
+// practice, it's not too bad -- we can simply try to fit the layout
+// in the input area minus whatever spacing we have, and then add
+// it back afterwards.
+//
+// The window scale is constant for the window's size regardless of the
+// input area or the layout scale or rows or anything else, and right
+// now just enlarges the window if it's too small. The fact that this
+// factor is stable makes it easy to calculate, so there's no sense
+// in not applying it in most calculations.
+//
+// The layout scale depends on the input area, the rows, etc, but is the
+// same for the entire layout, rather than being per-window. After
+// generating the rows of windows, we basically do some basic math to
+// fit the full, unscaled layout to the input area, as described above.
+//
+// With these two factors combined, the final scale of each thumbnail is
+// simply windowScale * layoutScale... almost.
+//
+// There's one additional constraint: the thumbnail scale must never be
+// larger than WINDOW_PREVIEW_MAXIMUM_SCALE, which means that the inequality:
+//
+// windowScale * layoutScale <= WINDOW_PREVIEW_MAXIMUM_SCALE
+//
+// must always be true. This is for each individual window -- while we
+// could adjust layoutScale to make the largest thumbnail smaller than
+// WINDOW_PREVIEW_MAXIMUM_SCALE, it would shrink windows which are already
+// under the inequality. To solve this, we simply cheat: we simply keep
+// each window's "cell" area to be the same, but we shrink the thumbnail
+// and center it horizontally, and align it to the bottom vertically.
+
+var LayoutStrategy = class {
+ constructor(params) {
+ params = Params.parse(params, {
+ monitor: null,
+ rowSpacing: 0,
+ columnSpacing: 0,
+ });
+
+ if (!params.monitor)
+ throw new Error(`No monitor param passed to ${this.constructor.name}`);
+
+ this._monitor = params.monitor;
+ this._rowSpacing = params.rowSpacing;
+ this._columnSpacing = params.columnSpacing;
+ }
+
+ // Compute a strategy-specific overall layout given a list of WindowPreviews
+ // @windows and the strategy-specific @layoutParams.
+ //
+ // Returns a strategy-specific layout object that is opaque to the user.
+ computeLayout(_windows, _layoutParams) {
+ throw new GObject.NotImplementedError(`computeLayout in ${this.constructor.name}`);
+ }
+
+ // Given @layout and @area, compute the overall scale of the layout and
+ // space occupied by the layout.
+ //
+ // This method returns an array where the first element is the scale and
+ // the second element is the space.
+ //
+ // This method must be called before calling computeWindowSlots(), as it
+ // sets the fixed overall scale of the layout.
+ computeScaleAndSpace(_layout, _area) {
+ throw new GObject.NotImplementedError(`computeScaleAndSpace in ${this.constructor.name}`);
+ }
+
+ // Returns an array with final position and size information for each
+ // window of the layout, given a bounding area that it will be inside of.
+ computeWindowSlots(_layout, _area) {
+ throw new GObject.NotImplementedError(`computeWindowSlots in ${this.constructor.name}`);
+ }
+};
+
+var UnalignedLayoutStrategy = class extends LayoutStrategy {
+ _newRow() {
+ // Row properties:
+ //
+ // * x, y are the position of row, relative to area
+ //
+ // * width, height are the scaled versions of fullWidth, fullHeight
+ //
+ // * width also has the spacing in between windows. It's not in
+ // fullWidth, as the spacing is constant, whereas fullWidth is
+ // meant to be scaled
+ //
+ // * neither height/fullHeight have any sort of spacing or padding
+ return {
+ x: 0, y: 0,
+ width: 0, height: 0,
+ fullWidth: 0, fullHeight: 0,
+ windows: [],
+ };
+ }
+
+ // Computes and returns an individual scaling factor for @window,
+ // to be applied in addition to the overall layout scale.
+ _computeWindowScale(window) {
+ // Since we align windows next to each other, the height of the
+ // thumbnails is much more important to preserve than the width of
+ // them, so two windows with equal height, but maybe differering
+ // widths line up.
+ let ratio = window.boundingBox.height / this._monitor.height;
+
+ // The purpose of this manipulation here is to prevent windows
+ // from getting too small. For something like a calculator window,
+ // we need to bump up the size just a bit to make sure it looks
+ // good. We'll use a multiplier of 1.5 for this.
+
+ // Map from [0, 1] to [1.5, 1]
+ return Util.lerp(1.5, 1, ratio);
+ }
+
+ _computeRowSizes(layout) {
+ let { rows, scale } = layout;
+ for (let i = 0; i < rows.length; i++) {
+ let row = rows[i];
+ row.width = row.fullWidth * scale + (row.windows.length - 1) * this._columnSpacing;
+ row.height = row.fullHeight * scale;
+ }
+ }
+
+ _keepSameRow(row, window, width, idealRowWidth) {
+ if (row.fullWidth + width <= idealRowWidth)
+ return true;
+
+ let oldRatio = row.fullWidth / idealRowWidth;
+ let newRatio = (row.fullWidth + width) / idealRowWidth;
+
+ if (Math.abs(1 - newRatio) < Math.abs(1 - oldRatio))
+ return true;
+
+ return false;
+ }
+
+ _sortRow(row) {
+ // Sort windows horizontally to minimize travel distance.
+ // This affects in what order the windows end up in a row.
+ row.windows.sort((a, b) => a.windowCenter.x - b.windowCenter.x);
+ }
+
+ computeLayout(windows, layoutParams) {
+ layoutParams = Params.parse(layoutParams, {
+ numRows: 0,
+ });
+
+ if (layoutParams.numRows === 0)
+ throw new Error(`${this.constructor.name}: No numRows given in layout params`);
+
+ const numRows = layoutParams.numRows;
+
+ let rows = [];
+ let totalWidth = 0;
+ for (let i = 0; i < windows.length; i++) {
+ let window = windows[i];
+ let s = this._computeWindowScale(window);
+ totalWidth += window.boundingBox.width * s;
+ }
+
+ let idealRowWidth = totalWidth / numRows;
+
+ // Sort windows vertically to minimize travel distance.
+ // This affects what rows the windows get placed in.
+ let sortedWindows = windows.slice();
+ sortedWindows.sort((a, b) => a.windowCenter.y - b.windowCenter.y);
+
+ let windowIdx = 0;
+ for (let i = 0; i < numRows; i++) {
+ let row = this._newRow();
+ rows.push(row);
+
+ for (; windowIdx < sortedWindows.length; windowIdx++) {
+ let window = sortedWindows[windowIdx];
+ let s = this._computeWindowScale(window);
+ let width = window.boundingBox.width * s;
+ let height = window.boundingBox.height * s;
+ row.fullHeight = Math.max(row.fullHeight, height);
+
+ // either new width is < idealWidth or new width is nearer from idealWidth then oldWidth
+ if (this._keepSameRow(row, window, width, idealRowWidth) || (i === numRows - 1)) {
+ row.windows.push(window);
+ row.fullWidth += width;
+ } else {
+ break;
+ }
+ }
+ }
+
+ let gridHeight = 0;
+ let maxRow;
+ for (let i = 0; i < numRows; i++) {
+ let row = rows[i];
+ this._sortRow(row);
+
+ if (!maxRow || row.fullWidth > maxRow.fullWidth)
+ maxRow = row;
+ gridHeight += row.fullHeight;
+ }
+
+ return {
+ numRows,
+ rows,
+ maxColumns: maxRow.windows.length,
+ gridWidth: maxRow.fullWidth,
+ gridHeight,
+ };
+ }
+
+ computeScaleAndSpace(layout, area) {
+ let hspacing = (layout.maxColumns - 1) * this._columnSpacing;
+ let vspacing = (layout.numRows - 1) * this._rowSpacing;
+
+ let spacedWidth = area.width - hspacing;
+ let spacedHeight = area.height - vspacing;
+
+ let horizontalScale = spacedWidth / layout.gridWidth;
+ let verticalScale = spacedHeight / layout.gridHeight;
+
+ // Thumbnails should be less than 70% of the original size
+ let scale = Math.min(
+ horizontalScale, verticalScale, WINDOW_PREVIEW_MAXIMUM_SCALE);
+
+ let scaledLayoutWidth = layout.gridWidth * scale + hspacing;
+ let scaledLayoutHeight = layout.gridHeight * scale + vspacing;
+ let space = (scaledLayoutWidth * scaledLayoutHeight) / (area.width * area.height);
+
+ layout.scale = scale;
+
+ return [scale, space];
+ }
+
+ computeWindowSlots(layout, area) {
+ this._computeRowSizes(layout);
+
+ let { rows, scale } = layout;
+
+ let slots = [];
+
+ // Do this in three parts.
+ let heightWithoutSpacing = 0;
+ for (let i = 0; i < rows.length; i++) {
+ let row = rows[i];
+ heightWithoutSpacing += row.height;
+ }
+
+ let verticalSpacing = (rows.length - 1) * this._rowSpacing;
+ let additionalVerticalScale = Math.min(1, (area.height - verticalSpacing) / heightWithoutSpacing);
+
+ // keep track how much smaller the grid becomes due to scaling
+ // so it can be centered again
+ let compensation = 0;
+ let y = 0;
+
+ for (let i = 0; i < rows.length; i++) {
+ let row = rows[i];
+
+ // If this window layout row doesn't fit in the actual
+ // geometry, then apply an additional scale to it.
+ let horizontalSpacing = (row.windows.length - 1) * this._columnSpacing;
+ let widthWithoutSpacing = row.width - horizontalSpacing;
+ let additionalHorizontalScale = Math.min(1, (area.width - horizontalSpacing) / widthWithoutSpacing);
+
+ if (additionalHorizontalScale < additionalVerticalScale) {
+ row.additionalScale = additionalHorizontalScale;
+ // Only consider the scaling in addition to the vertical scaling for centering.
+ compensation += (additionalVerticalScale - additionalHorizontalScale) * row.height;
+ } else {
+ row.additionalScale = additionalVerticalScale;
+ // No compensation when scaling vertically since centering based on a too large
+ // height would undo what vertical scaling is trying to achieve.
+ }
+
+ row.x = area.x + (Math.max(area.width - (widthWithoutSpacing * row.additionalScale + horizontalSpacing), 0) / 2);
+ row.y = area.y + (Math.max(area.height - (heightWithoutSpacing + verticalSpacing), 0) / 2) + y;
+ y += row.height * row.additionalScale + this._rowSpacing;
+ }
+
+ compensation /= 2;
+
+ for (let i = 0; i < rows.length; i++) {
+ const row = rows[i];
+ const rowY = row.y + compensation;
+ const rowHeight = row.height * row.additionalScale;
+
+ let x = row.x;
+ for (let j = 0; j < row.windows.length; j++) {
+ let window = row.windows[j];
+
+ let s = scale * this._computeWindowScale(window) * row.additionalScale;
+ let cellWidth = window.boundingBox.width * s;
+ let cellHeight = window.boundingBox.height * s;
+
+ s = Math.min(s, WINDOW_PREVIEW_MAXIMUM_SCALE);
+ let cloneWidth = window.boundingBox.width * s;
+ const cloneHeight = window.boundingBox.height * s;
+
+ let cloneX = x + (cellWidth - cloneWidth) / 2;
+ let cloneY;
+
+ // If there's only one row, align windows vertically centered inside the row
+ if (rows.length === 1)
+ cloneY = rowY + (rowHeight - cloneHeight) / 2;
+ // If there are multiple rows, align windows to the bottom edge of the row
+ else
+ cloneY = rowY + rowHeight - cellHeight;
+
+ // Align with the pixel grid to prevent blurry windows at scale = 1
+ cloneX = Math.floor(cloneX);
+ cloneY = Math.floor(cloneY);
+
+ slots.push([cloneX, cloneY, cloneWidth, cloneHeight, window]);
+ x += cellWidth + this._columnSpacing;
+ }
+ }
+ return slots;
+ }
+};
+
+function animateAllocation(actor, box) {
+ actor.save_easing_state();
+ actor.set_easing_mode(Clutter.AnimationMode.EASE_OUT_QUAD);
+ actor.set_easing_duration(200);
+
+ actor.allocate(box);
+
+ actor.restore_easing_state();
+
+ return actor.get_transition('allocation');
+}
+
+var WorkspaceLayout = GObject.registerClass({
+ Properties: {
+ 'spacing': GObject.ParamSpec.double(
+ 'spacing', 'Spacing', 'Spacing',
+ GObject.ParamFlags.READWRITE,
+ 0, Infinity, 20),
+ 'layout-frozen': GObject.ParamSpec.boolean(
+ 'layout-frozen', 'Layout frozen', 'Layout frozen',
+ GObject.ParamFlags.READWRITE,
+ false),
+ },
+}, class WorkspaceLayout extends Clutter.LayoutManager {
+ _init(metaWorkspace, monitorIndex, overviewAdjustment) {
+ super._init();
+
+ this._spacing = 20;
+ this._layoutFrozen = false;
+
+ this._metaWorkspace = metaWorkspace;
+ this._monitorIndex = monitorIndex;
+ this._overviewAdjustment = overviewAdjustment;
+
+ this._container = null;
+ this._windows = new Map();
+ this._sortedWindows = [];
+ this._lastBox = null;
+ this._windowSlots = [];
+ this._layout = null;
+
+ this._needsLayout = true;
+
+ this._stateAdjustment = new St.Adjustment({
+ value: 0,
+ lower: 0,
+ upper: 1,
+ });
+
+ this._stateAdjustment.connect('notify::value', () => {
+ this._syncOpacities();
+ this.syncOverlays();
+ this.layout_changed();
+ });
+
+ this._workarea = null;
+ this._workareasChangedId = 0;
+ }
+
+ _syncOpacity(actor, metaWindow) {
+ if (!metaWindow.showing_on_its_workspace())
+ actor.opacity = this._stateAdjustment.value * 255;
+ }
+
+ _syncOpacities() {
+ this._windows.forEach(({ metaWindow }, actor) => {
+ this._syncOpacity(actor, metaWindow);
+ });
+ }
+
+ _isBetterScaleAndSpace(oldScale, oldSpace, scale, space) {
+ let spacePower = (space - oldSpace) * LAYOUT_SPACE_WEIGHT;
+ let scalePower = (scale - oldScale) * LAYOUT_SCALE_WEIGHT;
+
+ if (scale > oldScale && space > oldSpace) {
+ // Win win -- better scale and better space
+ return true;
+ } else if (scale > oldScale && space <= oldSpace) {
+ // Keep new layout only if scale gain outweighs aspect space loss
+ return scalePower > spacePower;
+ } else if (scale <= oldScale && space > oldSpace) {
+ // Keep new layout only if aspect space gain outweighs scale loss
+ return spacePower > scalePower;
+ } else {
+ // Lose -- worse scale and space
+ return false;
+ }
+ }
+
+ _adjustSpacingAndPadding(rowSpacing, colSpacing, containerBox) {
+ if (this._sortedWindows.length === 0)
+ return [rowSpacing, colSpacing, containerBox];
+
+ // All of the overlays have the same chrome sizes,
+ // so just pick the first one.
+ const window = this._sortedWindows[0];
+
+ const [topOversize, bottomOversize] = window.chromeHeights();
+ const [leftOversize, rightOversize] = window.chromeWidths();
+
+ const oversize =
+ Math.max(topOversize, bottomOversize, leftOversize, rightOversize);
+
+ if (rowSpacing !== null)
+ rowSpacing += oversize;
+ if (colSpacing !== null)
+ colSpacing += oversize;
+
+ if (containerBox) {
+ const monitor = Main.layoutManager.monitors[this._monitorIndex];
+
+ const bottomPoint = new Graphene.Point3D({ y: containerBox.y2 });
+ const transformedBottomPoint =
+ this._container.apply_transform_to_point(bottomPoint);
+ const bottomFreeSpace =
+ (monitor.y + monitor.height) - transformedBottomPoint.y;
+
+ const [, bottomOverlap] = window.overlapHeights();
+
+ if ((bottomOverlap + oversize) > bottomFreeSpace)
+ containerBox.y2 -= (bottomOverlap + oversize) - bottomFreeSpace;
+ }
+
+ return [rowSpacing, colSpacing, containerBox];
+ }
+
+ _createBestLayout(area) {
+ const [rowSpacing, columnSpacing] =
+ this._adjustSpacingAndPadding(this._spacing, this._spacing, null);
+
+ // We look for the largest scale that allows us to fit the
+ // largest row/tallest column on the workspace.
+ this._layoutStrategy = new UnalignedLayoutStrategy({
+ monitor: Main.layoutManager.monitors[this._monitorIndex],
+ rowSpacing,
+ columnSpacing,
+ });
+
+ let lastLayout = null;
+ let lastNumColumns = -1;
+ let lastScale = 0;
+ let lastSpace = 0;
+
+ for (let numRows = 1; ; numRows++) {
+ const numColumns = Math.ceil(this._sortedWindows.length / numRows);
+
+ // If adding a new row does not change column count just stop
+ // (for instance: 9 windows, with 3 rows -> 3 columns, 4 rows ->
+ // 3 columns as well => just use 3 rows then)
+ if (numColumns === lastNumColumns)
+ break;
+
+ const layout = this._layoutStrategy.computeLayout(this._sortedWindows, {
+ numRows,
+ });
+
+ const [scale, space] = this._layoutStrategy.computeScaleAndSpace(layout, area);
+
+ if (lastLayout && !this._isBetterScaleAndSpace(lastScale, lastSpace, scale, space))
+ break;
+
+ lastLayout = layout;
+ lastNumColumns = numColumns;
+ lastScale = scale;
+ lastSpace = space;
+ }
+
+ return lastLayout;
+ }
+
+ _getWindowSlots(containerBox) {
+ [, , containerBox] =
+ this._adjustSpacingAndPadding(null, null, containerBox);
+
+ const availArea = {
+ x: parseInt(containerBox.x1),
+ y: parseInt(containerBox.y1),
+ width: parseInt(containerBox.get_width()),
+ height: parseInt(containerBox.get_height()),
+ };
+
+ return this._layoutStrategy.computeWindowSlots(this._layout, availArea);
+ }
+
+ _getAdjustedWorkarea(container) {
+ const workarea = this._workarea.copy();
+
+ if (container instanceof St.Widget) {
+ const themeNode = container.get_theme_node();
+ workarea.width -= themeNode.get_horizontal_padding();
+ workarea.height -= themeNode.get_vertical_padding();
+ }
+
+ return workarea;
+ }
+
+ _syncWorkareaTracking() {
+ if (this._container) {
+ if (this._workAreaChangedId)
+ return;
+ this._workarea = Main.layoutManager.getWorkAreaForMonitor(this._monitorIndex);
+ this._workareasChangedId =
+ global.display.connect('workareas-changed', () => {
+ this._workarea = Main.layoutManager.getWorkAreaForMonitor(this._monitorIndex);
+ this.layout_changed();
+ });
+ } else if (this._workareasChangedId) {
+ global.display.disconnect(this._workareasChangedId);
+ this._workareasChangedId = 0;
+ }
+ }
+
+ vfunc_set_container(container) {
+ this._container = container;
+ this._syncWorkareaTracking();
+ this._stateAdjustment.actor = container;
+ }
+
+ vfunc_get_preferred_width(container, forHeight) {
+ const workarea = this._getAdjustedWorkarea(container);
+ if (forHeight === -1)
+ return [0, workarea.width];
+
+ const workAreaAspectRatio = workarea.width / workarea.height;
+ const widthPreservingAspectRatio = forHeight * workAreaAspectRatio;
+
+ return [0, widthPreservingAspectRatio];
+ }
+
+ vfunc_get_preferred_height(container, forWidth) {
+ const workarea = this._getAdjustedWorkarea(container);
+ if (forWidth === -1)
+ return [0, workarea.height];
+
+ const workAreaAspectRatio = workarea.width / workarea.height;
+ const heightPreservingAspectRatio = forWidth / workAreaAspectRatio;
+
+ return [0, heightPreservingAspectRatio];
+ }
+
+ vfunc_allocate(container, box) {
+ const containerBox = container.allocation;
+ const [containerWidth, containerHeight] = containerBox.get_size();
+ const containerAllocationChanged =
+ this._lastBox === null || !this._lastBox.equal(containerBox);
+
+ // If the containers size changed, we can no longer keep around
+ // the old windowSlots, so we must unfreeze the layout.
+ //
+ // However, if the overview animation is in progress, don't unfreeze
+ // the layout. This is needed to prevent windows "snapping" to their
+ // new positions during the overview closing animation when the
+ // allocation subtly expands every frame.
+ if (this._layoutFrozen && containerAllocationChanged && !Main.overview.animationInProgress) {
+ this._layoutFrozen = false;
+ this.notify('layout-frozen');
+ }
+
+ const { ControlsState } = OverviewControls;
+ const { currentState } =
+ this._overviewAdjustment.getStateTransitionParams();
+ const inSessionTransition = currentState <= ControlsState.WINDOW_PICKER;
+
+ const window = this._sortedWindows[0];
+
+ if (inSessionTransition || !window) {
+ container.remove_clip();
+ } else {
+ const [, bottomOversize] = window.chromeHeights();
+ const [containerX, containerY] = containerBox.get_origin();
+
+ const extraHeightProgress =
+ currentState - OverviewControls.ControlsState.WINDOW_PICKER;
+
+ const extraClipHeight = bottomOversize * (1 - extraHeightProgress);
+
+ container.set_clip(containerX, containerY,
+ containerWidth, containerHeight + extraClipHeight);
+ }
+
+ let layoutChanged = false;
+ if (!this._layoutFrozen || !this._lastBox) {
+ if (this._needsLayout) {
+ this._layout = this._createBestLayout(this._workarea);
+ this._needsLayout = false;
+ layoutChanged = true;
+ }
+
+ if (layoutChanged || containerAllocationChanged) {
+ this._windowSlotsBox = box.copy();
+ this._windowSlots = this._getWindowSlots(this._windowSlotsBox);
+ }
+ }
+
+ const slotsScale = box.get_width() / this._windowSlotsBox.get_width();
+ const workareaX = this._workarea.x;
+ const workareaY = this._workarea.y;
+ const workareaWidth = this._workarea.width;
+ const stateAdjustementValue = this._stateAdjustment.value;
+
+ const allocationScale = containerWidth / workareaWidth;
+
+ const childBox = new Clutter.ActorBox();
+
+ const nSlots = this._windowSlots.length;
+ for (let i = 0; i < nSlots; i++) {
+ let [x, y, width, height, child] = this._windowSlots[i];
+ if (!child.visible)
+ continue;
+
+ x *= slotsScale;
+ y *= slotsScale;
+ width *= slotsScale;
+ height *= slotsScale;
+
+ const windowInfo = this._windows.get(child);
+
+ let workspaceBoxX, workspaceBoxY;
+ let workspaceBoxWidth, workspaceBoxHeight;
+
+ if (windowInfo.metaWindow.showing_on_its_workspace()) {
+ workspaceBoxX = (child.boundingBox.x - workareaX) * allocationScale;
+ workspaceBoxY = (child.boundingBox.y - workareaY) * allocationScale;
+ workspaceBoxWidth = child.boundingBox.width * allocationScale;
+ workspaceBoxHeight = child.boundingBox.height * allocationScale;
+ } else {
+ workspaceBoxX = workareaX * allocationScale;
+ workspaceBoxY = workareaY * allocationScale;
+ workspaceBoxWidth = 0;
+ workspaceBoxHeight = 0;
+ }
+
+ // Don't allow the scaled floating size to drop below
+ // the target layout size.
+ // We only want to apply this when the scaled floating size is
+ // actually larger than the target layout size, that is while
+ // animating between the session and the window picker.
+ if (inSessionTransition) {
+ workspaceBoxWidth = Math.max(workspaceBoxWidth, width);
+ workspaceBoxHeight = Math.max(workspaceBoxHeight, height);
+ }
+
+ x = Util.lerp(workspaceBoxX, x, stateAdjustementValue);
+ y = Util.lerp(workspaceBoxY, y, stateAdjustementValue);
+ width = Util.lerp(workspaceBoxWidth, width, stateAdjustementValue);
+ height = Util.lerp(workspaceBoxHeight, height, stateAdjustementValue);
+
+ childBox.set_origin(x, y);
+ childBox.set_size(width, height);
+
+ if (windowInfo.currentTransition) {
+ windowInfo.currentTransition.get_interval().set_final(childBox);
+
+ // The timeline of the transition might not have been updated
+ // before this allocation cycle, so make sure the child
+ // still updates needs_allocation to FALSE.
+ // Unfortunately, this relies on the fast paths in
+ // clutter_actor_allocate(), otherwise we'd start a new
+ // transition on the child, replacing the current one.
+ child.allocate(child.allocation);
+ continue;
+ }
+
+ // We want layout changes (ie. larger changes to the layout like
+ // reshuffling the window order) to be animated, but small changes
+ // like changes to the container size to happen immediately (for
+ // example if the container height is being animated, we want to
+ // avoid animating the children allocations to make sure they
+ // don't "lag behind" the other animation).
+ if (layoutChanged && !Main.overview.animationInProgress) {
+ const transition = animateAllocation(child, childBox);
+ if (transition) {
+ windowInfo.currentTransition = transition;
+ windowInfo.currentTransition.connect('stopped', () => {
+ windowInfo.currentTransition = null;
+ });
+ }
+ } else {
+ child.allocate(childBox);
+ }
+ }
+
+ this._lastBox = containerBox.copy();
+ }
+
+ _syncOverlay(preview) {
+ const active = this._metaWorkspace?.active ?? true;
+ preview.overlayEnabled = active && this._stateAdjustment.value === 1;
+ }
+
+ /**
+ * syncOverlays:
+ *
+ * Synchronizes the overlay state of all window previews.
+ */
+ syncOverlays() {
+ [...this._windows.keys()].forEach(preview => this._syncOverlay(preview));
+ }
+
+ /**
+ * addWindow:
+ * @param {WindowPreview} window: the window to add
+ * @param {Meta.Window} metaWindow: the MetaWindow of the window
+ *
+ * Adds @window to the workspace, it will be shown immediately if
+ * the layout isn't frozen using the layout-frozen property.
+ *
+ * If @window is already part of the workspace, nothing will happen.
+ */
+ addWindow(window, metaWindow) {
+ if (this._windows.has(window))
+ return;
+
+ this._windows.set(window, {
+ metaWindow,
+ sizeChangedId: metaWindow.connect('size-changed', () => {
+ this._needsLayout = true;
+ this.layout_changed();
+ }),
+ destroyId: window.connect('destroy', () =>
+ this.removeWindow(window)),
+ currentTransition: null,
+ });
+
+ this._sortedWindows.push(window);
+ this._sortedWindows.sort((a, b) => {
+ const winA = this._windows.get(a).metaWindow;
+ const winB = this._windows.get(b).metaWindow;
+
+ return winA.get_stable_sequence() - winB.get_stable_sequence();
+ });
+
+ this._syncOpacity(window, metaWindow);
+ this._syncOverlay(window);
+ this._container.add_child(window);
+
+ this._needsLayout = true;
+ this.layout_changed();
+ }
+
+ /**
+ * removeWindow:
+ * @param {WindowPreview} window: the window to remove
+ *
+ * Removes @window from the workspace if @window is a part of the
+ * workspace. If the layout-frozen property is set to true, the
+ * window will still be visible until the property is set to false.
+ */
+ removeWindow(window) {
+ const windowInfo = this._windows.get(window);
+ if (!windowInfo)
+ return;
+
+ windowInfo.metaWindow.disconnect(windowInfo.sizeChangedId);
+ window.disconnect(windowInfo.destroyId);
+ if (windowInfo.currentTransition)
+ window.remove_transition('allocation');
+
+ this._windows.delete(window);
+ this._sortedWindows.splice(this._sortedWindows.indexOf(window), 1);
+
+ // The layout might be frozen and we might not update the windowSlots
+ // on the next allocation, so remove the slot now already
+ const index = this._windowSlots.findIndex(s => s[4] === window);
+ if (index !== -1)
+ this._windowSlots.splice(index, 1);
+
+ // The window might have been reparented by DND
+ if (window.get_parent() === this._container)
+ this._container.remove_child(window);
+
+ this._needsLayout = true;
+ this.layout_changed();
+ }
+
+ syncStacking(stackIndices) {
+ const windows = [...this._windows.keys()];
+ windows.sort((a, b) => {
+ const seqA = this._windows.get(a).metaWindow.get_stable_sequence();
+ const seqB = this._windows.get(b).metaWindow.get_stable_sequence();
+
+ return stackIndices[seqA] - stackIndices[seqB];
+ });
+
+ let lastWindow = null;
+ for (const window of windows) {
+ window.setStackAbove(lastWindow);
+ lastWindow = window;
+ }
+
+ this._needsLayout = true;
+ this.layout_changed();
+ }
+
+ /**
+ * getFocusChain:
+ *
+ * Gets the focus chain of the workspace. This function will return
+ * an empty array if the floating window layout is used.
+ *
+ * @returns {Array} an array of {Clutter.Actor}s
+ */
+ getFocusChain() {
+ if (this._stateAdjustment.value === 0)
+ return [];
+
+ // The fifth element in the slot array is the WindowPreview
+ return this._windowSlots.map(s => s[4]);
+ }
+
+ /**
+ * An StAdjustment for controlling and transitioning between
+ * the alignment of windows using the layout strategy and the
+ * floating window layout.
+ *
+ * A value of 0 of the adjustment completely uses the floating
+ * window layout while a value of 1 completely aligns windows using
+ * the layout strategy.
+ *
+ * @type {St.Adjustment}
+ */
+ get stateAdjustment() {
+ return this._stateAdjustment;
+ }
+
+ get spacing() {
+ return this._spacing;
+ }
+
+ set spacing(s) {
+ if (this._spacing === s)
+ return;
+
+ this._spacing = s;
+
+ this._needsLayout = true;
+ this.notify('spacing');
+ this.layout_changed();
+ }
+
+ get layoutFrozen() {
+ return this._layoutFrozen;
+ }
+
+ set layoutFrozen(f) {
+ if (this._layoutFrozen === f)
+ return;
+
+ this._layoutFrozen = f;
+
+ this.notify('layout-frozen');
+ if (!this._layoutFrozen)
+ this.layout_changed();
+ }
+});
+
+var WorkspaceBackground = GObject.registerClass(
+class WorkspaceBackground extends Shell.WorkspaceBackground {
+ _init(monitorIndex, stateAdjustment) {
+ super._init({
+ style_class: 'workspace-background',
+ x_expand: true,
+ y_expand: true,
+ monitor_index: monitorIndex,
+ });
+
+ this._monitorIndex = monitorIndex;
+ this._workarea = Main.layoutManager.getWorkAreaForMonitor(monitorIndex);
+
+ this._stateAdjustment = stateAdjustment;
+ this._stateAdjustment.connectObject('notify::value', () => {
+ this._updateBorderRadius();
+ this.queue_relayout();
+ }, this);
+ this._stateAdjustment.bind_property(
+ 'value', this, 'state-adjustment-value',
+ GObject.BindingFlags.SYNC_CREATE
+ );
+
+ this._bin = new Clutter.Actor({
+ layout_manager: new Clutter.BinLayout(),
+ clip_to_allocation: true,
+ });
+
+ this._backgroundGroup = new Meta.BackgroundGroup({
+ layout_manager: new Clutter.BinLayout(),
+ x_expand: true,
+ y_expand: true,
+ });
+ this._bin.add_child(this._backgroundGroup);
+ this.add_child(this._bin);
+
+ this._bgManager = new Background.BackgroundManager({
+ container: this._backgroundGroup,
+ monitorIndex: this._monitorIndex,
+ controlPosition: false,
+ useContentSize: false,
+ });
+
+ this._bgManager.connect('changed', () => {
+ this._updateRoundedClipBounds();
+ this._updateBorderRadius();
+ });
+
+ global.display.connectObject('workareas-changed', () => {
+ this._workarea = Main.layoutManager.getWorkAreaForMonitor(monitorIndex);
+ this._updateRoundedClipBounds();
+ this.queue_relayout();
+ }, this);
+ this._updateRoundedClipBounds();
+
+ this._updateBorderRadius();
+
+ this.connect('destroy', this._onDestroy.bind(this));
+ }
+
+ _updateBorderRadius() {
+ const { scaleFactor } = St.ThemeContext.get_for_stage(global.stage);
+ const cornerRadius = scaleFactor * BACKGROUND_CORNER_RADIUS_PIXELS;
+
+ const backgroundContent = this._bgManager.backgroundActor.content;
+ backgroundContent.rounded_clip_radius =
+ Util.lerp(0, cornerRadius, this._stateAdjustment.value);
+ }
+
+ _updateRoundedClipBounds() {
+ const monitor = Main.layoutManager.monitors[this._monitorIndex];
+
+ const rect = new Graphene.Rect();
+ rect.origin.x = this._workarea.x - monitor.x;
+ rect.origin.y = this._workarea.y - monitor.y;
+ rect.size.width = this._workarea.width;
+ rect.size.height = this._workarea.height;
+
+ this._bgManager.backgroundActor.content.set_rounded_clip_bounds(rect);
+ }
+
+ _onDestroy() {
+ if (this._bgManager) {
+ this._bgManager.destroy();
+ this._bgManager = null;
+ }
+ }
+});
+
+/**
+ * @metaWorkspace: a #Meta.Workspace, or null
+ */
+var Workspace = GObject.registerClass(
+class Workspace extends St.Widget {
+ _init(metaWorkspace, monitorIndex, overviewAdjustment) {
+ super._init({
+ style_class: 'window-picker',
+ pivot_point: new Graphene.Point({ x: 0.5, y: 0.5 }),
+ layout_manager: new Clutter.BinLayout(),
+ });
+
+ const layoutManager = new WorkspaceLayout(metaWorkspace, monitorIndex,
+ overviewAdjustment);
+
+ // Background
+ this._background =
+ new WorkspaceBackground(monitorIndex, layoutManager.stateAdjustment);
+ this.add_child(this._background);
+
+ // Window previews
+ this._container = new Clutter.Actor({
+ reactive: true,
+ x_expand: true,
+ y_expand: true,
+ });
+ this._container.layout_manager = layoutManager;
+ this.add_child(this._container);
+
+ this.metaWorkspace = metaWorkspace;
+
+ this._overviewAdjustment = overviewAdjustment;
+
+ this.monitorIndex = monitorIndex;
+ this._monitor = Main.layoutManager.monitors[this.monitorIndex];
+
+ if (monitorIndex != Main.layoutManager.primaryIndex)
+ this.add_style_class_name('external-monitor');
+
+ const clickAction = new Clutter.ClickAction();
+ clickAction.connect('clicked', action => {
+ // Switch to the workspace when not the active one, leave the
+ // overview otherwise.
+ if (action.get_button() === 1 || action.get_button() === 0) {
+ const leaveOverview = this._shouldLeaveOverview();
+
+ this.metaWorkspace?.activate(global.get_current_time());
+ if (leaveOverview)
+ Main.overview.hide();
+ }
+ });
+ this.bind_property('mapped', clickAction, 'enabled', GObject.BindingFlags.SYNC_CREATE);
+ this._container.add_action(clickAction);
+
+ this.connect('style-changed', this._onStyleChanged.bind(this));
+ this.connect('destroy', this._onDestroy.bind(this));
+
+ this._skipTaskbarSignals = new Map();
+ const windows = global.get_window_actors().map(a => a.meta_window)
+ .filter(this._isMyWindow, this);
+
+ // Create clones for windows that should be
+ // visible in the Overview
+ this._windows = [];
+ for (let i = 0; i < windows.length; i++) {
+ if (this._isOverviewWindow(windows[i]))
+ this._addWindowClone(windows[i]);
+ }
+
+ // Track window changes, but let the window tracker process them first
+ this.metaWorkspace?.connectObject(
+ 'window-added', this._windowAdded.bind(this), GObject.ConnectFlags.AFTER,
+ 'window-removed', this._windowRemoved.bind(this), GObject.ConnectFlags.AFTER,
+ 'notify::active', () => layoutManager.syncOverlays(), this);
+ global.display.connectObject(
+ 'window-entered-monitor', this._windowEnteredMonitor.bind(this), GObject.ConnectFlags.AFTER,
+ 'window-left-monitor', this._windowLeftMonitor.bind(this), GObject.ConnectFlags.AFTER,
+ this);
+ this._layoutFrozenId = 0;
+
+ // DND requires this to be set
+ this._delegate = this;
+ }
+
+ _shouldLeaveOverview() {
+ if (!this.metaWorkspace || this.metaWorkspace.active)
+ return true;
+
+ const overviewState = this._overviewAdjustment.value;
+ return overviewState > OverviewControls.ControlsState.WINDOW_PICKER;
+ }
+
+ vfunc_get_focus_chain() {
+ return this._container.layout_manager.getFocusChain();
+ }
+
+ _lookupIndex(metaWindow) {
+ return this._windows.findIndex(w => w.metaWindow == metaWindow);
+ }
+
+ containsMetaWindow(metaWindow) {
+ return this._lookupIndex(metaWindow) >= 0;
+ }
+
+ isEmpty() {
+ return this._windows.length == 0;
+ }
+
+ syncStacking(stackIndices) {
+ this._container.layout_manager.syncStacking(stackIndices);
+ }
+
+ _doRemoveWindow(metaWin) {
+ let clone = this._removeWindowClone(metaWin);
+
+ if (!clone)
+ return;
+
+ clone.destroy();
+
+ // We need to reposition the windows; to avoid shuffling windows
+ // around while the user is interacting with the workspace, we delay
+ // the positioning until the pointer remains still for at least 750 ms
+ // or is moved outside the workspace
+ this._container.layout_manager.layout_frozen = true;
+
+ if (this._layoutFrozenId > 0) {
+ GLib.source_remove(this._layoutFrozenId);
+ this._layoutFrozenId = 0;
+ }
+
+ let [oldX, oldY] = global.get_pointer();
+
+ this._layoutFrozenId = GLib.timeout_add(
+ GLib.PRIORITY_DEFAULT,
+ WINDOW_REPOSITIONING_DELAY,
+ () => {
+ const [newX, newY] = global.get_pointer();
+ const pointerHasMoved = oldX !== newX || oldY !== newY;
+ const actorUnderPointer = global.stage.get_actor_at_pos(
+ Clutter.PickMode.REACTIVE, newX, newY);
+
+ if ((pointerHasMoved && this.contains(actorUnderPointer)) ||
+ this._windows.some(w => w.contains(actorUnderPointer))) {
+ oldX = newX;
+ oldY = newY;
+ return GLib.SOURCE_CONTINUE;
+ }
+
+ this._container.layout_manager.layout_frozen = false;
+ this._layoutFrozenId = 0;
+ return GLib.SOURCE_REMOVE;
+ });
+
+ GLib.Source.set_name_by_id(this._layoutFrozenId,
+ '[gnome-shell] this._layoutFrozenId');
+ }
+
+ _doAddWindow(metaWin) {
+ let win = metaWin.get_compositor_private();
+
+ if (!win) {
+ // Newly-created windows are added to a workspace before
+ // the compositor finds out about them...
+ let id = GLib.idle_add(GLib.PRIORITY_DEFAULT, () => {
+ if (metaWin.get_compositor_private() &&
+ metaWin.get_workspace() == this.metaWorkspace)
+ this._doAddWindow(metaWin);
+ return GLib.SOURCE_REMOVE;
+ });
+ GLib.Source.set_name_by_id(id, '[gnome-shell] this._doAddWindow');
+ return;
+ }
+
+ // We might have the window in our list already if it was on all workspaces and
+ // now was moved to this workspace
+ if (this._lookupIndex(metaWin) != -1)
+ return;
+
+ if (!this._isMyWindow(metaWin))
+ return;
+
+ this._skipTaskbarSignals.set(metaWin,
+ metaWin.connect('notify::skip-taskbar', () => {
+ if (metaWin.skip_taskbar)
+ this._doRemoveWindow(metaWin);
+ else
+ this._doAddWindow(metaWin);
+ }));
+
+ if (!this._isOverviewWindow(metaWin)) {
+ if (metaWin.get_transient_for() == null)
+ return;
+
+ // Let the top-most ancestor handle all transients
+ let parent = metaWin.find_root_ancestor();
+ let clone = this._windows.find(c => c.metaWindow == parent);
+
+ // If no clone was found, the parent hasn't been created yet
+ // and will take care of the dialog when added
+ if (clone)
+ clone.addDialog(metaWin);
+
+ return;
+ }
+
+ const clone = this._addWindowClone(metaWin);
+
+ clone.set_pivot_point(0.5, 0.5);
+ clone.scale_x = 0;
+ clone.scale_y = 0;
+ clone.ease({
+ scale_x: 1,
+ scale_y: 1,
+ duration: 250,
+ onStopped: () => clone.set_pivot_point(0, 0),
+ });
+
+ if (this._layoutFrozenId > 0) {
+ // If a window was closed before, unfreeze the layout to ensure
+ // the new window is immediately shown
+ this._container.layout_manager.layout_frozen = false;
+
+ GLib.source_remove(this._layoutFrozenId);
+ this._layoutFrozenId = 0;
+ }
+ }
+
+ _windowAdded(metaWorkspace, metaWin) {
+ if (!Main.overview.closing)
+ this._doAddWindow(metaWin);
+ }
+
+ _windowRemoved(metaWorkspace, metaWin) {
+ this._doRemoveWindow(metaWin);
+ }
+
+ _windowEnteredMonitor(metaDisplay, monitorIndex, metaWin) {
+ if (monitorIndex === this.monitorIndex && !Main.overview.closing)
+ this._doAddWindow(metaWin);
+ }
+
+ _windowLeftMonitor(metaDisplay, monitorIndex, metaWin) {
+ if (monitorIndex == this.monitorIndex)
+ this._doRemoveWindow(metaWin);
+ }
+
+ // check for maximized windows on the workspace
+ hasMaximizedWindows() {
+ for (let i = 0; i < this._windows.length; i++) {
+ let metaWindow = this._windows[i].metaWindow;
+ if (metaWindow.showing_on_its_workspace() &&
+ metaWindow.maximized_horizontally &&
+ metaWindow.maximized_vertically)
+ return true;
+ }
+ return false;
+ }
+
+ _clearSkipTaskbarSignals() {
+ for (const [metaWin, id] of this._skipTaskbarSignals)
+ metaWin.disconnect(id);
+ this._skipTaskbarSignals.clear();
+ }
+
+ prepareToLeaveOverview() {
+ this._clearSkipTaskbarSignals();
+
+ for (let i = 0; i < this._windows.length; i++)
+ this._windows[i].remove_all_transitions();
+
+ if (this._layoutFrozenId > 0) {
+ GLib.source_remove(this._layoutFrozenId);
+ this._layoutFrozenId = 0;
+ }
+
+ this._container.layout_manager.layout_frozen = true;
+ Main.overview.connectObject(
+ 'hidden', this._doneLeavingOverview.bind(this), this);
+ }
+
+ _onDestroy() {
+ this._clearSkipTaskbarSignals();
+
+ if (this._layoutFrozenId > 0) {
+ GLib.source_remove(this._layoutFrozenId);
+ this._layoutFrozenId = 0;
+ }
+
+ this._windows = [];
+ }
+
+ _doneLeavingOverview() {
+ this._container.layout_manager.layout_frozen = false;
+ }
+
+ _doneShowingOverview() {
+ this._container.layout_manager.layout_frozen = false;
+ }
+
+ _isMyWindow(window) {
+ const isOnWorkspace = this.metaWorkspace === null ||
+ window.located_on_workspace(this.metaWorkspace);
+ const isOnMonitor = window.get_monitor() === this.monitorIndex;
+
+ return isOnWorkspace && isOnMonitor;
+ }
+
+ _isOverviewWindow(window) {
+ return !window.skip_taskbar;
+ }
+
+ // Create a clone of a (non-desktop) window and add it to the window list
+ _addWindowClone(metaWindow) {
+ let clone = new WindowPreview(metaWindow, this, this._overviewAdjustment);
+
+ clone.connect('selected',
+ this._onCloneSelected.bind(this));
+ clone.connect('drag-begin', () => {
+ Main.overview.beginWindowDrag(metaWindow);
+ });
+ clone.connect('drag-cancelled', () => {
+ Main.overview.cancelledWindowDrag(metaWindow);
+ });
+ clone.connect('drag-end', () => {
+ Main.overview.endWindowDrag(metaWindow);
+ });
+ clone.connect('show-chrome', () => {
+ let focus = global.stage.key_focus;
+ if (focus == null || this.contains(focus))
+ clone.grab_key_focus();
+
+ this._windows.forEach(c => {
+ if (c !== clone)
+ c.hideOverlay(true);
+ });
+ });
+ clone.connect('destroy', () => {
+ this._doRemoveWindow(metaWindow);
+ });
+
+ this._container.layout_manager.addWindow(clone, metaWindow);
+
+ if (this._windows.length == 0)
+ clone.setStackAbove(null);
+ else
+ clone.setStackAbove(this._windows[this._windows.length - 1]);
+
+ this._windows.push(clone);
+
+ return clone;
+ }
+
+ _removeWindowClone(metaWin) {
+ // find the position of the window in our list
+ let index = this._lookupIndex(metaWin);
+
+ if (index == -1)
+ return null;
+
+ this._container.layout_manager.removeWindow(this._windows[index]);
+
+ return this._windows.splice(index, 1).pop();
+ }
+
+ _onStyleChanged() {
+ const themeNode = this.get_theme_node();
+ this._container.layout_manager.spacing = themeNode.get_length('spacing');
+ }
+
+ _onCloneSelected(clone, time) {
+ const wsIndex = this.metaWorkspace?.index();
+
+ if (this._shouldLeaveOverview())
+ Main.activateWindow(clone.metaWindow, time, wsIndex);
+ else
+ this.metaWorkspace?.activate(time);
+ }
+
+ // Draggable target interface
+ handleDragOver(source, _actor, _x, _y, _time) {
+ if (source.metaWindow && !this._isMyWindow(source.metaWindow))
+ return DND.DragMotionResult.MOVE_DROP;
+ if (source.app && source.app.can_open_new_window())
+ return DND.DragMotionResult.COPY_DROP;
+ if (!source.app && source.shellWorkspaceLaunch)
+ return DND.DragMotionResult.COPY_DROP;
+
+ return DND.DragMotionResult.CONTINUE;
+ }
+
+ acceptDrop(source, actor, x, y, time) {
+ let workspaceManager = global.workspace_manager;
+ let workspaceIndex = this.metaWorkspace
+ ? this.metaWorkspace.index()
+ : workspaceManager.get_active_workspace_index();
+
+ if (source.metaWindow) {
+ const window = source.metaWindow;
+ if (this._isMyWindow(window))
+ return false;
+
+ Main.moveWindowToMonitorAndWorkspace(window,
+ this.monitorIndex, workspaceIndex);
+ return true;
+ } else if (source.app && source.app.can_open_new_window()) {
+ if (source.animateLaunchAtPos)
+ source.animateLaunchAtPos(actor.x, actor.y);
+
+ source.app.open_new_window(workspaceIndex);
+ return true;
+ } else if (!source.app && source.shellWorkspaceLaunch) {
+ // While unused in our own drag sources, shellWorkspaceLaunch allows
+ // extensions to define custom actions for their drag sources.
+ source.shellWorkspaceLaunch({
+ workspace: workspaceIndex,
+ timestamp: time,
+ });
+ return true;
+ }
+
+ return false;
+ }
+
+ get stateAdjustment() {
+ return this._container.layout_manager.stateAdjustment;
+ }
+});