7097796c3f
Currently, the login animation can occur before the user's wallpaper has been loaded. When this happens, we wind up displaying a solid blue background for half a second or so before the proper background is displayed. This looks jarring and bad. It's great that we can start GNOME quickly, but starting up before the wallpaper is ready is *too* quickly. I've been meaning to fix this since 2014. Better late than never! We can just have BackgroundManager emit a loaded signal the first time it loads its first background, and have the startup animation code wait for that before proceeding. Some of this code is by Florian, who helped with promisifying. Thanks! https://bugzilla.gnome.org/show_bug.cgi?id=734996
1421 lines
48 KiB
JavaScript
1421 lines
48 KiB
JavaScript
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
|
|
/* exported MonitorConstraint, LayoutManager */
|
|
|
|
const { Clutter, Gio, GLib, GObject, Meta, Shell, St } = imports.gi;
|
|
const Signals = imports.signals;
|
|
|
|
const Background = imports.ui.background;
|
|
const BackgroundMenu = imports.ui.backgroundMenu;
|
|
const LoginManager = imports.misc.loginManager;
|
|
|
|
const DND = imports.ui.dnd;
|
|
const Main = imports.ui.main;
|
|
const Params = imports.misc.params;
|
|
const Ripples = imports.ui.ripples;
|
|
|
|
var STARTUP_ANIMATION_TIME = 500;
|
|
var KEYBOARD_ANIMATION_TIME = 150;
|
|
var BACKGROUND_FADE_ANIMATION_TIME = 1000;
|
|
|
|
var HOT_CORNER_PRESSURE_THRESHOLD = 100; // pixels
|
|
var HOT_CORNER_PRESSURE_TIMEOUT = 1000; // 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;
|
|
}
|
|
}
|
|
|
|
var 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');
|
|
}
|
|
|
|
// eslint-disable-next-line camelcase
|
|
get work_area() {
|
|
return this._workArea;
|
|
}
|
|
|
|
// eslint-disable-next-line camelcase
|
|
set work_area(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);
|
|
}
|
|
});
|
|
|
|
var Monitor = 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,
|
|
};
|
|
|
|
var LayoutManager = GObject.registerClass({
|
|
Signals: { 'hot-corners-changed': {},
|
|
'startup-complete': {},
|
|
'startup-prepared': {},
|
|
'monitors-changed': {},
|
|
'system-modal-opened': {},
|
|
'keyboard-visible-changed': { param_types: [GObject.TYPE_BOOLEAN] } },
|
|
}, 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);
|
|
|
|
// 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 });
|
|
this.addChrome(this.overviewGroup);
|
|
|
|
this.screenShieldGroup = new St.Widget({
|
|
name: 'screenShieldGroup',
|
|
visible: false,
|
|
clip_to_allocation: true,
|
|
layout_manager: new Clutter.BinLayout(),
|
|
});
|
|
this.addChrome(this.screenShieldGroup);
|
|
|
|
this.panelBox = new St.BoxLayout({ name: 'panelBox',
|
|
vertical: true });
|
|
this.addChrome(this.panelBox, { affectsStruts: true,
|
|
trackFullscreen: true });
|
|
this.panelBox.connect('allocation-changed',
|
|
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;
|
|
|
|
// 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));
|
|
|
|
let monitorManager = Meta.MonitorManager.get();
|
|
monitorManager.connect('monitors-changed',
|
|
this._monitorsChanged.bind(this));
|
|
this._monitorsChanged();
|
|
|
|
// NVIDIA drivers don't preserve FBO contents across
|
|
// suspend/resume, see
|
|
// https://bugzilla.gnome.org/show_bug.cgi?id=739178
|
|
if (Shell.util_need_background_refresh()) {
|
|
LoginManager.getLoginManager().connect('prepare-for-sleep',
|
|
(lm, suspending) => {
|
|
if (suspending)
|
|
return;
|
|
Meta.Background.refresh_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._inOverview = true;
|
|
this._updateVisibility();
|
|
}
|
|
|
|
hideOverview() {
|
|
this.overviewGroup.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;
|
|
}
|
|
}
|
|
|
|
_updateHotCorners() {
|
|
// destroy old hot corners
|
|
this.hotCorners.forEach(corner => {
|
|
if (corner)
|
|
corner.destroy();
|
|
});
|
|
this.hotCorners = [];
|
|
|
|
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) {
|
|
let 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() {
|
|
this.screenShieldGroup.set_position(0, 0);
|
|
this.screenShieldGroup.set_size(global.screen_width, global.screen_height);
|
|
|
|
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);
|
|
|
|
let 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, () => {
|
|
this._systemBackground.show();
|
|
global.stage.show();
|
|
this._prepareStartupAnimation();
|
|
return GLib.SOURCE_REMOVE;
|
|
});
|
|
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);
|
|
|
|
if (Meta.is_restart()) {
|
|
// On restart, we don't do an animation. Force an update of the
|
|
// regions immediately so that maximized windows restore to the
|
|
// right size taking struts into account.
|
|
this._updateRegions();
|
|
} else if (Main.sessionMode.isGreeter) {
|
|
this.panelBox.translation_y = -this.panelBox.height;
|
|
} else {
|
|
// We need to force an update of the regions now before we scale
|
|
// the UI group to get the correct allocation for the struts.
|
|
this._updateRegions();
|
|
|
|
this.keyboardBox.hide();
|
|
|
|
let monitor = this.primaryMonitor;
|
|
let x = monitor.x + monitor.width / 2.0;
|
|
let 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();
|
|
}
|
|
|
|
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,
|
|
onComplete: () => this._startupAnimationComplete(),
|
|
});
|
|
}
|
|
|
|
_startupAnimationSession() {
|
|
this.uiGroup.ease({
|
|
scale_x: 1,
|
|
scale_y: 1,
|
|
opacity: 255,
|
|
duration: STARTUP_ANIMATION_TIME,
|
|
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
|
|
onComplete: () => this._startupAnimationComplete(),
|
|
});
|
|
}
|
|
|
|
_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');
|
|
}
|
|
|
|
showKeyboard() {
|
|
this.keyboardBox.show();
|
|
this.keyboardBox.ease({
|
|
translation_y: -this.keyboardBox.height,
|
|
opacity: 255,
|
|
duration: KEYBOARD_ANIMATION_TIME,
|
|
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
|
|
onComplete: () => {
|
|
this._showKeyboardComplete();
|
|
},
|
|
});
|
|
this.emit('keyboard-visible-changed', true);
|
|
}
|
|
|
|
_showKeyboardComplete() {
|
|
// Poke Chrome to update the input shape; it doesn't notice
|
|
// anchor point changes
|
|
this._updateRegions();
|
|
|
|
this._keyboardHeightNotifyId = this.keyboardBox.connect('notify::height', () => {
|
|
this.keyboardBox.translation_y = -this.keyboardBox.height;
|
|
});
|
|
}
|
|
|
|
hideKeyboard(immediate) {
|
|
if (this._keyboardHeightNotifyId) {
|
|
this.keyboardBox.disconnect(this._keyboardHeightNotifyId);
|
|
this._keyboardHeightNotifyId = 0;
|
|
}
|
|
this.keyboardBox.ease({
|
|
translation_y: 0,
|
|
opacity: 0,
|
|
duration: immediate ? 0 : KEYBOARD_ANIMATION_TIME,
|
|
mode: Clutter.AnimationMode.EASE_IN_QUAD,
|
|
onComplete: () => {
|
|
this._hideKeyboardComplete();
|
|
},
|
|
});
|
|
|
|
this.emit('keyboard-visible-changed', false);
|
|
}
|
|
|
|
_hideKeyboardComplete() {
|
|
this.keyboardBox.hide();
|
|
this._updateRegions();
|
|
}
|
|
|
|
// 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;
|
|
actorData.visibleId = actor.connect('notify::visible',
|
|
this._queueUpdateRegions.bind(this));
|
|
actorData.allocationId = actor.connect('notify::allocation',
|
|
this._queueUpdateRegions.bind(this));
|
|
actorData.destroyId = actor.connect('destroy',
|
|
this._untrackActor.bind(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;
|
|
let actorData = this._trackedActors[i];
|
|
|
|
this._trackedActors.splice(i, 1);
|
|
actor.disconnect(actorData.visibleId);
|
|
actor.disconnect(actorData.allocationId);
|
|
actor.disconnect(actorData.destroyId);
|
|
|
|
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._startingUp)
|
|
return;
|
|
|
|
if (!this._updateRegionIdle) {
|
|
this._updateRegionIdle = Meta.later_add(Meta.LaterType.BEFORE_REDRAW,
|
|
this._updateRegions.bind(this));
|
|
}
|
|
}
|
|
|
|
_getWindowActorsForWorkspace(workspace) {
|
|
return global.get_window_actors().filter(actor => {
|
|
let win = actor.meta_window;
|
|
return win.located_on_workspace(workspace);
|
|
});
|
|
}
|
|
|
|
_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) {
|
|
Meta.later_remove(this._updateRegionIdle);
|
|
delete this._updateRegionIdle;
|
|
}
|
|
|
|
// No need to update when we have a modal.
|
|
if (Main.modalCount > 0)
|
|
return GLib.SOURCE_REMOVE;
|
|
|
|
let rects = [], struts = [], i;
|
|
let isPopupMenuVisible = global.top_window_group.get_children().some(isPopupMetaWindow);
|
|
let wantsInputRegion = !isPopupMenuVisible;
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
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.
|
|
var 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_anchor_point_from_gravity(Clutter.Gravity.NORTH_EAST);
|
|
} 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()) {
|
|
this._ripples.playAnimation(this._x, this._y);
|
|
Main.overview.toggle();
|
|
}
|
|
}
|
|
|
|
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(crossingEvent) {
|
|
if (crossingEvent.related != this._corner)
|
|
this._entered = false;
|
|
return Clutter.EVENT_PROPAGATE;
|
|
}
|
|
});
|
|
|
|
var PressureBarrier = class PressureBarrier {
|
|
constructor(threshold, timeout, actionMode) {
|
|
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();
|
|
}
|
|
};
|
|
Signals.addSignalMethods(PressureBarrier.prototype);
|