summaryrefslogtreecommitdiffstats
path: root/js/ui/layout.js
diff options
context:
space:
mode:
Diffstat (limited to 'js/ui/layout.js')
-rw-r--r--js/ui/layout.js1409
1 files changed, 1409 insertions, 0 deletions
diff --git a/js/ui/layout.js b/js/ui/layout.js
new file mode 100644
index 0000000..b082ec8
--- /dev/null
+++ b/js/ui/layout.js
@@ -0,0 +1,1409 @@
+// -*- 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 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('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;
+
+ // 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();
+ }
+
+ // 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_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(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);