1313 lines
57 KiB
JavaScript
Executable File
1313 lines
57 KiB
JavaScript
Executable File
//////////////////////////////////////////////////////////////////////////////////////////
|
|
// ,-. ,--. ,-. , , ,---. ,-. ;-. ,-. . . ,-. ,--. //
|
|
// | \ | ( ` | / | / \ | ) / | | | ) | //
|
|
// | | |- `-. |< | | | |-' | | | |-< |- //
|
|
// | / | . ) | \ | \ / | \ | | | ) | //
|
|
// `-' `--' `-' ' ` ' `-' ' `-' `--` `-' `--' //
|
|
//////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
// SPDX-FileCopyrightText: Simon Schneegans <code@simonschneegans.de>
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
'use strict';
|
|
|
|
import Gio from 'gi://Gio';
|
|
import Meta from 'gi://Meta';
|
|
import Clutter from 'gi://Clutter';
|
|
import Graphene from 'gi://Graphene';
|
|
import GObject from 'gi://GObject';
|
|
import Shell from 'gi://Shell';
|
|
import St from 'gi://St';
|
|
|
|
import * as Util from 'resource:///org/gnome/shell/misc/util.js';
|
|
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
|
|
import * as Config from 'resource:///org/gnome/shell/misc/config.js';
|
|
import {PressureBarrier} from 'resource:///org/gnome/shell/ui/layout.js';
|
|
import {WorkspacesView, FitMode} from 'resource:///org/gnome/shell/ui/workspacesView.js';
|
|
import {SwipeTracker} from 'resource:///org/gnome/shell/ui/swipeTracker.js';
|
|
import {WorkspaceAnimationController} from 'resource:///org/gnome/shell/ui/workspaceAnimation.js';
|
|
import {Extension} from 'resource:///org/gnome/shell/extensions/extension.js';
|
|
|
|
import * as utils from './src/utils.js';
|
|
import {DragGesture} from './src/DragGesture.js';
|
|
import {Skybox} from './src/Skybox.js';
|
|
|
|
//////////////////////////////////////////////////////////////////////////////////////////
|
|
// This extensions tweaks the positioning of workspaces in overview mode and while //
|
|
// switching workspaces in desktop mode to make them look like cube faces. //
|
|
//////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
const [GS_VERSION] = Config.PACKAGE_VERSION.split('.').map(s => Number(s));
|
|
|
|
// Maximum degrees the cube can be rotated up and down.
|
|
const MAX_VERTICAL_ROTATION = 50;
|
|
|
|
// Spacing to the screen sides of the vertically rotated cube.
|
|
const PADDING_V_ROTATION = 0.2;
|
|
|
|
export default class DesktopCube extends Extension {
|
|
_lastWorkspaceWidth = 0;
|
|
|
|
// ------------------------------------------------------------------------ public stuff
|
|
|
|
// This function could be called after the extension is enabled, which could be done
|
|
// from GNOME Tweaks, when you log in or when the screen is unlocked.
|
|
enable() {
|
|
|
|
// Store a reference to the settings object.
|
|
this._settings = this.getSettings();
|
|
|
|
// We will monkey-patch these methods. Let's store the original ones.
|
|
this._origEndGesture = SwipeTracker.prototype._endGesture;
|
|
this._origUpdateWorkspacesState = WorkspacesView.prototype._updateWorkspacesState;
|
|
this._origGetSpacing = WorkspacesView.prototype._getSpacing;
|
|
this._origUpdateVisibility = WorkspacesView.prototype._updateVisibility;
|
|
this._origPrepSwitch = WorkspaceAnimationController.prototype._prepareWorkspaceSwitch;
|
|
this._origFinalSwitch = WorkspaceAnimationController.prototype._finishWorkspaceSwitch;
|
|
|
|
// We will use extensionThis to refer to the extension inside the patched methods.
|
|
const extensionThis = this;
|
|
|
|
// -----------------------------------------------------------------------------------
|
|
// ------------------------------- cubify the overview -------------------------------
|
|
// -----------------------------------------------------------------------------------
|
|
|
|
// Normally, all workspaces outside the current field-of-view are hidden. We want to
|
|
// show all workspaces, so we patch this method. The original code is about here:
|
|
// https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/main/js/ui/workspacesView.js#L420
|
|
WorkspacesView.prototype._updateVisibility = function() {
|
|
this._workspaces.forEach((w) => {
|
|
w.show();
|
|
});
|
|
};
|
|
|
|
// Usually, workspaces are placed next to each other separated by a few pixels (this
|
|
// separation is usually computed by the method below). To create the desktop cube, we
|
|
// have to position all workspaces on top of each other and rotate the around a pivot
|
|
// point in the center of the cube.
|
|
// The original arrangement of the workspaces is implemented in WorkspacesView's
|
|
// vfunc_allocate() which cannot be monkey-patched. As a workaround, we return a
|
|
// negative spacing in the method below...
|
|
// The original code is about here:
|
|
// https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/main/js/ui/workspacesView.js#L219
|
|
WorkspacesView.prototype._getSpacing = function(box, fitMode, vertical) {
|
|
// We use the "normal" workspace spacing in desktop and app-grid mode.
|
|
const origValue =
|
|
extensionThis._origGetSpacing.apply(this, [box, fitMode, vertical]);
|
|
|
|
if (fitMode == FitMode.ALL) {
|
|
return origValue;
|
|
}
|
|
|
|
// Compute the negative spacing required to arrange workspaces on top of each other.
|
|
const overlapValue = -this._workspaces[0].get_preferred_width(box.get_size()[1])[1];
|
|
|
|
// Blend between the negative overlap-spacing and the "normal" spacing value.
|
|
const cubeMode = extensionThis._getCubeMode(this);
|
|
return Util.lerp(origValue, overlapValue, cubeMode);
|
|
};
|
|
|
|
// This is the main method which is called whenever the workspaces need to be
|
|
// repositioned.
|
|
// https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/main/js/ui/workspacesView.js#L255
|
|
WorkspacesView.prototype._updateWorkspacesState = function() {
|
|
// Use the original method if we have just one workspace.
|
|
const faceCount = this._workspaces.length;
|
|
if (faceCount <= 1) {
|
|
extensionThis._origUpdateWorkspacesState.apply(this);
|
|
return;
|
|
}
|
|
|
|
// Here's a minor hack to improve the performance: During the transitions to / from
|
|
// the app drawer, this._updateWorkspacesState is called twice a frame. Once from
|
|
// the notify handler of this._fitModeAdjustment and thereafter once from the notify
|
|
// handler of this._overviewAdjustment. As this seems not so useful (and degrades
|
|
// performance a lot), we skip the first call. I am not aware of any cases where
|
|
// this._fitModeAdjustment is changed without any of the over adjustments to change
|
|
// as well...
|
|
// https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/main/js/ui/workspacesView.js#L109
|
|
// https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/main/js/ui/workspacesView.js#L45
|
|
if ((new Error()).stack.includes('fitModeNotify')) {
|
|
return;
|
|
}
|
|
|
|
// Compute blending state from and to the overview, from and to the app grid, and
|
|
// from and to the desktop mode. We will use cubeMode to fold and unfold the
|
|
// cube, overviewMode to add some depth between windows and backgrounds, and
|
|
// appDrawerMode to attenuate the scaling effect of the active workspace.
|
|
const appDrawerMode = extensionThis._getAppDrawerMode(this);
|
|
const overviewMode = extensionThis._getOverviewMode(this);
|
|
const cubeMode = extensionThis._getCubeMode(this);
|
|
|
|
// First we need the width of a single workspace. Simply calling
|
|
// this._workspaces[0]._background.width does not work in all cases, as this method
|
|
// seems to be called also when the background actor is not on the stage. As a hacky
|
|
// workaround, we store the last valid workspace width we got and use that value if
|
|
// we cannot get a new one...
|
|
let workspaceWidth = extensionThis._lastWorkspaceWidth;
|
|
const bg = this._workspaces[0]._background;
|
|
if (bg.get_stage() && bg.allocation.get_width() > 0) {
|
|
workspaceWidth = bg.allocation.get_width();
|
|
|
|
// Add gaps between workspaces in overview mode.
|
|
workspaceWidth +=
|
|
overviewMode * 2 * extensionThis._settings.get_int('workpace-separation');
|
|
|
|
extensionThis._lastWorkspaceWidth = workspaceWidth;
|
|
}
|
|
|
|
// That's the angle between consecutive workspaces.
|
|
const faceAngle = extensionThis._getFaceAngle(faceCount);
|
|
|
|
// That's the z-distance from the cube faces to the rotation pivot.
|
|
const centerDepth = extensionThis._getCenterDist(workspaceWidth, faceAngle);
|
|
|
|
// Apply vertical rotation if required. This comes from the pitch value of the
|
|
// modified SwipeTracker created by _addOverviewDragGesture() further below.
|
|
this.pivot_point_z = -centerDepth;
|
|
this.set_pivot_point(0.5, 0.5);
|
|
this.rotation_angle_x = extensionThis._pitch.value * MAX_VERTICAL_ROTATION;
|
|
|
|
// During rotations, the cube is scaled down and the windows are "exploded". If we
|
|
// are directly facing a cube side, the strengths of both effects are approaching
|
|
// zero. The strengths of both effects are small during horizontal rotations to make
|
|
// workspace-switching not so obtrusive. However, during vertical rotations, the
|
|
// effects are stronger.
|
|
const [depthOffset, explode] = extensionThis._getExplodeFactors(
|
|
this._scrollAdjustment.value, extensionThis._pitch.value, centerDepth,
|
|
this._monitorIndex);
|
|
|
|
// Now loop through all workspace and compute the individual rotations.
|
|
this._workspaces.forEach((w, index) => {
|
|
// First update the corner radii. Corners are only rounded in overview.
|
|
w.stateAdjustment.value = overviewMode;
|
|
|
|
// Now update the rotation of the cube face. The rotation center is -centerDepth
|
|
// units behind the front face.
|
|
w.pivot_point_z = -centerDepth;
|
|
|
|
// Make cube smaller during rotations.
|
|
w.translation_z = -depthOffset;
|
|
|
|
// The rotation angle is transitioned proportional to cubeMode^1.5. This slows
|
|
// down the rotation a bit closer to the desktop and to the app drawer.
|
|
w.rotation_angle_y =
|
|
Math.pow(cubeMode, 1.5) * (-this._scrollAdjustment.value + index) * faceAngle;
|
|
|
|
// Distance to being the active workspace in [-1...0...1].
|
|
const dist = Math.clamp(index - this._scrollAdjustment.value, -1, 1);
|
|
|
|
// This moves next and previous workspaces a bit to the left and right. This
|
|
// ensures that we can actually see them if we look at the cube from the front.
|
|
// The value is set to zero if we have five or more workspaces.
|
|
if (faceCount <= 4) {
|
|
w.translation_x =
|
|
dist * overviewMode * extensionThis._settings.get_int('horizontal-stretch');
|
|
} else {
|
|
w.translation_x = 0;
|
|
}
|
|
|
|
// Update opacity only in overview mode.
|
|
const opacityA = extensionThis._settings.get_int('active-workpace-opacity');
|
|
const opacityB = extensionThis._settings.get_int('inactive-workpace-opacity');
|
|
const opacity = Util.lerp(opacityA, opacityB, Math.abs(dist));
|
|
w._background.set_opacity(Util.lerp(255, opacity, overviewMode));
|
|
|
|
// Update workspace scale only in app grid mode. The 0.94 is supposed to be the
|
|
// same value as the WORKSPACE_INACTIVE_SCALE defined here:
|
|
// https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/main/js/ui/workspacesView.js#L21
|
|
// As this is defined as 'const', we cannot access it here. But the exact value
|
|
// also not really matters...
|
|
const scale = Util.lerp(1, 0.94, Math.abs(dist) * appDrawerMode);
|
|
w.set_scale(scale, scale);
|
|
|
|
// Now we add some depth separation between the window clones. If the explode
|
|
// factor becomes too small, the depth sorting becomes non-deterministic.
|
|
if (explode > 0.001) {
|
|
|
|
const sortedActors = w._container.layout_manager._sortedWindows;
|
|
|
|
// Distribute the window clones translation_z values between zero and
|
|
// explode.
|
|
sortedActors.forEach((windowActor, j) => {
|
|
windowActor.translation_z = explode * (j + 1) / sortedActors.length;
|
|
});
|
|
|
|
// Now sort the window clones according to the orthogonal distance of the actor
|
|
// planes to the camera. This ensures proper depth sorting among the window
|
|
// clones.
|
|
if (sortedActors.length > 1) {
|
|
extensionThis._depthSortWindowActors(w._container.get_children(),
|
|
this._monitorIndex);
|
|
}
|
|
}
|
|
|
|
// Now we sort the children of the workspace (e.g. the background actor
|
|
// and the container for the window clones) by their orthogonal distance to the
|
|
// virtual camera. We add a tiny translation to the window-clone container to
|
|
// allow for proper sorting.
|
|
w._container.translation_z = 1;
|
|
extensionThis._depthSortWindowActors(w.get_children(), this._monitorIndex);
|
|
});
|
|
|
|
// The depth-sorting of cube faces is quite simple, we sort them by increasing
|
|
// rotation angle so that they are drawn back-to-front.
|
|
extensionThis._depthSortCubeFaces(this._workspaces);
|
|
};
|
|
|
|
|
|
// -----------------------------------------------------------------------------------
|
|
// --------------------- cubify workspace-switch in desktop mode ---------------------
|
|
// -----------------------------------------------------------------------------------
|
|
|
|
// This override rotates the workspaces during the transition to look like cube
|
|
// faces. The original movement of the workspaces is implemented in the setter of
|
|
// the progress property. We do not touch this, as keeping track of this progress
|
|
// is rather important. Instead, we listen to progress changes and tweak the
|
|
// transformation accordingly.
|
|
// This lambda is called in two places (further down), once for updates of the
|
|
// progress property, once for updates during gesture swipes. The latter does not
|
|
// trigger notify signals of the former for some reason...
|
|
const updateMonitorGroup = (group) => {
|
|
// First, we prevent any horizontal movement by countering the translation. We
|
|
// cannot simply set the x property to zero as this is used to track the
|
|
// progress.
|
|
group._container.translation_x = -group._container.x;
|
|
|
|
// That's the desired angle between consecutive workspaces.
|
|
const faceAngle = extensionThis._getFaceAngle(group._workspaceGroups.length);
|
|
|
|
// That's the z-distance from the cube faces to the rotation pivot.
|
|
const centerDepth =
|
|
extensionThis._getCenterDist(group._workspaceGroups[0].width, faceAngle);
|
|
|
|
// Apply vertical rotation if required. This comes from the pitch value of the
|
|
// modified SwipeTracker created by _addDesktopDragGesture() further below.
|
|
group._container.pivot_point_z = -centerDepth;
|
|
group._container.set_pivot_point(0.5, 0.5);
|
|
group._container.rotation_angle_x =
|
|
extensionThis._pitch.value * MAX_VERTICAL_ROTATION;
|
|
|
|
// During rotations, the cube is scaled down and the windows are "exploded". If we
|
|
// are directly facing a cube side, the strengths of both effects are approaching
|
|
// zero. The strengths of both effects are small during horizontal rotations to make
|
|
// workspace-switching not so obtrusive. However, during vertical rotations, the
|
|
// effects are stronger.
|
|
const [depthOffset, explode] = extensionThis._getExplodeFactors(
|
|
group.progress, extensionThis._pitch.value, centerDepth, group._monitor.index);
|
|
|
|
// Rotate the individual faces.
|
|
group._workspaceGroups.forEach((child, i) => {
|
|
child.set_pivot_point_z(-centerDepth);
|
|
child.set_pivot_point(0.5, 0.5);
|
|
child.rotation_angle_y = (i - group.progress) * faceAngle;
|
|
child.translation_z = -depthOffset;
|
|
child.clip_to_allocation = false;
|
|
|
|
// Counter the horizontal movement.
|
|
child.translation_x = -child.x;
|
|
|
|
// Make cube transparent during vertical rotations.
|
|
child._background.opacity = 255 * (1.0 - Math.abs(extensionThis._pitch.value));
|
|
|
|
// Now we add some depth separation between the window clones. We get the stacking
|
|
// order from the global window list. If the explode factor becomes too small, the
|
|
// depth sorting becomes non-deterministic.
|
|
if (explode > 0.001) {
|
|
const windowActors = global.get_window_actors().filter(w => {
|
|
return child._shouldShowWindow(w.meta_window);
|
|
});
|
|
|
|
// Distribute the window clones translation_z values between zero and
|
|
// explode.
|
|
windowActors.forEach((windowActor, j) => {
|
|
const record = child._windowRecords.find(r => r.windowActor === windowActor);
|
|
if (record) {
|
|
record.clone.translation_z = explode * (j + 1) / windowActors.length;
|
|
}
|
|
});
|
|
|
|
// Now sort the window clones and the background actor according to the
|
|
// orthogonal distance of the actor planes to the camera. This ensures proper
|
|
// depth sorting.
|
|
extensionThis._depthSortWindowActors(child.get_children(),
|
|
group._monitor.index);
|
|
}
|
|
});
|
|
|
|
// The depth-sorting of cube faces is quite simple, we sort them by increasing
|
|
// rotation angle.
|
|
extensionThis._depthSortCubeFaces(group._workspaceGroups);
|
|
|
|
// Update horizontal rotation of the background panorama during workspace switches.
|
|
if (this._skybox) {
|
|
this._skybox.yaw =
|
|
2 * Math.PI * group.progress / global.workspaceManager.get_n_workspaces();
|
|
}
|
|
};
|
|
|
|
// Whenever a workspace-switch is about to happen, we tweak the MonitorGroup class a
|
|
// bit to arrange the workspaces in a cube-like fashion. We have to adjust to parts of
|
|
// the code as the automatic transitions (e.g. when switching with key combinations)
|
|
// are handled differently than the gesture based switches.
|
|
// https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/main/js/ui/workspaceAnimation.js#L299
|
|
WorkspaceAnimationController.prototype._prepareWorkspaceSwitch = function() {
|
|
// Here, we call the original method without any arguments. Usually, GNOME Shell
|
|
// "skips" workspaces when switching to a workspace which is more than one workspace
|
|
// to the left or the right. This behavior is not desirable for thr cube, as it
|
|
// messes with your spatial memory. If no workspaceIndices are given to this method,
|
|
// all workspaces will be shown during the workspace switch.
|
|
extensionThis._origPrepSwitch.apply(this, []);
|
|
|
|
// Now tweak the monitor groups.
|
|
this._switchData.monitors.forEach(m => {
|
|
// Call the method above whenever the transition progress changes.
|
|
m.connect('notify::progress', () => updateMonitorGroup(m));
|
|
|
|
// Call the method above whenever a gesture is active.
|
|
const orig = m.updateSwipeForMonitor;
|
|
m.updateSwipeForMonitor = function(progress, baseMonitorGroup) {
|
|
orig.apply(m, [progress, baseMonitorGroup]);
|
|
updateMonitorGroup(m);
|
|
};
|
|
});
|
|
|
|
// Make sure that the background panorama is drawn above the window group during a
|
|
// workspace switch.
|
|
if (extensionThis._skybox) {
|
|
extensionThis._skybox.get_parent().remove_child(extensionThis._skybox);
|
|
Main.uiGroup.insert_child_above(extensionThis._skybox, global.window_group);
|
|
|
|
// If the workspaces are only on the primary monitor, the skybox would cover all
|
|
// other non-rotating screens. Therefore, we temporarily limit its size to the
|
|
// primary monitor's size.
|
|
if (Meta.prefs_get_workspaces_only_on_primary()) {
|
|
const monitor =
|
|
global.display.get_monitor_geometry(global.display.get_primary_monitor());
|
|
extensionThis._skybox.width = monitor.width;
|
|
extensionThis._skybox.height = monitor.height;
|
|
extensionThis._skybox.x = monitor.x;
|
|
extensionThis._skybox.y = monitor.y;
|
|
}
|
|
}
|
|
};
|
|
|
|
// Re-attach the background panorama to the stage once the workspace switch is done.
|
|
WorkspaceAnimationController.prototype._finishWorkspaceSwitch = function(...params) {
|
|
extensionThis._origFinalSwitch.apply(this, params);
|
|
|
|
// Make sure that the skybox covers the entire stage again.
|
|
if (extensionThis._skybox) {
|
|
extensionThis._skybox.get_parent().remove_child(extensionThis._skybox);
|
|
global.stage.insert_child_below(extensionThis._skybox, null);
|
|
|
|
if (Meta.prefs_get_workspaces_only_on_primary()) {
|
|
extensionThis._skybox.width = global.stage.width;
|
|
extensionThis._skybox.height = global.stage.height;
|
|
extensionThis._skybox.x = global.stage.x;
|
|
extensionThis._skybox.y = global.stage.y;
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
// -----------------------------------------------------------------------------------
|
|
// ------------------------- enable cube rotation by dragging ------------------------
|
|
// -----------------------------------------------------------------------------------
|
|
|
|
// Usually, in GNOME Shell 40+, workspaces are move horizontally. We tweaked this to
|
|
// look like a horizontal rotation above. To store the current vertical rotation, we
|
|
// use the adjustment below.
|
|
this._pitch = new St.Adjustment({actor: global.stage, lower: -1, upper: 1});
|
|
|
|
// The overview's SwipeTracker will control the _overviewAdjustment of the
|
|
// WorkspacesDisplay. However, only horizontal swipes will update this adjustment. If
|
|
// only our pitch adjustment is changed (e.g. the user moved the mouse only
|
|
// vertically), the _overviewAdjustment will not change and therefore the workspaces
|
|
// will not been redrawn. Here we force redrawing by notifying changes if the pitch
|
|
// value changes.
|
|
this._pitch.connect('notify::value', () => {
|
|
if (Main.actionMode == Shell.ActionMode.OVERVIEW) {
|
|
Main.overview._overview._controls._workspacesDisplay._overviewAdjustment.notify(
|
|
'value');
|
|
}
|
|
});
|
|
|
|
// In GNOME Shell, SwipeTrackers are used all over the place to capture swipe
|
|
// gestures. There's one for entering the overview, one for switching workspaces in
|
|
// desktop mode, one for switching workspaces in overview mode, one for horizontal
|
|
// scrolling in the app drawer, and many more. The ones used for workspace-switching
|
|
// usually do not respond to single-click dragging but only to multi-touch gestures.
|
|
// We want to be able to rotate the cube with the left mouse button, so we add an
|
|
// additional gesture to these two SwipeTracker instances tracking single-click drags.
|
|
|
|
// First, we fix an issue which leads to very quick workspace switches when the
|
|
// SwipeTracker are used with mouse clicks. When the mouse button is released, no
|
|
// event is added to the history. This means that the velocity is always calculated
|
|
// relative to the last received mouse movement. Even if he mouse pointer was
|
|
// stationary for some time, high velocities will be computed.
|
|
SwipeTracker.prototype._endGesture = function(time, distance, isTouchpad) {
|
|
// Add a final time step to the history.
|
|
this._history.append(time, 0);
|
|
|
|
// Then call the original method.
|
|
extensionThis._origEndGesture.apply(this, [time, distance, isTouchpad]);
|
|
};
|
|
|
|
// Add single-click drag gesture to the desktop.
|
|
if (this._settings.get_boolean('enable-desktop-dragging')) {
|
|
this._addDesktopDragGesture();
|
|
}
|
|
|
|
this._settings.connect('changed::enable-desktop-dragging', () => {
|
|
if (this._settings.get_boolean('enable-desktop-dragging')) {
|
|
this._addDesktopDragGesture();
|
|
} else {
|
|
this._removeDesktopDragGesture();
|
|
}
|
|
});
|
|
|
|
// Add single-click drag gesture to the panel.
|
|
if (this._settings.get_boolean('enable-panel-dragging')) {
|
|
this._addPanelDragGesture();
|
|
}
|
|
|
|
this._settings.connect('changed::enable-panel-dragging', () => {
|
|
if (this._settings.get_boolean('enable-panel-dragging')) {
|
|
this._addPanelDragGesture();
|
|
} else {
|
|
this._removePanelDragGesture();
|
|
}
|
|
});
|
|
|
|
// Add single-click drag gesture to the overview.
|
|
if (this._settings.get_boolean('enable-overview-dragging')) {
|
|
this._addOverviewDragGesture();
|
|
}
|
|
|
|
this._settings.connect('changed::enable-overview-dragging', () => {
|
|
if (this._settings.get_boolean('enable-overview-dragging')) {
|
|
this._addOverviewDragGesture();
|
|
} else {
|
|
this._removeOverviewDragGesture();
|
|
}
|
|
});
|
|
|
|
|
|
// -----------------------------------------------------------------------------------
|
|
// ---------------------------------- add the skybox ---------------------------------
|
|
// -----------------------------------------------------------------------------------
|
|
|
|
// This is called whenever the skybox texture setting is changed.
|
|
const updateSkybox = () => {
|
|
// First, delete the existing skybox.
|
|
if (this._skybox) {
|
|
this._skybox.destroy();
|
|
delete this._skybox;
|
|
}
|
|
|
|
const file = this._settings.get_string('background-panorama');
|
|
|
|
// Then, load a new one (if any).
|
|
if (file != '') {
|
|
try {
|
|
this._skybox = new Skybox(file);
|
|
|
|
// We add the skybox below everything.
|
|
global.stage.insert_child_below(this._skybox, null);
|
|
|
|
// Make sure that the skybox covers the entire stage.
|
|
global.stage.bind_property('width', this._skybox, 'width',
|
|
GObject.BindingFlags.SYNC_CREATE);
|
|
global.stage.bind_property('height', this._skybox, 'height',
|
|
GObject.BindingFlags.SYNC_CREATE);
|
|
|
|
} catch (error) {
|
|
utils.debug('Failed to set skybox: ' + error);
|
|
}
|
|
}
|
|
};
|
|
|
|
// Update the skybox whenever the corresponding setting is changed.
|
|
this._settings.connect('changed::background-panorama', updateSkybox);
|
|
updateSkybox();
|
|
|
|
// Update vertical rotation of the background panorama.
|
|
this._pitch.connect('notify::value', () => {
|
|
if (this._skybox) {
|
|
this._skybox.pitch = (this._pitch.value * MAX_VERTICAL_ROTATION) * Math.PI / 180;
|
|
}
|
|
});
|
|
|
|
// Update horizontal rotation of the background panorama during workspace switches in
|
|
// the overview.
|
|
Main.overview._overview.controls._workspaceAdjustment.connect('notify::value', () => {
|
|
if (this._skybox) {
|
|
this._skybox.yaw = 2 * Math.PI *
|
|
Main.overview._overview.controls._workspaceAdjustment.value /
|
|
global.workspaceManager.get_n_workspaces();
|
|
}
|
|
});
|
|
|
|
|
|
// -----------------------------------------------------------------------------------
|
|
// ----------------------- enable edge-drag workspace-switches -----------------------
|
|
// -----------------------------------------------------------------------------------
|
|
|
|
// We add two Meta.Barriers, one at each side of the stage. If the pointer hits one of
|
|
// these with enough pressure while dragging a window, we initiate a workspace-switch.
|
|
// The last parameter (0) is actually supposed to be a bitwise combination of
|
|
// Shell.ActionModes. The pressure barrier will only trigger, if Main.actionMode
|
|
// equals one of the given action modes. This works well for Shell.ActionMode.NORMAL
|
|
// and Shell.ActionMode.OVERVIEW, however it does not work for Shell.ActionMode.NONE
|
|
// (which actually equals zero). However, when we want the barriers to also trigger in
|
|
// Shell.ActionMode.NONE, as this is the mode during a drag-operation in the overview.
|
|
// Therefore, we modify the _onBarrierHit method of the pressure barrier to completely
|
|
// ignore this parameter. Instead, we check for the correct action mode in the trigger
|
|
// handler.
|
|
this._pressureBarrier =
|
|
new PressureBarrier(this._settings.get_int('edge-switch-pressure'), 1000, 0);
|
|
|
|
// Update pressure threshold when the corresponding settings key changes.
|
|
this._settings.connect('changed::edge-switch-pressure', () => {
|
|
this._pressureBarrier._threshold = this._settings.get_int('edge-switch-pressure');
|
|
});
|
|
|
|
// This is an exact copy of the original _onBarrierHit, with only one line disabled to
|
|
// ignore the given ActionMode.
|
|
// https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/main/js/ui/layout.js#L1366
|
|
this._pressureBarrier._onBarrierHit = function(barrier, event) {
|
|
barrier._isHit = true;
|
|
|
|
// If we've triggered the barrier, wait until the pointer has the
|
|
// left the barrier hitbox until we trigger it again.
|
|
if (this._isTriggered) return;
|
|
|
|
if (this._eventFilter && this._eventFilter(event)) return;
|
|
|
|
// Throw out all events not in the proper keybinding mode
|
|
// if (!(this._actionMode & Main.actionMode)) return;
|
|
|
|
let slide = this._getDistanceAlongBarrier(barrier, event);
|
|
let distance = this._getDistanceAcrossBarrier(barrier, event);
|
|
|
|
if (distance >= this._threshold) {
|
|
this._trigger();
|
|
return;
|
|
}
|
|
|
|
// Throw out events where the cursor is move more
|
|
// along the axis of the barrier than moving with
|
|
// the barrier.
|
|
if (slide > distance) return;
|
|
|
|
this._lastTime = event.time;
|
|
|
|
this._trimBarrierEvents();
|
|
distance = Math.min(15, distance);
|
|
|
|
this._barrierEvents.push([event.time, distance]);
|
|
this._currentPressure += distance;
|
|
|
|
if (this._currentPressure >= this._threshold) this._trigger();
|
|
};
|
|
|
|
// Now we add the left and right barrier to the pressure barrier.
|
|
const createBarriers = () => {
|
|
if (this._leftBarrier) {
|
|
this._pressureBarrier.removeBarrier(this._leftBarrier);
|
|
this._leftBarrier.destroy();
|
|
}
|
|
|
|
if (this._rightBarrier) {
|
|
this._pressureBarrier.removeBarrier(this._rightBarrier);
|
|
this._rightBarrier.destroy();
|
|
}
|
|
|
|
// Since GNOME 46, the display property is not required anymore.
|
|
if (GS_VERSION <= 45) {
|
|
this._leftBarrier = new Meta.Barrier({
|
|
display: global.display,
|
|
x1: 0,
|
|
x2: 0,
|
|
y1: 1,
|
|
y2: global.stage.height,
|
|
directions: Meta.BarrierDirection.POSITIVE_X,
|
|
});
|
|
|
|
this._rightBarrier = new Meta.Barrier({
|
|
display: global.display,
|
|
x1: global.stage.width,
|
|
x2: global.stage.width,
|
|
y1: 1,
|
|
y2: global.stage.height,
|
|
directions: Meta.BarrierDirection.NEGATIVE_X,
|
|
});
|
|
} else {
|
|
this._leftBarrier = new Meta.Barrier({
|
|
x1: 0,
|
|
x2: 0,
|
|
y1: 1,
|
|
y2: global.stage.height,
|
|
directions: Meta.BarrierDirection.POSITIVE_X,
|
|
});
|
|
|
|
this._rightBarrier = new Meta.Barrier({
|
|
x1: global.stage.width,
|
|
x2: global.stage.width,
|
|
y1: 1,
|
|
y2: global.stage.height,
|
|
directions: Meta.BarrierDirection.NEGATIVE_X,
|
|
});
|
|
}
|
|
|
|
this._pressureBarrier.addBarrier(this._leftBarrier);
|
|
this._pressureBarrier.addBarrier(this._rightBarrier);
|
|
};
|
|
|
|
// Re-create the barriers whenever the stage's allocation is changed.
|
|
this._stageAllocationID = global.stage.connect('notify::allocation', createBarriers);
|
|
createBarriers();
|
|
|
|
// When the pressure barrier is triggered, the corresponding setting is enabled, and a
|
|
// window is currently dragged, we move the dragged window to the adjacent workspace
|
|
// and activate it as well.
|
|
this._pressureBarrier.connect('trigger', () => {
|
|
const direction =
|
|
this._leftBarrier._isHit ? Meta.MotionDirection.LEFT : Meta.MotionDirection.RIGHT;
|
|
|
|
const newWorkspace =
|
|
global.workspace_manager.get_active_workspace().get_neighbor(direction);
|
|
|
|
if (Main.actionMode == Shell.ActionMode.NORMAL && this._draggedWindow &&
|
|
this._settings.get_boolean('enable-desktop-edge-switch')) {
|
|
Main.wm.actionMoveWindow(this._draggedWindow, newWorkspace);
|
|
} else if (Main.actionMode == Shell.ActionMode.NONE && Main.overview.visible &&
|
|
this._settings.get_boolean('enable-overview-edge-switch')) {
|
|
newWorkspace.activate(global.get_current_time());
|
|
}
|
|
});
|
|
|
|
// Keep a reference to the currently dragged window.
|
|
global.display.connect('grab-op-begin', (d, win, op) => {
|
|
if (op == Meta.GrabOp.MOVING) {
|
|
this._draggedWindow = win;
|
|
}
|
|
});
|
|
|
|
// Release the reference to the currently dragged window.
|
|
global.display.connect('grab-op-end', (d, win, op) => {
|
|
if (op == Meta.GrabOp.MOVING) {
|
|
this._draggedWindow = null;
|
|
}
|
|
});
|
|
|
|
|
|
// -----------------------------------------------------------------------------------
|
|
// ------------------- fix perspective of multi-monitor setups -----------------------
|
|
// -----------------------------------------------------------------------------------
|
|
|
|
// Usually, GNOME Shell uses one central perspective for all monitors combined. This
|
|
// results in a somewhat sheared appearance of the cube on multi-monitor setups where
|
|
// the primary monitor is not in the middle (or cubes are shown on multiple monitors).
|
|
// With the code below, we modify the projection and view matrices for each monitor so
|
|
// that each monitor uses its own central perspective. This seems to be possible on
|
|
// Wayland only. On X11, there's only one set of projection and view matrices for all
|
|
// monitors combined, so we tweak them so that the projection center is in the middle
|
|
// of the primary monitor. So it will at least only look bad on X11 if the cube is
|
|
// shown on all monitors...
|
|
const updateMonitorPerspective = () => {
|
|
// Disable the perspective fixes first...
|
|
this._disablePerspectiveCorrection();
|
|
|
|
// Store this so we do not have to get it too often.
|
|
this._enablePerMonitorPerspective =
|
|
this._settings.get_boolean('per-monitor-perspective') &&
|
|
global.display.get_n_monitors() > 1;
|
|
|
|
// ... and then enable them if required.
|
|
if (this._enablePerMonitorPerspective) {
|
|
this._enablePerspectiveCorrection();
|
|
}
|
|
};
|
|
|
|
this._settings.connect('changed::per-monitor-perspective', updateMonitorPerspective);
|
|
this._monitorsChangedID = global.backend.get_monitor_manager().connect(
|
|
'monitors-changed', updateMonitorPerspective);
|
|
|
|
updateMonitorPerspective();
|
|
}
|
|
|
|
// This function could be called after the extension is uninstalled, disabled in GNOME
|
|
// Tweaks, when you log out or when the screen locks.
|
|
disable() {
|
|
|
|
// Restore the original behavior.
|
|
SwipeTracker.prototype._endGesture = this._origEndGesture;
|
|
WorkspacesView.prototype._updateWorkspacesState = this._origUpdateWorkspacesState;
|
|
WorkspacesView.prototype._getSpacing = this._origGetSpacing;
|
|
WorkspacesView.prototype._updateVisibility = this._origUpdateVisibility;
|
|
WorkspaceAnimationController.prototype._prepareWorkspaceSwitch = this._origPrepSwitch;
|
|
WorkspaceAnimationController.prototype._finishWorkspaceSwitch = this._origFinalSwitch;
|
|
|
|
// Remove all drag-to-rotate gestures.
|
|
this._removeDesktopDragGesture();
|
|
this._removePanelDragGesture();
|
|
this._removeOverviewDragGesture();
|
|
|
|
// Clean up skybox.
|
|
if (this._skybox) {
|
|
this._skybox.destroy();
|
|
this._skybox = null;
|
|
}
|
|
|
|
// Clean up the edge-workspace-switching.
|
|
global.stage.disconnect(this._stageAllocationID);
|
|
|
|
this._pressureBarrier.destroy();
|
|
this._leftBarrier.destroy();
|
|
this._rightBarrier.destroy();
|
|
|
|
this._pressureBarrier = null;
|
|
this._leftBarrier = null;
|
|
this._rightBarrier = null;
|
|
|
|
// Clean up perspective correction.
|
|
this._disablePerspectiveCorrection();
|
|
global.backend.get_monitor_manager().disconnect(this._monitorsChangedID);
|
|
|
|
// Make sure that the settings object is freed.
|
|
this._settings = null;
|
|
}
|
|
|
|
// ----------------------------------------------------------------------- private stuff
|
|
|
|
// Calls inhibit_culling on the given actor and recursively on all mapped children.
|
|
_inhibitCulling(actor) {
|
|
if (actor.mapped) {
|
|
actor.inhibit_culling();
|
|
actor._culling_inhibited_by_desktop_cube = true;
|
|
actor.get_children().forEach(c => this._inhibitCulling(c));
|
|
}
|
|
};
|
|
|
|
// Calls uninhibit_culling on the given actor and recursively on all children. It will
|
|
// only call uninhibit_culling() on those actors which were inhibited before.
|
|
_uninhibitCulling(actor) {
|
|
if (actor._culling_inhibited_by_desktop_cube) {
|
|
delete actor._culling_inhibited_by_desktop_cube;
|
|
actor.uninhibit_culling();
|
|
actor.get_children().forEach(c => this._uninhibitCulling(c));
|
|
}
|
|
};
|
|
|
|
// Returns a value between [0...1] blending between overview (0) and app grid mode (1).
|
|
_getAppDrawerMode(workspacesView) {
|
|
return workspacesView._fitModeAdjustment.value;
|
|
}
|
|
|
|
// Returns a value between [0...1] blending between desktop / app drawer mode (0) and
|
|
// overview mode (1).
|
|
_getOverviewMode(workspacesView) {
|
|
return workspacesView._overviewAdjustment.value -
|
|
2 * this._getAppDrawerMode(workspacesView);
|
|
}
|
|
|
|
// Returns a value between [0...1]. If it's 0, the cube should be unfolded, if it's 1,
|
|
// the cube should be drawn like, well, a cube :).
|
|
_getCubeMode(workspacesView) {
|
|
return 1 - this._getAppDrawerMode(workspacesView)
|
|
}
|
|
|
|
// Returns the angle between consecutive workspaces.
|
|
_getFaceAngle(faceCount) {
|
|
|
|
// With this setting, our "cube" only covers 180°, if there are only two workspaces,
|
|
// it covers 90°. This prevents the affordance that it could be possible to switch
|
|
// from the last ot the first workspace.
|
|
if (this._settings.get_boolean('last-first-gap')) {
|
|
return (faceCount == 2 ? 90 : 180) / (faceCount - 1);
|
|
}
|
|
|
|
// Else the "cube" covers 360°.
|
|
return 360.0 / faceCount;
|
|
}
|
|
|
|
// Returns the z-distance from the cube faces to the rotation pivot.
|
|
_getCenterDist(workspaceWidth, faceAngle) {
|
|
let centerDepth = workspaceWidth / 2;
|
|
if (faceAngle < 180) {
|
|
centerDepth /= Math.tan(faceAngle * 0.5 * Math.PI / 180);
|
|
}
|
|
return centerDepth;
|
|
}
|
|
|
|
// This sorts the given list of children actors (which are supposed to be attached to
|
|
// the same parent) by increasing absolute rotation-y angle. This is used for
|
|
// depth-sorting, as cube faces which are less rotated, are in front of others.
|
|
_depthSortCubeFaces(actors) {
|
|
// First create a copy of the actors list and sort it by decreasing rotation angle.
|
|
const copy = actors.slice();
|
|
copy.sort((a, b) => {
|
|
return Math.abs(b.rotation_angle_y) - Math.abs(a.rotation_angle_y);
|
|
});
|
|
|
|
// Then sort the children actors accordingly.
|
|
const parent = actors[0].get_parent();
|
|
for (let i = 0; i < copy.length; i++) {
|
|
parent.set_child_at_index(copy[i], -1);
|
|
}
|
|
}
|
|
|
|
// This sorts the given list of children actors (which are supposed to be attached to
|
|
// the same parent) by increasing orthogonal distance to the camera. To do this, the
|
|
// camera position is projected onto the plane defined by the actor and the absolute
|
|
// distance from the camera to its projected position is computed. This is used for
|
|
// depth-sorting a list of parallel actors.
|
|
_depthSortWindowActors(actors, monitorIndex) {
|
|
|
|
// Sanity check.
|
|
if (actors.length <= 1) {
|
|
return;
|
|
}
|
|
|
|
// First, compute distance of virtual camera to the front workspace plane.
|
|
const camera = new Graphene.Point3D({
|
|
x: global.stage.width / 2,
|
|
y: global.stage.height / 2,
|
|
z: global.stage.height /
|
|
(2 * Math.tan(global.stage.perspective.fovy / 2 * Math.PI / 180))
|
|
});
|
|
|
|
// All actors are expected to share the same parent.
|
|
const parent = actors[0].get_parent();
|
|
|
|
// If the perspective is corrected for multi-monitor setups, the virtual camera is not
|
|
// in the middle of the stage but rather in front of each monitor.
|
|
if (this._enablePerMonitorPerspective) {
|
|
|
|
let monitor;
|
|
|
|
if (Meta.is_wayland_compositor()) {
|
|
|
|
// On Wayland, each monitor should have its own StageView. Therefore, the virtual
|
|
// camera has been positioned in front of each monitor separately.
|
|
monitor = global.display.get_monitor_geometry(monitorIndex);
|
|
|
|
} else {
|
|
|
|
// On X11, there's only one StageView. We move the virtual camera so that it is in
|
|
// front of the primary monitor.
|
|
monitor =
|
|
global.display.get_monitor_geometry(global.display.get_primary_monitor());
|
|
}
|
|
|
|
camera.x = monitor.x + monitor.width / 2;
|
|
camera.y = monitor.y + monitor.height / 2;
|
|
}
|
|
|
|
// Create a list of the orthogonal distances to the camera for each actor.
|
|
const distances = actors.map((a, i) => {
|
|
// A point on the actor plane.
|
|
const onActor = a.apply_relative_transform_to_point(
|
|
null, new Graphene.Point3D({x: 0, y: 0, z: 0}));
|
|
|
|
// A point one unit above the actor plane.
|
|
const aboveActor = a.apply_relative_transform_to_point(
|
|
null, new Graphene.Point3D({x: 0, y: 0, z: 1000}));
|
|
|
|
// The normal vector on the actor plane.
|
|
const normal = new Graphene.Point3D({
|
|
x: aboveActor.x - onActor.x,
|
|
y: aboveActor.y - onActor.y,
|
|
z: aboveActor.z - onActor.z,
|
|
});
|
|
|
|
const length =
|
|
Math.sqrt(normal.x * normal.x + normal.y * normal.y + normal.z * normal.z);
|
|
normal.x /= length;
|
|
normal.y /= length;
|
|
normal.z /= length;
|
|
|
|
onActor.x -= camera.x;
|
|
onActor.y -= camera.y;
|
|
onActor.z -= camera.z;
|
|
|
|
// Return the length of the projected vector.
|
|
return {
|
|
index: i,
|
|
distance: onActor.x * normal.x + onActor.y * normal.y + onActor.z * normal.z
|
|
};
|
|
});
|
|
|
|
// Sort by decreasing distance.
|
|
distances.sort((a, b) => {
|
|
return Math.abs(b.distance) - Math.abs(a.distance);
|
|
});
|
|
|
|
// Then use this to create a sorted list of actors.
|
|
const copy = distances.map(e => {
|
|
return actors[e.index];
|
|
});
|
|
|
|
// Finally, sort the children actors accordingly.
|
|
for (let i = 0; i < copy.length; i++) {
|
|
parent.set_child_at_index(copy[i], -1);
|
|
}
|
|
}
|
|
|
|
// During rotations, the cube is scaled down and the windows are "exploded". If we
|
|
// are directly facing a cube side, the strengths of both effects are approaching
|
|
// zero. The strengths of both effects are small during horizontal rotations to make
|
|
// workspace-switching not so obtrusive. However, during vertical rotations, the
|
|
// effects are stronger.
|
|
// This method returns two values:
|
|
// result[0]: A translation value by which the cube should be moved backwards.
|
|
// result[1]: A translation value by which windows may be moved away from the cube.
|
|
_getExplodeFactors(hRotation, vRotation, centerDepth, monitorIndex) {
|
|
|
|
// These are zero if we are facing a workspace and one if we look directly at an
|
|
// edge between adjacent workspaces or if the cube is rotated vertically
|
|
// respectively.
|
|
const hFactor = 1.0 - 2.0 * Math.abs(hRotation % 1 - 0.5);
|
|
const vFactor = Math.abs(vRotation);
|
|
|
|
// For horizontal rotations, we want to scale the cube (or rather move it backwards)
|
|
// a tiny bit to reveal a bit of parallax. However, if we have many cube sides, this
|
|
// looks weird, so we reduce the effect there. We use the offset which would make
|
|
// the cube's corners stay behind the original workspace faces during he rotation.
|
|
const monitor = global.display.get_monitor_geometry(monitorIndex);
|
|
const cornerDist =
|
|
Math.sqrt(Math.pow(centerDepth, 2) + Math.pow(monitor.width / 2, 2));
|
|
const hDepthOffset =
|
|
this._settings.get_double('window-parallax') * (cornerDist - centerDepth);
|
|
|
|
// The explode factor is set to the hDepthOffset value to make the front-most
|
|
// window stay at a constant depth.
|
|
const hExplode = hDepthOffset;
|
|
|
|
// For vertical rotations, we move the cube backwards to reveal everything. The
|
|
// maximum explode width is set to half of the workspace size.
|
|
const vExplode = this._settings.get_boolean('do-explode') ?
|
|
Math.max(monitor.width, monitor.height) / 2 :
|
|
0;
|
|
const diameter = 2 * (vExplode + centerDepth);
|
|
const camDist =
|
|
monitor.height / (2 * Math.tan(global.stage.perspective.fovy / 2 * Math.PI / 180));
|
|
const vDepthOffset =
|
|
(1 + PADDING_V_ROTATION) * diameter * camDist / monitor.width - centerDepth;
|
|
|
|
// Use current maximum of both values.
|
|
const depthOffset = Math.max(hFactor * hDepthOffset, vFactor * vDepthOffset);
|
|
const explode = Math.max(hFactor * hExplode, vFactor * vExplode);
|
|
|
|
// Do not explode the cube in app drawer state. The stateAdjustment is...
|
|
// ... 0 on the desktop
|
|
// ... 1 in the window picker
|
|
// ... 2 in the app drawer
|
|
const windowPickerFactor =
|
|
Math.min(1.0, 2.0 - Main.overview._overview.controls._stateAdjustment.value);
|
|
|
|
return [depthOffset * windowPickerFactor, explode * windowPickerFactor];
|
|
}
|
|
|
|
// Usually, GNOME Shell uses one central perspective for all monitors combined. This
|
|
// results in a somewhat sheared appearance of the cube on multi-monitor setups where
|
|
// the primary monitor is not in the middle (or cubes are shown on multiple monitors).
|
|
// With the code below, we modify the projection and view matrices for each monitor so
|
|
// that each monitor uses its own central perspective. This seems to be possible on
|
|
// Wayland only. On X11, there's only one set of projection and view matrices for all
|
|
// monitors combined, so we tweak them so that the projection center is in the middle of
|
|
// the primary monitor. So it will at least only look bad on X11 if the cube is shown on
|
|
// all monitors...
|
|
_enablePerspectiveCorrection() {
|
|
|
|
this._stageBeforeUpdateID = global.stage.connect('before-update', (stage, view) => {
|
|
// Do nothing if neither overview or desktop switcher are shown.
|
|
if (!Main.overview.visible && Main.wm._workspaceAnimation._switchData == null) {
|
|
return;
|
|
}
|
|
|
|
// Usually, the virtual camera is positioned centered in front of the stage. We will
|
|
// move the virtual camera around. These variables will be the new stage-relative
|
|
// coordinates of the virtual camera.
|
|
let cameraX, cameraY;
|
|
|
|
if (Meta.is_wayland_compositor()) {
|
|
|
|
// On Wayland, each monitor has its own StageView. Therefore we can move the
|
|
// virtual camera for each monitor separately.
|
|
cameraX = view.layout.x + view.layout.width / 2;
|
|
cameraY = view.layout.y + view.layout.height / 2;
|
|
|
|
} else {
|
|
|
|
// On X11, there's only one StageView. We move the virtual camera so that it is in
|
|
// front of the current monitor.
|
|
const currentMonitorRect =
|
|
global.display.get_monitor_geometry(global.display.get_current_monitor());
|
|
|
|
cameraX = currentMonitorRect.x + currentMonitorRect.width / 2;
|
|
cameraY = currentMonitorRect.y + currentMonitorRect.height / 2;
|
|
}
|
|
|
|
// This is the offset to the original, centered camera position. Y is flipped due to
|
|
// some negative scaling at some point in Mutter.
|
|
const camOffsetX = stage.width / 2 - cameraX;
|
|
const camOffsetY = cameraY - stage.height / 2;
|
|
|
|
const z_near = stage.perspective.z_near;
|
|
const z_far = stage.perspective.z_far;
|
|
|
|
// The code below is copied from Mutter's Clutter.
|
|
// https://gitlab.gnome.org/GNOME/mutter/-/blob/main/clutter/clutter/clutter-stage.c#L2255
|
|
const A = 0.57735025882720947265625;
|
|
const B = 0.866025388240814208984375;
|
|
const C = 0.86162912845611572265625;
|
|
const D = 0.00872653536498546600341796875;
|
|
|
|
const z_2d = z_near * A * B * C / D + z_near;
|
|
|
|
// The code below is copied from Mutter's Clutter as well.
|
|
// https://gitlab.gnome.org/GNOME/mutter/-/blob/main/clutter/clutter/clutter-stage.c#L2270
|
|
const top = z_near * Math.tan(stage.perspective.fovy * Math.PI / 360.0);
|
|
const left = -top * stage.perspective.aspect;
|
|
const right = top * stage.perspective.aspect;
|
|
const bottom = -top;
|
|
|
|
const left_2d_plane = left / z_near * z_2d;
|
|
const right_2d_plane = right / z_near * z_2d;
|
|
const bottom_2d_plane = bottom / z_near * z_2d;
|
|
const top_2d_plane = top / z_near * z_2d;
|
|
|
|
const width_2d_start = right_2d_plane - left_2d_plane;
|
|
const height_2d_start = top_2d_plane - bottom_2d_plane;
|
|
|
|
const width_scale = width_2d_start / stage.width;
|
|
const height_scale = height_2d_start / stage.height;
|
|
// End of the copy-paste code.
|
|
|
|
// Compute the required offset of the frustum planes at the near plane. This
|
|
// basically updates the projection matrix according to our new camera position.
|
|
const offsetX = camOffsetX * width_scale / z_2d * z_near;
|
|
const offsetY = camOffsetY * height_scale / z_2d * z_near;
|
|
|
|
// Set the new frustum.
|
|
view.get_framebuffer().frustum(left + offsetX, right + offsetX, bottom + offsetY,
|
|
top + offsetY, z_near, z_far);
|
|
|
|
// Translate the virtual camera. This basically updates the view matrix according to
|
|
// our new camera position.
|
|
view.get_framebuffer().push_matrix();
|
|
view.get_framebuffer().translate(camOffsetX * width_scale,
|
|
camOffsetY * height_scale, 0);
|
|
|
|
// If the perspective of each monitor is computed separately, the culling of GNOME
|
|
// Shell does not work anymore as it still uses the original frustum. The only
|
|
// workaround is to disable culling altogether. This will be bad performance-wise,
|
|
// but I do not see an alternative.
|
|
// If the overview is shown, we inhibit culling for the WorkspacesDisplay. If the
|
|
// desktop-workspace-switcher is shown, we inhibit culling for all shown monitor
|
|
// groups.
|
|
if (Main.overview.visible) {
|
|
this._inhibitCulling(Main.overview._overview.controls._workspacesDisplay);
|
|
} else if (Main.wm._workspaceAnimation._switchData) {
|
|
Main.wm._workspaceAnimation._switchData.monitors.forEach(m => {
|
|
this._inhibitCulling(m);
|
|
});
|
|
}
|
|
});
|
|
|
|
// Revert the matrix changes before the update,
|
|
this._stageAfterUpdateID = global.stage.connect('after-update', (stage, view) => {
|
|
// Nothing to do if neither overview or desktop switcher are shown.
|
|
if (!Main.overview.visible && Main.wm._workspaceAnimation._switchData == null) {
|
|
return;
|
|
}
|
|
|
|
view.get_framebuffer().pop_matrix();
|
|
view.get_framebuffer().perspective(stage.perspective.fovy, stage.perspective.aspect,
|
|
stage.perspective.z_near,
|
|
stage.perspective.z_far);
|
|
|
|
// Re-enable culling for all relevant actors.
|
|
if (Main.overview.visible) {
|
|
this._uninhibitCulling(Main.overview._overview.controls._workspacesDisplay);
|
|
} else if (Main.wm._workspaceAnimation._switchData) {
|
|
Main.wm._workspaceAnimation._switchData.monitors.forEach(m => {
|
|
this._uninhibitCulling(m);
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
// Reverts the changes done with the method above.
|
|
_disablePerspectiveCorrection() {
|
|
|
|
if (this._stageBeforeUpdateID) {
|
|
global.stage.disconnect(this._stageBeforeUpdateID);
|
|
this._stageBeforeUpdateID = 0;
|
|
}
|
|
|
|
if (this._stageAfterUpdateID) {
|
|
global.stage.disconnect(this._stageAfterUpdateID);
|
|
this._stageAfterUpdateID = 0;
|
|
}
|
|
}
|
|
|
|
// This creates a custom drag gesture and adds it to the given SwipeTracker. The swipe
|
|
// tracker will now also respond to horizontal drags. The additional gesture also
|
|
// reports vertical drag movements via the "pitch" property. This method returns an
|
|
// object containing the gesture, an St.Adjustment which will contain this pitch value,
|
|
// and a connection ID which is used by _removeDragGesture() to clean up. When the
|
|
// SwipeTracker's gesture ends, the St.Adjustment's value will be eased to zero.
|
|
_addDragGesture(actor, tracker, mode) {
|
|
const gesture = new DragGesture(actor, mode);
|
|
gesture.connect('begin', tracker._beginGesture.bind(tracker));
|
|
gesture.connect('update', tracker._updateGesture.bind(tracker));
|
|
gesture.connect('end', tracker._endTouchGesture.bind(tracker));
|
|
tracker.bind_property('distance', gesture, 'distance',
|
|
GObject.BindingFlags.SYNC_CREATE);
|
|
|
|
// Update the gesture's sensitivity when the corresponding settings value changes.
|
|
this._settings.bind('mouse-rotation-speed', gesture, 'sensitivity',
|
|
Gio.SettingsBindFlags.GET);
|
|
|
|
// Connect the gesture's pitch property to the pitch adjustment.
|
|
gesture.bind_property('pitch', this._pitch, 'value', 0);
|
|
|
|
// Ease the pitch adjustment to zero if the SwipeTracker reports an ended gesture.
|
|
// This ensures that the cube smoothly rotates back when released. The end-signal
|
|
// returns a suitable duration for this, however this depends on the horizontal
|
|
// rotation required to move the cube back. Here, we compute a duration required for
|
|
// the vertical rotation and use the maximum of both values for the final easing.
|
|
const gestureEndID = tracker.connect('end', (g, duration) => {
|
|
this._pitch.remove_transition('value');
|
|
this._pitch.ease(0, {
|
|
duration: Math.max(500 * Math.abs(this._pitch.value), duration),
|
|
mode: Clutter.AnimationMode.EASE_OUT_CUBIC,
|
|
});
|
|
});
|
|
|
|
// We return all things which are required to remove the gesture again. This can be
|
|
// done with the _removeDragGesture() method below.
|
|
return {
|
|
actor: actor,
|
|
tracker: tracker,
|
|
gesture: gesture,
|
|
trackerConnection: gestureEndID
|
|
};
|
|
}
|
|
|
|
// Removes a single-click drag gesture created earlier via _addDragGesture(). The info
|
|
// parameter should be the object returned by _addDragGesture().
|
|
_removeDragGesture(info) {
|
|
info.gesture.destroy();
|
|
info.tracker.disconnect(info.trackerConnection);
|
|
}
|
|
|
|
// Calls _addDragGesture() for the SwipeTracker and actor responsible for
|
|
// workspace-switching in desktop mode when dragging on the background.
|
|
_addDesktopDragGesture() {
|
|
// The SwipeTracker for switching workspaces in desktop mode is created here:
|
|
// https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/main/js/ui/workspaceAnimation.js#L285
|
|
const tracker = Main.wm._workspaceAnimation._swipeTracker;
|
|
let actor = Main.layoutManager._backgroundGroup;
|
|
const mode = Shell.ActionMode.NORMAL;
|
|
|
|
// If not in the overview, you can usually only swipe to adjacent workspaces. This
|
|
// https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/main/js/ui/swipeTracker.js#L633
|
|
// allows us to override this behavior.
|
|
tracker.allowLongSwipes = true;
|
|
|
|
// We have to make the background reactive. Make sure to store the current state so
|
|
// that we can reset it later.
|
|
this._origBackgroundReactivity = actor.reactive;
|
|
actor.reactive = true;
|
|
|
|
this._desktopDragGesture = this._addDragGesture(actor, tracker, mode);
|
|
}
|
|
|
|
// Calls _addDragGesture() for the SwipeTracker and actor responsible for
|
|
// workspace-switching in desktop mode when dragging on the panel.
|
|
_addPanelDragGesture() {
|
|
// The SwipeTracker for switching workspaces in desktop mode is created here:
|
|
// https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/main/js/ui/workspaceAnimation.js#L285
|
|
const tracker = Main.wm._workspaceAnimation._swipeTracker;
|
|
const actor = Main.panel;
|
|
const mode = Shell.ActionMode.NORMAL;
|
|
|
|
// If not in the overview, you can usually only swipe to adjacent workspaces. This
|
|
// https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/main/js/ui/swipeTracker.js#L633
|
|
// allows us to override this behavior.
|
|
tracker.allowLongSwipes = true;
|
|
|
|
// We have to prevent moving fullscreen windows when dragging.
|
|
this._origPanelTryDragWindow = actor._tryDragWindow;
|
|
actor._tryDragWindow = () => Clutter.EVENT_PROPAGATE;
|
|
|
|
this._panelDragGesture = this._addDragGesture(actor, tracker, mode);
|
|
}
|
|
|
|
// Calls _addDragGesture() for the SwipeTracker and actor responsible for
|
|
// workspace-switching in overview mode.
|
|
_addOverviewDragGesture() {
|
|
// The SwipeTracker for switching workspaces in overview mode is created here:
|
|
// https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/main/js/ui/workspacesView.js#L827
|
|
const tracker = Main.overview._overview._controls._workspacesDisplay._swipeTracker;
|
|
const actor = Main.layoutManager.overviewGroup;
|
|
const mode = Shell.ActionMode.OVERVIEW;
|
|
|
|
this._overviewDragGesture = this._addDragGesture(actor, tracker, mode);
|
|
}
|
|
|
|
// Calls _removeDragGesture() for the SwipeTracker and actor responsible for
|
|
// workspace-switching in desktop mode when dragging on the background.
|
|
_removeDesktopDragGesture() {
|
|
if (this._desktopDragGesture) {
|
|
|
|
// Restore original behavior.
|
|
this._desktopDragGesture.tracker.allowLongSwipes = false;
|
|
|
|
// Make sure to restore the original state.
|
|
this._desktopDragGesture.actor.reactive = this._origBackgroundReactivity;
|
|
|
|
this._removeDragGesture(this._desktopDragGesture);
|
|
|
|
delete this._desktopDragGesture;
|
|
}
|
|
}
|
|
|
|
// Calls _removeDragGesture() for the SwipeTracker and actor responsible for
|
|
// workspace-switching in desktop mode when dragging on the panel.
|
|
_removePanelDragGesture() {
|
|
if (this._panelDragGesture) {
|
|
|
|
// Restore original behavior.
|
|
this._panelDragGesture.tracker.allowLongSwipes = false;
|
|
|
|
// Make sure to restore the original state.
|
|
this._panelDragGesture.actor._tryDragWindow = this._origPanelTryDragWindow;
|
|
|
|
this._removeDragGesture(this._panelDragGesture);
|
|
|
|
delete this._panelDragGesture;
|
|
}
|
|
}
|
|
|
|
// Calls _removeDragGesture() for the SwipeTracker and actor responsible for
|
|
// workspace-switching in overview mode.
|
|
_removeOverviewDragGesture() {
|
|
if (this._overviewDragGesture) {
|
|
this._removeDragGesture(this._overviewDragGesture);
|
|
|
|
delete this._overviewDragGesture;
|
|
}
|
|
}
|
|
}
|
|
|
|
// This function is called once when the extension is loaded, not enabled.
|
|
function init() {
|
|
return new Extension();
|
|
}
|