workspace: Introduce layout manager for allocating the Workspace

Add a new ClutterLayoutManager for layouting the workspaces of the
overview, WorkspaceLayout.

This layout manager integrates the existing LayoutStrategies used to
layout the window clones of the overview and supports freezing the
layout, animating between layout changes and adjusting the spacing for
the width and height of the window chrome. It also adds support for a
layout of the windows that looks the same as the actual workspace,
transitioning between that layout and the LayoutStrategy can be done by
setting the value of an StAdjustment, available using the
stateAdjustment getter function.

This will replace the current static-positioning based layouting of the
window clones in the next commit.

https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/1305
This commit is contained in:
Jonas Dreßler 2020-06-03 18:10:09 +02:00 committed by Florian Müllner
parent b7db56ca9b
commit 21187a4cec

View File

@ -389,6 +389,426 @@ function rectEqual(one, two) {
one.height == two.height;
}
function animateAllocation(actor, box) {
if (actor.allocation.equal(box) ||
actor.allocation.get_width() === 0 ||
actor.allocation.get_height() === 0) {
actor.allocate(box);
return null;
}
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) {
super._init();
this._spacing = 20;
this._layoutFrozen = false;
this._monitorIndex = monitorIndex;
this._workarea = metaWorkspace
? metaWorkspace.get_work_area_for_monitor(this._monitorIndex)
: Main.layoutManager.getWorkAreaForMonitor(this._monitorIndex);
this._container = null;
this._windows = new Map();
this._sortedWindows = [];
this._lastBox = null;
this._windowSlots = [];
this._layout = null;
this._stateAdjustment = new St.Adjustment({
value: 1,
lower: 0,
upper: 1,
});
this._stateAdjustment.connect('notify::value', () =>
this.layout_changed());
}
_isBetterLayout(oldLayout, newLayout) {
if (oldLayout.scale === undefined)
return true;
let spacePower = (newLayout.space - oldLayout.space) * LAYOUT_SPACE_WEIGHT;
let scalePower = (newLayout.scale - oldLayout.scale) * LAYOUT_SCALE_WEIGHT;
if (newLayout.scale > oldLayout.scale && newLayout.space > oldLayout.space) {
// Win win -- better scale and better space
return true;
} else if (newLayout.scale > oldLayout.scale && newLayout.space <= oldLayout.space) {
// Keep new layout only if scale gain outweighs aspect space loss
return scalePower > spacePower;
} else if (newLayout.scale <= oldLayout.scale && newLayout.space > oldLayout.space) {
// 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 [colSpacing, rowSpacing, 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();
if (rowSpacing)
rowSpacing += Math.max(topOversize, bottomOversize);
if (colSpacing)
colSpacing += Math.max(leftOversize, rightOversize);
if (containerBox) {
containerBox.x1 += leftOversize;
containerBox.x2 -= rightOversize;
containerBox.y1 += topOversize;
containerBox.y2 -= bottomOversize;
}
return [rowSpacing, colSpacing, containerBox];
}
_createBestLayout(area) {
const [rowSpacing, colSpacing] =
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.
const strategy = new UnalignedLayoutStrategy(
Main.layoutManager.monitors[this._monitorIndex],
rowSpacing,
colSpacing);
let lastLayout = {};
for (let numRows = 1; ; numRows++) {
let 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 === lastLayout.numColumns)
break;
let layout = { area, strategy, numRows, numColumns };
strategy.computeLayout(this._sortedWindows, layout);
strategy.computeScaleAndSpace(layout);
if (!this._isBetterLayout(lastLayout, layout))
break;
lastLayout = layout;
}
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._layout.strategy.computeWindowSlots(this._layout, availArea);
}
vfunc_set_container(container) {
this._container = container;
this._stateAdjustment.actor = container;
}
vfunc_get_preferred_width(container, forHeight) {
if (forHeight === -1)
return [0, this._workarea.width];
const workAreaAspectRatio = this._workarea.width / this._workarea.height;
const widthPreservingAspectRatio = forHeight * workAreaAspectRatio;
return [0, widthPreservingAspectRatio];
}
vfunc_get_preferred_height(container, forWidth) {
if (forWidth === -1)
return [0, this._workarea.height];
const workAreaAspectRatio = this._workarea.width / this._workarea.height;
const heightPreservingAspectRatio = forWidth / workAreaAspectRatio;
return [0, heightPreservingAspectRatio];
}
vfunc_allocate(container, box) {
const containerAllocationChanged =
this._lastBox === null || !this._lastBox.equal(box);
this._lastBox = box.copy();
// If the containers size changed, we can no longer keep around
// the old windowSlots, so we must unfreeze the layout
if (this._layoutFrozen && containerAllocationChanged) {
this._layoutFrozen = false;
this.notify('layout-frozen');
}
let layoutChanged = false;
if (!this._layoutFrozen) {
if (this._layout === null) {
this._layout = this._createBestLayout(this._workarea);
layoutChanged = true;
}
if (layoutChanged || containerAllocationChanged)
this._windowSlots = this._getWindowSlots(box.copy());
}
const allocationScale = box.get_width() / this._workarea.width;
const workspaceBox = new Clutter.ActorBox();
const layoutBox = new Clutter.ActorBox();
let childBox = new Clutter.ActorBox();
for (const child of container) {
if (!child.visible)
continue;
// The fifth element in the slot array is the WindowPreview
const index = this._windowSlots.findIndex(s => s[4] === child);
if (index === -1)
continue;
const [x, y, width, height] = this._windowSlots[index];
const windowInfo = this._windows.get(child);
child.slotId = index;
workspaceBox.x1 = child.boundingBox.x - this._workarea.x;
workspaceBox.x2 = workspaceBox.x1 + child.boundingBox.width;
workspaceBox.y1 = child.boundingBox.y - this._workarea.y;
workspaceBox.y2 = workspaceBox.y1 + child.boundingBox.height;
workspaceBox.scale(allocationScale);
layoutBox.x1 = x;
layoutBox.x2 = layoutBox.x1 + width;
layoutBox.y1 = y;
layoutBox.y2 = layoutBox.y1 + height;
childBox = workspaceBox.interpolate(layoutBox,
this._stateAdjustment.value);
if (windowInfo.currentTransition) {
windowInfo.currentTransition.get_interval().set_final(childBox);
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) {
const transition = animateAllocation(child, childBox);
if (transition) {
windowInfo.currentTransition = transition;
windowInfo.currentTransition.connect('stopped', () => {
windowInfo.currentTransition = null;
});
}
} else {
child.allocate(childBox);
}
}
}
/**
* 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._layout = null;
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._container.add_child(window);
this._layout = null;
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
this._windowSlots.splice(
this._windowSlots.findIndex(s => s[4] === window), 1);
// The window might have been reparented by DND
if (window.get_parent() === this._container)
this._container.remove_child(window);
this._layout = null;
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._layout = null;
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._layout = null;
this.notify('spacing');
this.layout_changed();
}
// eslint-disable-next-line camelcase
get layout_frozen() {
return this._layoutFrozen;
}
// eslint-disable-next-line camelcase
set layout_frozen(f) {
if (this._layoutFrozen === f)
return;
this._layoutFrozen = f;
this.notify('layout-frozen');
if (!this._layoutFrozen)
this.layout_changed();
}
});
/**
* @metaWorkspace: a #Meta.Workspace, or null
*/