gnome-shell/js/ui/layout.js

1484 lines
48 KiB
JavaScript
Raw Normal View History

// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
import Clutter from 'gi://Clutter';
import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import Meta from 'gi://Meta';
import Shell from 'gi://Shell';
import St from 'gi://St';
import * as Signals from '../misc/signals.js';
import * as Background from './background.js';
import * as BackgroundMenu from './backgroundMenu.js';
import * as DND from './dnd.js';
import * as Main from './main.js';
import * as Params from '../misc/params.js';
import * as Ripples from './ripples.js';
const STARTUP_ANIMATION_TIME = 500;
const BACKGROUND_FADE_ANIMATION_TIME = 1000;
const HOT_CORNER_PRESSURE_THRESHOLD = 100; // pixels
const HOT_CORNER_PRESSURE_TIMEOUT = 1000; // ms
const SCREEN_TRANSITION_DELAY = 250; // ms
const SCREEN_TRANSITION_DURATION = 500; // ms
function isPopupMetaWindow(actor) {
switch (actor.meta_window.get_window_type()) {
case Meta.WindowType.DROPDOWN_MENU:
case Meta.WindowType.POPUP_MENU:
case Meta.WindowType.COMBO:
return true;
default:
return false;
}
}
export const MonitorConstraint = GObject.registerClass({
Properties: {
'primary': GObject.ParamSpec.boolean(
'primary', 'Primary', 'Track primary monitor',
GObject.ParamFlags.READABLE | GObject.ParamFlags.WRITABLE,
false),
'index': GObject.ParamSpec.int(
'index', 'Monitor index', 'Track specific monitor',
GObject.ParamFlags.READABLE | GObject.ParamFlags.WRITABLE,
-1, 64, -1),
'work-area': GObject.ParamSpec.boolean(
'work-area', 'Work-area', 'Track monitor\'s work-area',
GObject.ParamFlags.READABLE | GObject.ParamFlags.WRITABLE,
false),
},
}, class MonitorConstraint extends Clutter.Constraint {
_init(props) {
this._primary = false;
this._index = -1;
this._workArea = false;
super._init(props);
}
get primary() {
return this._primary;
}
set primary(v) {
if (v)
this._index = -1;
this._primary = v;
if (this.actor)
this.actor.queue_relayout();
this.notify('primary');
}
get index() {
return this._index;
}
set index(v) {
this._primary = false;
this._index = v;
if (this.actor)
this.actor.queue_relayout();
this.notify('index');
}
get workArea() {
return this._workArea;
}
set workArea(v) {
if (v === this._workArea)
return;
this._workArea = v;
if (this.actor)
this.actor.queue_relayout();
this.notify('work-area');
}
vfunc_set_actor(actor) {
if (actor) {
if (!this._monitorsChangedId) {
this._monitorsChangedId =
Main.layoutManager.connect('monitors-changed', () => {
this.actor.queue_relayout();
});
}
if (!this._workareasChangedId) {
this._workareasChangedId =
global.display.connect('workareas-changed', () => {
if (this._workArea)
this.actor.queue_relayout();
});
}
} else {
if (this._monitorsChangedId)
Main.layoutManager.disconnect(this._monitorsChangedId);
this._monitorsChangedId = 0;
if (this._workareasChangedId)
global.display.disconnect(this._workareasChangedId);
this._workareasChangedId = 0;
}
super.vfunc_set_actor(actor);
}
vfunc_update_allocation(actor, actorBox) {
if (!this._primary && this._index < 0)
return;
if (!Main.layoutManager.primaryMonitor)
return;
let index;
if (this._primary)
index = Main.layoutManager.primaryIndex;
else
index = Math.min(this._index, Main.layoutManager.monitors.length - 1);
let rect;
if (this._workArea) {
let workspaceManager = global.workspace_manager;
let ws = workspaceManager.get_workspace_by_index(0);
rect = ws.get_work_area_for_monitor(index);
} else {
rect = Main.layoutManager.monitors[index];
}
actorBox.init_rect(rect.x, rect.y, rect.width, rect.height);
}
});
class Monitor {
constructor(index, geometry, geometryScale) {
this.index = index;
this.x = geometry.x;
this.y = geometry.y;
this.width = geometry.width;
this.height = geometry.height;
this.geometry_scale = geometryScale;
}
get inFullscreen() {
return global.display.get_monitor_in_fullscreen(this.index);
}
}
const UiActor = GObject.registerClass(
class UiActor extends St.Widget {
vfunc_get_preferred_width(_forHeight) {
let width = global.stage.width;
return [width, width];
}
vfunc_get_preferred_height(_forWidth) {
let height = global.stage.height;
return [height, height];
}
});
const defaultParams = {
trackFullscreen: false,
affectsStruts: false,
affectsInputRegion: true,
};
export const LayoutManager = GObject.registerClass({
Signals: {
'hot-corners-changed': {},
'startup-complete': {},
'startup-prepared': {},
'monitors-changed': {},
'system-modal-opened': {},
},
}, class LayoutManager extends GObject.Object {
_init() {
super._init();
this._rtl = Clutter.get_default_text_direction() === Clutter.TextDirection.RTL;
this.monitors = [];
this.primaryMonitor = null;
this.primaryIndex = -1;
this.hotCorners = [];
this._keyboardIndex = -1;
this._rightPanelBarrier = null;
this._inOverview = false;
this._updateRegionIdle = 0;
this._trackedActors = [];
this._topActors = [];
this._isPopupWindowVisible = false;
this._startingUp = true;
this._pendingLoadBackground = false;
// Set up stage hierarchy to group all UI actors under one container.
this.uiGroup = new UiActor({name: 'uiGroup'});
this.uiGroup.set_flags(Clutter.ActorFlags.NO_LAYOUT);
global.stage.add_child(this.uiGroup);
global.stage.remove_actor(global.window_group);
this.uiGroup.add_actor(global.window_group);
global.connect('shutdown', () => {
const monitorManager = global.backend.get_monitor_manager();
monitorManager.disconnectObject(this);
const adoptedUiGroupActors = [
global.window_group,
global.top_window_group,
Meta.get_feedback_group_for_display(global.display),
];
for (let adoptedActor of adoptedUiGroupActors) {
this.uiGroup.remove_actor(adoptedActor);
global.stage.add_actor(adoptedActor);
}
this._destroyHotCorners();
this.uiGroup.destroy();
});
// Using addChrome() to add actors to uiGroup will position actors
// underneath the top_window_group.
// To insert actors at the top of uiGroup, we use addTopChrome() or
// add the actor directly using uiGroup.add_actor().
global.stage.remove_actor(global.top_window_group);
this.uiGroup.add_actor(global.top_window_group);
this.overviewGroup = new St.Widget({
name: 'overviewGroup',
visible: false,
reactive: true,
constraints: new Clutter.BindConstraint({
source: this.uiGroup,
coordinate: Clutter.BindCoordinate.ALL,
}),
});
this.addChrome(this.overviewGroup);
this.screenShieldGroup = new St.Widget({
name: 'screenShieldGroup',
visible: false,
clip_to_allocation: true,
layout_manager: new Clutter.BinLayout(),
constraints: new Clutter.BindConstraint({
source: this.uiGroup,
coordinate: Clutter.BindCoordinate.ALL,
}),
});
this.addChrome(this.screenShieldGroup);
this.panelBox = new St.BoxLayout({
name: 'panelBox',
vertical: true,
});
this.addChrome(this.panelBox, {
affectsStruts: true,
trackFullscreen: true,
});
this.panelBox.connect('notify::allocation',
this._panelBoxChanged.bind(this));
this.modalDialogGroup = new St.Widget({
name: 'modalDialogGroup',
layout_manager: new Clutter.BinLayout(),
});
this.uiGroup.add_actor(this.modalDialogGroup);
this.keyboardBox = new St.BoxLayout({
name: 'keyboardBox',
reactive: true,
track_hover: true,
});
this.addTopChrome(this.keyboardBox);
this._keyboardHeightNotifyId = 0;
this.screenshotUIGroup = new St.Widget({
name: 'screenshotUIGroup',
layout_manager: new Clutter.BinLayout(),
});
this.addTopChrome(this.screenshotUIGroup);
// A dummy actor that tracks the mouse or text cursor, based on the
// position and size set in setDummyCursorGeometry.
this.dummyCursor = new St.Widget({width: 0, height: 0, opacity: 0});
this.uiGroup.add_actor(this.dummyCursor);
let feedbackGroup = Meta.get_feedback_group_for_display(global.display);
global.stage.remove_actor(feedbackGroup);
this.uiGroup.add_actor(feedbackGroup);
this._backgroundGroup = new Meta.BackgroundGroup();
global.window_group.add_child(this._backgroundGroup);
global.window_group.set_child_below_sibling(this._backgroundGroup, null);
this._bgManagers = [];
this._interfaceSettings = new Gio.Settings({
schema_id: 'org.gnome.desktop.interface',
});
this._interfaceSettings.connect('changed::enable-hot-corners',
this._updateHotCorners.bind(this));
// Need to update struts on new workspaces when they are added
let workspaceManager = global.workspace_manager;
workspaceManager.connect('notify::n-workspaces',
this._queueUpdateRegions.bind(this));
let display = global.display;
display.connect('restacked',
this._windowsRestacked.bind(this));
display.connect('in-fullscreen-changed',
this._updateFullscreen.bind(this));
const monitorManager = global.backend.get_monitor_manager();
monitorManager.connectObject(
'monitors-changed', this._monitorsChanged.bind(this),
this);
this._monitorsChanged();
this.screenTransition = new ScreenTransition();
this.uiGroup.add_child(this.screenTransition);
this.screenTransition.add_constraint(new Clutter.BindConstraint({
source: this.uiGroup,
coordinate: Clutter.BindCoordinate.ALL,
}));
}
// This is called by Main after everything else is constructed
init() {
Main.sessionMode.connect('updated', this._sessionUpdated.bind(this));
this._loadBackground();
}
showOverview() {
this.overviewGroup.show();
this.screenTransition.hide();
this._inOverview = true;
this._updateVisibility();
}
hideOverview() {
this.overviewGroup.hide();
this.screenTransition.hide();
this._inOverview = false;
this._updateVisibility();
}
_sessionUpdated() {
this._updateVisibility();
this._queueUpdateRegions();
}
_updateMonitors() {
let display = global.display;
this.monitors = [];
let nMonitors = display.get_n_monitors();
for (let i = 0; i < nMonitors; i++) {
this.monitors.push(new Monitor(i,
display.get_monitor_geometry(i),
display.get_monitor_scale(i)));
}
if (nMonitors === 0) {
this.primaryIndex = this.bottomIndex = -1;
} else if (nMonitors === 1) {
this.primaryIndex = this.bottomIndex = 0;
} else {
// If there are monitors below the primary, then we need
// to split primary from bottom.
this.primaryIndex = this.bottomIndex = display.get_primary_monitor();
for (let i = 0; i < this.monitors.length; i++) {
let monitor = this.monitors[i];
if (this._isAboveOrBelowPrimary(monitor)) {
if (monitor.y > this.monitors[this.bottomIndex].y)
this.bottomIndex = i;
}
}
}
if (this.primaryIndex !== -1) {
this.primaryMonitor = this.monitors[this.primaryIndex];
this.bottomMonitor = this.monitors[this.bottomIndex];
if (this._pendingLoadBackground) {
this._loadBackground();
this._pendingLoadBackground = false;
}
} else {
this.primaryMonitor = null;
this.bottomMonitor = null;
}
}
_destroyHotCorners() {
this.hotCorners.forEach(corner => corner?.destroy());
this.hotCorners = [];
}
_updateHotCorners() {
// destroy old hot corners
this._destroyHotCorners();
if (!this._interfaceSettings.get_boolean('enable-hot-corners')) {
this.emit('hot-corners-changed');
return;
}
let size = this.panelBox.height;
// build new hot corners
for (let i = 0; i < this.monitors.length; i++) {
let monitor = this.monitors[i];
let cornerX = this._rtl ? monitor.x + monitor.width : monitor.x;
let cornerY = monitor.y;
let haveTopLeftCorner = true;
if (i !== this.primaryIndex) {
// Check if we have a top left (right for RTL) corner.
// I.e. if there is no monitor directly above or to the left(right)
let besideX = this._rtl ? monitor.x + 1 : cornerX - 1;
let besideY = cornerY;
let aboveX = cornerX;
let aboveY = cornerY - 1;
for (let j = 0; j < this.monitors.length; j++) {
if (i === j)
continue;
let otherMonitor = this.monitors[j];
if (besideX >= otherMonitor.x &&
besideX < otherMonitor.x + otherMonitor.width &&
besideY >= otherMonitor.y &&
besideY < otherMonitor.y + otherMonitor.height) {
haveTopLeftCorner = false;
break;
}
if (aboveX >= otherMonitor.x &&
aboveX < otherMonitor.x + otherMonitor.width &&
aboveY >= otherMonitor.y &&
aboveY < otherMonitor.y + otherMonitor.height) {
haveTopLeftCorner = false;
break;
}
}
}
if (haveTopLeftCorner) {
let corner = new HotCorner(this, monitor, cornerX, cornerY);
corner.setBarrierSize(size);
this.hotCorners.push(corner);
} else {
this.hotCorners.push(null);
}
}
this.emit('hot-corners-changed');
}
_addBackgroundMenu(bgManager) {
BackgroundMenu.addBackgroundMenu(bgManager.backgroundActor, this);
}
_createBackgroundManager(monitorIndex) {
const bgManager = new Background.BackgroundManager({
container: this._backgroundGroup,
layoutManager: this,
monitorIndex,
});
bgManager.connect('changed', this._addBackgroundMenu.bind(this));
this._addBackgroundMenu(bgManager);
return bgManager;
}
_showSecondaryBackgrounds() {
for (let i = 0; i < this.monitors.length; i++) {
if (i !== this.primaryIndex) {
let backgroundActor = this._bgManagers[i].backgroundActor;
backgroundActor.show();
backgroundActor.opacity = 0;
backgroundActor.ease({
opacity: 255,
duration: BACKGROUND_FADE_ANIMATION_TIME,
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
});
}
}
}
_waitLoaded(bgManager) {
return new Promise(resolve => {
const id = bgManager.connect('loaded', () => {
bgManager.disconnect(id);
resolve();
});
});
}
_updateBackgrounds() {
for (let i = 0; i < this._bgManagers.length; i++)
this._bgManagers[i].destroy();
this._bgManagers = [];
if (Main.sessionMode.isGreeter)
return Promise.resolve();
for (let i = 0; i < this.monitors.length; i++) {
let bgManager = this._createBackgroundManager(i);
this._bgManagers.push(bgManager);
if (i !== this.primaryIndex && this._startingUp)
bgManager.backgroundActor.hide();
}
return Promise.all(this._bgManagers.map(this._waitLoaded));
}
_updateKeyboardBox() {
this.keyboardBox.set_position(
this.keyboardMonitor.x,
this.keyboardMonitor.y + this.keyboardMonitor.height);
this.keyboardBox.set_size(this.keyboardMonitor.width, -1);
}
_updateBoxes() {
if (!this.primaryMonitor)
return;
this.panelBox.set_position(this.primaryMonitor.x, this.primaryMonitor.y);
this.panelBox.set_size(this.primaryMonitor.width, -1);
this.keyboardIndex = this.primaryIndex;
}
_panelBoxChanged() {
this._updatePanelBarrier();
let size = this.panelBox.height;
this.hotCorners.forEach(corner => {
if (corner)
corner.setBarrierSize(size);
});
}
_updatePanelBarrier() {
if (this._rightPanelBarrier) {
this._rightPanelBarrier.destroy();
this._rightPanelBarrier = null;
}
if (!this.primaryMonitor)
return;
if (this.panelBox.height) {
let primary = this.primaryMonitor;
this._rightPanelBarrier = new Meta.Barrier({
display: global.display,
x1: primary.x + primary.width, y1: primary.y,
x2: primary.x + primary.width, y2: primary.y + this.panelBox.height,
directions: Meta.BarrierDirection.NEGATIVE_X,
});
}
}
_monitorsChanged() {
this._updateMonitors();
this._updateBoxes();
this._updateHotCorners();
this._updateBackgrounds();
this._updateFullscreen();
this._updateVisibility();
this._queueUpdateRegions();
this.emit('monitors-changed');
}
_isAboveOrBelowPrimary(monitor) {
let primary = this.monitors[this.primaryIndex];
let monitorLeft = monitor.x, monitorRight = monitor.x + monitor.width;
let primaryLeft = primary.x, primaryRight = primary.x + primary.width;
if ((monitorLeft >= primaryLeft && monitorLeft < primaryRight) ||
(monitorRight > primaryLeft && monitorRight <= primaryRight) ||
(primaryLeft >= monitorLeft && primaryLeft < monitorRight) ||
(primaryRight > monitorLeft && primaryRight <= monitorRight))
return true;
return false;
}
get currentMonitor() {
let index = global.display.get_current_monitor();
return this.monitors[index];
}
get keyboardMonitor() {
return this.monitors[this.keyboardIndex];
}
get focusIndex() {
let i = Main.layoutManager.primaryIndex;
if (global.stage.key_focus != null)
i = this.findIndexForActor(global.stage.key_focus);
else if (global.display.focus_window != null)
i = global.display.focus_window.get_monitor();
return i;
}
get focusMonitor() {
if (this.focusIndex < 0)
return null;
return this.monitors[this.focusIndex];
}
set keyboardIndex(v) {
this._keyboardIndex = v;
this._updateKeyboardBox();
}
get keyboardIndex() {
return this._keyboardIndex;
}
_loadBackground() {
if (!this.primaryMonitor) {
this._pendingLoadBackground = true;
return;
}
this._systemBackground = new Background.SystemBackground();
this._systemBackground.hide();
global.stage.insert_child_below(this._systemBackground, null);
const constraint = new Clutter.BindConstraint({
source: global.stage,
coordinate: Clutter.BindCoordinate.ALL,
});
this._systemBackground.add_constraint(constraint);
let signalId = this._systemBackground.connect('loaded', () => {
this._systemBackground.disconnect(signalId);
// We're mostly prepared for the startup animation
// now, but since a lot is going on asynchronously
// during startup, let's defer the startup animation
// until the event loop is uncontended and idle.
// This helps to prevent us from running the animation
// when the system is bogged down
const id = GLib.idle_add(GLib.PRIORITY_LOW, () => {
if (this.primaryMonitor) {
this._systemBackground.show();
global.stage.show();
this._prepareStartupAnimation().catch(logError);
return GLib.SOURCE_REMOVE;
} else {
return GLib.SOURCE_CONTINUE;
}
});
GLib.Source.set_name_by_id(id, '[gnome-shell] Startup Animation');
});
}
// Startup Animations
//
// We have two different animations, depending on whether we're a greeter
// or a normal session.
//
// In the greeter, we want to animate the panel from the top, and smoothly
// fade the login dialog on top of whatever plymouth left on screen which
// we get as a still frame background before drawing anything else.
//
// Here we just have the code to animate the panel, and fade up the background.
// The login dialog animation is handled by modalDialog.js
//
// When starting a normal user session, we want to grow it out of the middle
// of the screen.
async _prepareStartupAnimation() {
// During the initial transition, add a simple actor to block all events,
// so they don't get delivered to X11 windows that have been transformed.
this._coverPane = new Clutter.Actor({
opacity: 0,
width: global.screen_width,
height: global.screen_height,
reactive: true,
});
this.addChrome(this._coverPane);
// Force an update of the regions before we scale the UI group to
// get the correct allocation for the struts.
// Do this even when we don't animate on restart, so that maximized
// windows restore to the right size.
this._updateRegions();
if (Meta.is_restart()) {
// On restart, we don't do an animation.
} else if (Main.sessionMode.isGreeter) {
this.panelBox.translation_y = -this.panelBox.height;
} else {
this.keyboardBox.hide();
let monitor = this.primaryMonitor;
if (!Main.sessionMode.hasOverview) {
const x = monitor.x + monitor.width / 2.0;
const y = monitor.y + monitor.height / 2.0;
this.uiGroup.set_pivot_point(
x / global.screen_width,
y / global.screen_height);
this.uiGroup.scale_x = this.uiGroup.scale_y = 0.75;
this.uiGroup.opacity = 0;
}
global.window_group.set_clip(monitor.x, monitor.y, monitor.width, monitor.height);
await this._updateBackgrounds();
}
// Hack: Work around grab issue when testing greeter UI in nested
if (GLib.getenv('GDM_GREETER_TEST') === '1')
setTimeout(() => this.emit('startup-prepared'), 200);
else
this.emit('startup-prepared');
this._startupAnimation();
}
_startupAnimation() {
if (Meta.is_restart())
this._startupAnimationComplete();
else if (Main.sessionMode.isGreeter)
this._startupAnimationGreeter();
else
this._startupAnimationSession();
}
_startupAnimationGreeter() {
this.panelBox.ease({
translation_y: 0,
duration: STARTUP_ANIMATION_TIME,
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
onStopped: () => this._startupAnimationComplete(),
});
}
_startupAnimationSession() {
const onStopped = () => this._startupAnimationComplete();
if (Main.sessionMode.hasOverview) {
Main.overview.runStartupAnimation(onStopped);
} else {
this.uiGroup.ease({
scale_x: 1,
scale_y: 1,
opacity: 255,
duration: STARTUP_ANIMATION_TIME,
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
onStopped,
});
}
}
_startupAnimationComplete() {
this._coverPane.destroy();
this._coverPane = null;
this._systemBackground.destroy();
this._systemBackground = null;
this._startingUp = false;
this.keyboardBox.show();
if (!Main.sessionMode.isGreeter) {
this._showSecondaryBackgrounds();
global.window_group.remove_clip();
}
this._queueUpdateRegions();
this.emit('startup-complete');
}
// setDummyCursorGeometry:
//
// The cursor dummy is a standard widget commonly used for popup
// menus and box pointers to track, as the box pointer API only
// tracks actors. If you want to pop up a menu based on where the
// user clicked, or where the text cursor is, the cursor dummy
// is what you should use. Given that the menu should not track
// the actual mouse pointer as it moves, you need to call this
// function before you show the menu to ensure it is at the right
// position and has the right size.
setDummyCursorGeometry(x, y, w, h) {
this.dummyCursor.set_position(Math.round(x), Math.round(y));
this.dummyCursor.set_size(Math.round(w), Math.round(h));
}
// addChrome:
// @actor: an actor to add to the chrome
// @params: (optional) additional params
//
// Adds @actor to the chrome, and (unless %affectsInputRegion in
// @params is %false) extends the input region to include it.
// Changes in @actor's size, position, and visibility will
// automatically result in appropriate changes to the input
// region.
//
// If %affectsStruts in @params is %true (and @actor is along a
// screen edge), then @actor's size and position will also affect
// the window manager struts. Changes to @actor's visibility will
// NOT affect whether or not the strut is present, however.
//
// If %trackFullscreen in @params is %true, the actor's visibility
// will be bound to the presence of fullscreen windows on the same
// monitor (it will be hidden whenever a fullscreen window is visible,
// and shown otherwise)
addChrome(actor, params) {
this.uiGroup.add_actor(actor);
if (this.uiGroup.contains(global.top_window_group))
this.uiGroup.set_child_below_sibling(actor, global.top_window_group);
this._trackActor(actor, params);
}
// addTopChrome:
// @actor: an actor to add to the chrome
// @params: (optional) additional params
//
// Like addChrome(), but adds @actor above all windows, including popups.
addTopChrome(actor, params) {
this.uiGroup.add_actor(actor);
this._trackActor(actor, params);
}
// trackChrome:
// @actor: a descendant of the chrome to begin tracking
// @params: parameters describing how to track @actor
//
// Tells the chrome to track @actor. This can be used to extend the
// struts or input region to cover specific children.
//
// @params can have any of the same values as in addChrome(),
// though some possibilities don't make sense. By default, @actor has
// the same params as its chrome ancestor.
trackChrome(actor, params = {}) {
let ancestor = actor.get_parent();
let index = this._findActor(ancestor);
while (ancestor && index === -1) {
ancestor = ancestor.get_parent();
index = this._findActor(ancestor);
}
let ancestorData = ancestor
? this._trackedActors[index]
: defaultParams;
// We can't use Params.parse here because we want to drop
// the extra values like ancestorData.actor
for (let prop in defaultParams) {
if (!Object.prototype.hasOwnProperty.call(params, prop))
params[prop] = ancestorData[prop];
}
this._trackActor(actor, params);
}
// untrackChrome:
// @actor: an actor previously tracked via trackChrome()
//
// Undoes the effect of trackChrome()
untrackChrome(actor) {
this._untrackActor(actor);
}
// removeChrome:
// @actor: a chrome actor
//
// Removes @actor from the chrome
removeChrome(actor) {
this.uiGroup.remove_actor(actor);
this._untrackActor(actor);
}
_findActor(actor) {
for (let i = 0; i < this._trackedActors.length; i++) {
let actorData = this._trackedActors[i];
if (actorData.actor === actor)
return i;
}
return -1;
}
_trackActor(actor, params) {
if (this._findActor(actor) !== -1)
throw new Error('trying to re-track existing chrome actor');
let actorData = Params.parse(params, defaultParams);
actorData.actor = actor;
actor.connectObject(
'notify::visible', this._queueUpdateRegions.bind(this),
'notify::allocation', this._queueUpdateRegions.bind(this),
'destroy', this._untrackActor.bind(this), this);
// Note that destroying actor will unset its parent, so we don't
// need to connect to 'destroy' too.
this._trackedActors.push(actorData);
this._updateActorVisibility(actorData);
this._queueUpdateRegions();
}
_untrackActor(actor) {
let i = this._findActor(actor);
if (i === -1)
return;
this._trackedActors.splice(i, 1);
actor.disconnectObject(this);
this._queueUpdateRegions();
}
_updateActorVisibility(actorData) {
if (!actorData.trackFullscreen)
return;
let monitor = this.findMonitorForActor(actorData.actor);
actorData.actor.visible = !(global.window_group.visible &&
monitor &&
monitor.inFullscreen);
}
_updateVisibility() {
let windowsVisible = Main.sessionMode.hasWindows && !this._inOverview;
global.window_group.visible = windowsVisible;
global.top_window_group.visible = windowsVisible;
this._trackedActors.forEach(this._updateActorVisibility.bind(this));
}
getWorkAreaForMonitor(monitorIndex) {
// Assume that all workspaces will have the same
// struts and pick the first one.
let workspaceManager = global.workspace_manager;
let ws = workspaceManager.get_workspace_by_index(0);
return ws.get_work_area_for_monitor(monitorIndex);
}
// This call guarantees that we return some monitor to simplify usage of it
// In practice all tracked actors should be visible on some monitor anyway
findIndexForActor(actor) {
let [x, y] = actor.get_transformed_position();
let [w, h] = actor.get_transformed_size();
let rect = new Meta.Rectangle({x, y, width: w, height: h});
return global.display.get_monitor_index_for_rect(rect);
}
findMonitorForActor(actor) {
let index = this.findIndexForActor(actor);
if (index >= 0 && index < this.monitors.length)
return this.monitors[index];
return null;
}
_queueUpdateRegions() {
if (!this._updateRegionIdle) {
const laters = global.compositor.get_laters();
this._updateRegionIdle = laters.add(
Meta.LaterType.BEFORE_REDRAW, this._updateRegions.bind(this));
}
}
_updateFullscreen() {
this._updateVisibility();
this._queueUpdateRegions();
}
_windowsRestacked() {
let changed = false;
if (this._isPopupWindowVisible !== global.top_window_group.get_children().some(isPopupMetaWindow))
changed = true;
if (changed) {
this._updateVisibility();
this._queueUpdateRegions();
}
}
_updateRegions() {
if (this._updateRegionIdle) {
const laters = global.compositor.get_laters();
laters.remove(this._updateRegionIdle);
delete this._updateRegionIdle;
}
let rects = [], struts = [], i;
let isPopupMenuVisible = global.top_window_group.get_children().some(isPopupMetaWindow);
const wantsInputRegion =
!this._startingUp &&
!isPopupMenuVisible &&
Main.modalCount === 0 &&
!Meta.is_wayland_compositor();
for (i = 0; i < this._trackedActors.length; i++) {
let actorData = this._trackedActors[i];
if (!(actorData.affectsInputRegion && wantsInputRegion) && !actorData.affectsStruts)
continue;
let [x, y] = actorData.actor.get_transformed_position();
let [w, h] = actorData.actor.get_transformed_size();
x = Math.round(x);
y = Math.round(y);
w = Math.round(w);
h = Math.round(h);
if (actorData.affectsInputRegion && wantsInputRegion && actorData.actor.get_paint_visibility())
rects.push(new Meta.Rectangle({x, y, width: w, height: h}));
let monitor = null;
if (actorData.affectsStruts)
monitor = this.findMonitorForActor(actorData.actor);
if (monitor) {
// Limit struts to the size of the screen
let x1 = Math.max(x, 0);
let x2 = Math.min(x + w, global.screen_width);
let y1 = Math.max(y, 0);
let y2 = Math.min(y + h, global.screen_height);
// Metacity wants to know what side of the monitor the
// strut is considered to be attached to. First, we find
// the monitor that contains the strut. If the actor is
// only touching one edge, or is touching the entire
// border of that monitor, then it's obvious which side
// to call it. If it's in a corner, we pick a side
// arbitrarily. If it doesn't touch any edges, or it
// spans the width/height across the middle of the
// screen, then we don't create a strut for it at all.
let side;
if (x1 <= monitor.x && x2 >= monitor.x + monitor.width) {
if (y1 <= monitor.y)
side = Meta.Side.TOP;
else if (y2 >= monitor.y + monitor.height)
side = Meta.Side.BOTTOM;
else
continue;
} else if (y1 <= monitor.y && y2 >= monitor.y + monitor.height) {
if (x1 <= monitor.x)
side = Meta.Side.LEFT;
else if (x2 >= monitor.x + monitor.width)
side = Meta.Side.RIGHT;
else
continue;
} else if (x1 <= monitor.x) {
side = Meta.Side.LEFT;
} else if (y1 <= monitor.y) {
side = Meta.Side.TOP;
} else if (x2 >= monitor.x + monitor.width) {
side = Meta.Side.RIGHT;
} else if (y2 >= monitor.y + monitor.height) {
side = Meta.Side.BOTTOM;
} else {
continue;
}
let strutRect = new Meta.Rectangle({x: x1, y: y1, width: x2 - x1, height: y2 - y1});
let strut = new Meta.Strut({rect: strutRect, side});
struts.push(strut);
}
}
if (wantsInputRegion)
global.set_stage_input_region(rects);
this._isPopupWindowVisible = isPopupMenuVisible;
let workspaceManager = global.workspace_manager;
for (let w = 0; w < workspaceManager.n_workspaces; w++) {
let workspace = workspaceManager.get_workspace_by_index(w);
workspace.set_builtin_struts(struts);
}
return GLib.SOURCE_REMOVE;
}
modalEnded() {
// We don't update the stage input region while in a modal,
// so queue an update now.
this._queueUpdateRegions();
}
});
// HotCorner:
//
// This class manages a "hot corner" that can toggle switching to
// overview.
export const HotCorner = GObject.registerClass(
class HotCorner extends Clutter.Actor {
_init(layoutManager, monitor, x, y) {
super._init();
// We use this flag to mark the case where the user has entered the
// hot corner and has not left both the hot corner and a surrounding
// guard area (the "environs"). This avoids triggering the hot corner
// multiple times due to an accidental jitter.
this._entered = false;
this._monitor = monitor;
this._x = x;
this._y = y;
this._setupFallbackCornerIfNeeded(layoutManager);
this._pressureBarrier = new PressureBarrier(
HOT_CORNER_PRESSURE_THRESHOLD,
HOT_CORNER_PRESSURE_TIMEOUT,
Shell.ActionMode.NORMAL | Shell.ActionMode.OVERVIEW);
this._pressureBarrier.connect('trigger', this._toggleOverview.bind(this));
let px = 0.0;
let py = 0.0;
if (Clutter.get_default_text_direction() === Clutter.TextDirection.RTL) {
px = 1.0;
py = 0.0;
}
this._ripples = new Ripples.Ripples(px, py, 'ripple-box');
this._ripples.addTo(layoutManager.uiGroup);
this.connect('destroy', this._onDestroy.bind(this));
}
setBarrierSize(size) {
if (this._verticalBarrier) {
this._pressureBarrier.removeBarrier(this._verticalBarrier);
this._verticalBarrier.destroy();
this._verticalBarrier = null;
}
if (this._horizontalBarrier) {
this._pressureBarrier.removeBarrier(this._horizontalBarrier);
this._horizontalBarrier.destroy();
this._horizontalBarrier = null;
}
if (size > 0) {
if (Clutter.get_default_text_direction() === Clutter.TextDirection.RTL) {
this._verticalBarrier = new Meta.Barrier({
display: global.display,
x1: this._x, x2: this._x, y1: this._y, y2: this._y + size,
directions: Meta.BarrierDirection.NEGATIVE_X,
});
this._horizontalBarrier = new Meta.Barrier({
display: global.display,
x1: this._x - size, x2: this._x, y1: this._y, y2: this._y,
directions: Meta.BarrierDirection.POSITIVE_Y,
});
} else {
this._verticalBarrier = new Meta.Barrier({
display: global.display,
x1: this._x, x2: this._x, y1: this._y, y2: this._y + size,
directions: Meta.BarrierDirection.POSITIVE_X,
});
this._horizontalBarrier = new Meta.Barrier({
display: global.display,
x1: this._x, x2: this._x + size, y1: this._y, y2: this._y,
directions: Meta.BarrierDirection.POSITIVE_Y,
});
}
this._pressureBarrier.addBarrier(this._verticalBarrier);
this._pressureBarrier.addBarrier(this._horizontalBarrier);
}
}
_setupFallbackCornerIfNeeded(layoutManager) {
if (!global.display.supports_extended_barriers()) {
this.set({
name: 'hot-corner-environs',
x: this._x,
y: this._y,
width: 3,
height: 3,
reactive: true,
});
this._corner = new Clutter.Actor({
name: 'hot-corner',
width: 1,
height: 1,
opacity: 0,
reactive: true,
});
this._corner._delegate = this;
this.add_child(this._corner);
layoutManager.addChrome(this);
if (Clutter.get_default_text_direction() === Clutter.TextDirection.RTL) {
this._corner.set_position(this.width - this._corner.width, 0);
this.set_pivot_point(1.0, 0.0);
this.translation_x = -this.width;
} else {
this._corner.set_position(0, 0);
}
this._corner.connect('enter-event',
this._onCornerEntered.bind(this));
this._corner.connect('leave-event',
this._onCornerLeft.bind(this));
}
}
_onDestroy() {
this.setBarrierSize(0);
this._pressureBarrier.destroy();
this._pressureBarrier = null;
this._ripples.destroy();
}
_toggleOverview() {
if (this._monitor.inFullscreen && !Main.overview.visible)
return;
if (Main.overview.shouldToggleByCornerOrButton()) {
Main.overview.toggle();
if (Main.overview.animationInProgress)
this._ripples.playAnimation(this._x, this._y);
}
}
handleDragOver(source, _actor, _x, _y, _time) {
if (source !== Main.xdndHandler)
return DND.DragMotionResult.CONTINUE;
this._toggleOverview();
return DND.DragMotionResult.CONTINUE;
}
_onCornerEntered() {
if (!this._entered) {
this._entered = true;
this._toggleOverview();
}
return Clutter.EVENT_PROPAGATE;
}
_onCornerLeft(actor, event) {
if (event.get_related() !== this)
this._entered = false;
// Consume event, otherwise this will confuse onEnvironsLeft
return Clutter.EVENT_STOP;
}
vfunc_leave_event(event) {
if (event.get_related() !== this._corner)
this._entered = false;
return Clutter.EVENT_PROPAGATE;
}
});
class PressureBarrier extends Signals.EventEmitter {
constructor(threshold, timeout, actionMode) {
super();
this._threshold = threshold;
this._timeout = timeout;
this._actionMode = actionMode;
this._barriers = [];
this._eventFilter = null;
this._isTriggered = false;
this._reset();
}
addBarrier(barrier) {
barrier._pressureHitId = barrier.connect('hit', this._onBarrierHit.bind(this));
barrier._pressureLeftId = barrier.connect('left', this._onBarrierLeft.bind(this));
this._barriers.push(barrier);
}
_disconnectBarrier(barrier) {
barrier.disconnect(barrier._pressureHitId);
barrier.disconnect(barrier._pressureLeftId);
}
removeBarrier(barrier) {
this._disconnectBarrier(barrier);
this._barriers.splice(this._barriers.indexOf(barrier), 1);
}
destroy() {
this._barriers.forEach(this._disconnectBarrier.bind(this));
this._barriers = [];
}
setEventFilter(filter) {
this._eventFilter = filter;
}
_reset() {
this._barrierEvents = [];
this._currentPressure = 0;
this._lastTime = 0;
}
_isHorizontal(barrier) {
return barrier.y1 === barrier.y2;
}
_getDistanceAcrossBarrier(barrier, event) {
if (this._isHorizontal(barrier))
return Math.abs(event.dy);
else
return Math.abs(event.dx);
}
_getDistanceAlongBarrier(barrier, event) {
if (this._isHorizontal(barrier))
return Math.abs(event.dx);
else
return Math.abs(event.dy);
}
_trimBarrierEvents() {
// Events are guaranteed to be sorted in time order from
// oldest to newest, so just look for the first old event,
// and then chop events after that off.
let i = 0;
let threshold = this._lastTime - this._timeout;
while (i < this._barrierEvents.length) {
let [time, distance_] = this._barrierEvents[i];
if (time >= threshold)
break;
i++;
}
let firstNewEvent = i;
for (i = 0; i < firstNewEvent; i++) {
let [time_, distance] = this._barrierEvents[i];
this._currentPressure -= distance;
}
this._barrierEvents = this._barrierEvents.slice(firstNewEvent);
}
_onBarrierLeft(barrier, _event) {
barrier._isHit = false;
if (this._barriers.every(b => !b._isHit)) {
this._reset();
this._isTriggered = false;
}
}
_trigger() {
this._isTriggered = true;
this.emit('trigger');
this._reset();
}
_onBarrierHit(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();
}
}
const ScreenTransition = GObject.registerClass(
class ScreenTransition extends Clutter.Actor {
_init() {
super._init({visible: false});
}
vfunc_hide() {
this.content = null;
super.vfunc_hide();
}
run() {
if (this.visible)
return;
Main.uiGroup.set_child_above_sibling(this, null);
const rect = new imports.gi.cairo.RectangleInt({
x: 0,
y: 0,
width: global.screen_width,
height: global.screen_height,
});
const [, , , scale] = global.stage.get_capture_final_size(rect);
this.content = global.stage.paint_to_content(rect, scale, Clutter.PaintFlag.NO_CURSORS);
this.opacity = 255;
this.show();
this.ease({
opacity: 0,
duration: SCREEN_TRANSITION_DURATION,
delay: SCREEN_TRANSITION_DELAY,
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
onStopped: () => this.hide(),
});
}
});