// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- const Clutter = imports.gi.Clutter; const GLib = imports.gi.GLib; const GObject = imports.gi.GObject; const Lang = imports.lang; const Mainloop = imports.mainloop; const Meta = imports.gi.Meta; const Shell = imports.gi.Shell; const Signals = imports.signals; const St = imports.gi.St; 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 Tweener = imports.ui.tweener; const STARTUP_ANIMATION_TIME = 0.5; const KEYBOARD_ANIMATION_TIME = 0.15; const BACKGROUND_FADE_ANIMATION_TIME = 1.0; const DEFAULT_BACKGROUND_COLOR = Clutter.Color.from_pixel(0x2e3436ff); // The message tray takes this much pressure // in the pressure barrier at once to release it. const MESSAGE_TRAY_PRESSURE_THRESHOLD = 250; // pixels const MESSAGE_TRAY_PRESSURE_TIMEOUT = 1000; // ms const HOT_CORNER_PRESSURE_THRESHOLD = 100; // pixels const 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; } } const MonitorConstraint = new Lang.Class({ Name: 'MonitorConstraint', Extends: Clutter.Constraint, 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)}, _init: function(props) { this._primary = false; this._index = -1; this.parent(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'); }, vfunc_set_actor: function(actor) { if (actor) { if (!this._monitorsChangedId) { this._monitorsChangedId = Main.layoutManager.connect('monitors-changed', Lang.bind(this, function() { this.actor.queue_relayout(); })); } } else { if (this._monitorsChangedId) Main.layoutManager.disconnect(this._monitorsChangedId); this._monitorsChangedId = 0; } this.parent(actor); }, vfunc_update_allocation: function(actor, actorBox) { if (!this._primary && this._index < 0) return; let monitor; if (this._primary) { monitor = Main.layoutManager.primaryMonitor; } else { let index = Math.min(this._index, Main.layoutManager.monitors.length - 1); monitor = Main.layoutManager.monitors[index]; } actorBox.init_rect(monitor.x, monitor.y, monitor.width, monitor.height); } }); const defaultParams = { trackFullscreen: false, affectsStruts: false, affectsInputRegion: true }; const LayoutManager = new Lang.Class({ Name: 'LayoutManager', _init: function () { 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._trayBarrier = null; this._inOverview = false; this._updateRegionIdle = 0; this._trackedActors = []; this._topActors = []; this._isPopupWindowVisible = false; this._startingUp = true; // Normally, the stage is always covered so Clutter doesn't need to clear // it; however it becomes visible during the startup animation // See the comment below for a longer explanation global.stage.color = DEFAULT_BACKGROUND_COLOR; // Set up stage hierarchy to group all UI actors under one container. this.uiGroup = new Shell.GenericContainer({ name: 'uiGroup' }); this.uiGroup.connect('allocate', function (actor, box, flags) { let children = actor.get_children(); for (let i = 0; i < children.length; i++) children[i].allocate_preferred_size(flags); }); this.uiGroup.connect('get-preferred-width', function(actor, forHeight, alloc) { let width = global.stage.width; [alloc.min_size, alloc.natural_size] = [width, width]; }); this.uiGroup.connect('get-preferred-height', function(actor, forWidth, alloc) { let height = global.stage.height; [alloc.min_size, alloc.natural_size] = [height, height]; }); global.stage.remove_actor(global.window_group); this.uiGroup.add_actor(global.window_group); global.stage.remove_actor(global.overlay_group); this.uiGroup.add_actor(global.overlay_group); global.stage.add_child(this.uiGroup); 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', Lang.bind(this, this._panelBoxChanged)); this.trayBox = new St.Widget({ name: 'trayBox', layout_manager: new Clutter.BinLayout() }); this.addChrome(this.trayBox); this._setupTrayPressure(); this.keyboardBox = new St.BoxLayout({ name: 'keyboardBox', reactive: true, track_hover: true }); this.addChrome(this.keyboardBox); this._keyboardHeightNotifyId = 0; global.stage.remove_actor(global.top_window_group); this.uiGroup.add_actor(global.top_window_group); this._backgroundGroup = new Meta.BackgroundGroup(); global.window_group.add_child(this._backgroundGroup); this._backgroundGroup.lower_bottom(); this._bgManagers = []; // This blocks the XDND picks from finding the activities button // and we never attempt to pick anything from it anyway so make // it invisible from picks Shell.util_set_hidden_from_pick(global.top_window_group, true); // Need to update struts on new workspaces when they are added global.screen.connect('notify::n-workspaces', Lang.bind(this, this._queueUpdateRegions)); global.screen.connect('restacked', Lang.bind(this, this._windowsRestacked)); global.screen.connect('monitors-changed', Lang.bind(this, this._monitorsChanged)); this._monitorsChanged(); }, // This is called by Main after everything else is constructed; // it needs access to Main.overview, which didn't exist // yet when the LayoutManager was constructed. init: function() { Main.overview.connect('showing', Lang.bind(this, this._overviewShowing)); Main.overview.connect('hidden', Lang.bind(this, this._overviewHidden)); Main.sessionMode.connect('updated', Lang.bind(this, this._sessionUpdated)); this._prepareStartupAnimation(); }, _overviewShowing: function() { this._inOverview = true; this._updateVisibility(); this._queueUpdateRegions(); }, _overviewHidden: function() { this._inOverview = false; this._updateVisibility(); this._queueUpdateRegions(); }, _sessionUpdated: function() { this._updateVisibility(); this._queueUpdateRegions(); }, _updateMonitors: function() { let screen = global.screen; this.monitors = []; let nMonitors = screen.get_n_monitors(); for (let i = 0; i < nMonitors; i++) this.monitors.push(screen.get_monitor_geometry(i)); 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 = screen.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; } } } this.primaryMonitor = this.monitors[this.primaryIndex]; this.bottomMonitor = this.monitors[this.bottomIndex]; }, _updateHotCorners: function() { // destroy old hot corners for (let i = 0; i < this.hotCorners.length; i++) this.hotCorners[i].destroy(); this.hotCorners = []; 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; if (i != this.primaryIndex) { let haveTopLeftCorner = true; // 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) continue; } let corner = new HotCorner(this, monitor, cornerX, cornerY); corner.setBarrierSize(size); this.hotCorners.push(corner); } this.emit('hot-corners-changed'); }, _createBackground: function(monitorIndex) { let bgManager = new Background.BackgroundManager({ container: this._backgroundGroup, layoutManager: this, monitorIndex: monitorIndex }); BackgroundMenu.addBackgroundMenu(bgManager.background.actor); bgManager.connect('changed', Lang.bind(this, function() { BackgroundMenu.addBackgroundMenu(bgManager.background.actor); })); this._bgManagers.push(bgManager); return bgManager.background; }, _createSecondaryBackgrounds: function() { for (let i = 0; i < this.monitors.length; i++) { if (i != this.primaryIndex) { let background = this._createBackground(i); background.actor.opacity = 0; Tweener.addTween(background.actor, { opacity: 255, time: BACKGROUND_FADE_ANIMATION_TIME, transition: 'easeOutQuad' }); } } }, _createPrimaryBackground: function() { this._createBackground(this.primaryIndex); }, _updateBackgrounds: function() { let i; for (i = 0; i < this._bgManagers.length; i++) this._bgManagers[i].destroy(); this._bgManagers = []; if (Main.sessionMode.isGreeter) return; if (this._startingUp) return; for (let i = 0; i < this.monitors.length; i++) { this._createBackground(i); } }, _updateBoxes: function() { this.screenShieldGroup.set_position(0, 0); this.screenShieldGroup.set_size(global.screen_width, global.screen_height); this.panelBox.set_position(this.primaryMonitor.x, this.primaryMonitor.y); this.panelBox.set_size(this.primaryMonitor.width, -1); if (this.keyboardIndex < 0) this.keyboardIndex = this.primaryIndex; this.trayBox.set_position(this.bottomMonitor.x, this.bottomMonitor.y + this.bottomMonitor.height); this.trayBox.set_size(this.bottomMonitor.width, -1); }, _panelBoxChanged: function() { this._updatePanelBarrier(); let size = this.panelBox.height; this.hotCorners.forEach(function(corner) { corner.setBarrierSize(size); }); }, _updatePanelBarrier: function() { if (this._rightPanelBarrier) { this._rightPanelBarrier.destroy(); this._rightPanelBarrier = null; } 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 }); } }, _setupTrayPressure: function() { this._trayPressure = new PressureBarrier(MESSAGE_TRAY_PRESSURE_THRESHOLD, MESSAGE_TRAY_PRESSURE_TIMEOUT, Shell.KeyBindingMode.NORMAL | Shell.KeyBindingMode.OVERVIEW); this._trayPressure.setEventFilter(this._trayBarrierEventFilter); this._trayPressure.connect('trigger', function(barrier) { if (Main.layoutManager.bottomMonitor.inFullscreen) return; Main.messageTray.openTray(); }); }, _updateTrayBarrier: function() { let monitor = this.bottomMonitor; if (this._trayBarrier) { this._trayPressure.removeBarrier(this._trayBarrier); this._trayBarrier.destroy(); this._trayBarrier = null; } this._trayBarrier = new Meta.Barrier({ display: global.display, x1: monitor.x, x2: monitor.x + monitor.width, y1: monitor.y + monitor.height, y2: monitor.y + monitor.height, directions: Meta.BarrierDirection.NEGATIVE_Y }); this._trayPressure.addBarrier(this._trayBarrier); }, _trayBarrierEventFilter: function(event) { // Throw out all events where the pointer was grabbed by another // client, as the client that grabbed the pointer expects to have // complete control over it if (event.grabbed && Main.modalCount == 0) return true; return false; }, _monitorsChanged: function() { this._updateMonitors(); this._updateBoxes(); this._updateTrayBarrier(); this._updateHotCorners(); this._updateBackgrounds(); this._updateFullscreen(); this._updateVisibility(); this._queueUpdateRegions(); this.emit('monitors-changed'); }, _isAboveOrBelowPrimary: function(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.screen.get_current_monitor(); return this.monitors[index]; }, get keyboardMonitor() { return this.monitors[this.keyboardIndex]; }, get focusIndex() { let i = Main.layoutManager.primaryIndex; if (global.stage_input_mode == Shell.StageInputMode.FOCUSED || global.stage_input_mode == Shell.StageInputMode.FULLSCREEN) { let focusActor = global.stage.key_focus; if (focusActor) i = this.findIndexForActor(focusActor); } else { let focusWindow = global.display.focus_window; if (focusWindow) i = focusWindow.get_monitor(); } return i; }, get focusMonitor() { return this.monitors[this.focusIndex]; }, set keyboardIndex(v) { this._keyboardIndex = v; this.keyboardBox.set_position(this.keyboardMonitor.x, this.keyboardMonitor.y + this.keyboardMonitor.height); this.keyboardBox.set_size(this.keyboardMonitor.width, -1); }, get keyboardIndex() { return this._keyboardIndex; }, // 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. // // Usually, we don't want to paint the stage background color because the // MetaBackgroundActor inside global.window_group covers the entirety of the // screen. So, we set no_clear_hint at the end of the animation. _prepareStartupAnimation: function() { // Set ourselves to FULLSCREEN input mode while the animation is running // so events don't get delivered to X11 windows (which are distorted by the animation) global.stage_input_mode = Shell.StageInputMode.FULLSCREEN; 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 coorect allocation for the struts. this._updateRegions(); this.trayBox.hide(); 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.5; this.uiGroup.opacity = 0; } this._systemBackground = new Background.SystemBackground(); this._systemBackground.actor.hide(); global.stage.insert_child_below(this._systemBackground.actor, null); let constraint = new Clutter.BindConstraint({ source: global.stage, coordinate: Clutter.BindCoordinate.ALL }); this._systemBackground.actor.add_constraint(constraint); let signalId = this._systemBackground.connect('loaded', Lang.bind(this, function() { this._systemBackground.disconnect(signalId); this._systemBackground.actor.show(); global.stage.show(); this.emit('startup-prepared'); // 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 GLib.idle_add(GLib.PRIORITY_LOW, Lang.bind(this, function() { this._startupAnimation(); return false; })); })); }, _startupAnimation: function() { if (Main.sessionMode.isGreeter) this._startupAnimationGreeter(); else this._startupAnimationSession(); }, _startupAnimationGreeter: function() { Tweener.addTween(this.panelBox, { translation_y: 0, time: STARTUP_ANIMATION_TIME, transition: 'easeOutQuad', onComplete: this._startupAnimationComplete, onCompleteScope: this }); }, _startupAnimationSession: function() { this._createPrimaryBackground(); Tweener.addTween(this.uiGroup, { scale_x: 1, scale_y: 1, opacity: 255, time: STARTUP_ANIMATION_TIME, transition: 'easeOutQuad', onComplete: this._startupAnimationComplete, onCompleteScope: this }); }, _startupAnimationComplete: function() { // At this point, the UI group is covering everything, so // we no longer need to clear the stage global.stage.no_clear_hint = true; global.stage_input_mode = Shell.StageInputMode.NORMAL; this._systemBackground.actor.destroy(); this._systemBackground = null; this._startingUp = false; this.trayBox.show(); this.keyboardBox.show(); if (!Main.sessionMode.isGreeter) this._createSecondaryBackgrounds(); this._queueUpdateRegions(); this.emit('startup-complete'); }, showKeyboard: function () { this.keyboardBox.raise_top(); Tweener.addTween(this.keyboardBox, { anchor_y: this.keyboardBox.height, time: KEYBOARD_ANIMATION_TIME, transition: 'easeOutQuad', onComplete: this._showKeyboardComplete, onCompleteScope: this }); this.emit('keyboard-visible-changed', true); }, _showKeyboardComplete: function() { // Poke Chrome to update the input shape; it doesn't notice // anchor point changes this._updateRegions(); this._keyboardHeightNotifyId = this.keyboardBox.connect('notify::height', Lang.bind(this, function () { this.keyboardBox.anchor_y = this.keyboardBox.height; })); }, hideKeyboard: function (immediate) { if (this._keyboardHeightNotifyId) { this.keyboardBox.disconnect(this._keyboardHeightNotifyId); this._keyboardHeightNotifyId = 0; } Tweener.addTween(this.keyboardBox, { anchor_y: 0, time: immediate ? 0 : KEYBOARD_ANIMATION_TIME, transition: 'easeInQuad', onComplete: this._hideKeyboardComplete, onCompleteScope: this }); this.emit('keyboard-visible-changed', false); }, _hideKeyboardComplete: function() { this._updateRegions(); }, // 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: function(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, which must be a descendant // of an actor added via addChrome(). 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: function(actor, params) { let ancestor = actor.get_parent(); let index = this._findActor(ancestor); while (ancestor && index == -1) { ancestor = ancestor.get_parent(); index = this._findActor(ancestor); } if (!ancestor) throw new Error('actor is not a descendent of a chrome actor'); let ancestorData = this._trackedActors[index]; if (!params) params = {}; // We can't use Params.parse here because we want to drop // the extra values like ancestorData.actor for (let prop in defaultParams) { if (!params.hasOwnProperty(prop)) params[prop] = ancestorData[prop]; } this._trackActor(actor, params); }, // untrackChrome: // @actor: an actor previously tracked via trackChrome() // // Undoes the effect of trackChrome() untrackChrome: function(actor) { this._untrackActor(actor); }, // removeChrome: // @actor: a chrome actor // // Removes @actor from the chrome removeChrome: function(actor) { this.uiGroup.remove_actor(actor); this._untrackActor(actor); }, _findActor: function(actor) { for (let i = 0; i < this._trackedActors.length; i++) { let actorData = this._trackedActors[i]; if (actorData.actor == actor) return i; } return -1; }, _trackActor: function(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.isToplevel = actor.get_parent() == this.uiGroup; actorData.visibleId = actor.connect('notify::visible', Lang.bind(this, this._queueUpdateRegions)); actorData.allocationId = actor.connect('notify::allocation', Lang.bind(this, this._queueUpdateRegions)); actorData.parentSetId = actor.connect('parent-set', Lang.bind(this, this._actorReparented)); // Note that destroying actor will unset its parent, so we don't // need to connect to 'destroy' too. this._trackedActors.push(actorData); this._queueUpdateRegions(); }, _untrackActor: function(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.parentSetId); this._queueUpdateRegions(); }, _actorReparented: function(actor, oldParent) { let newParent = actor.get_parent(); if (!newParent) { this._untrackActor(actor); } else { let i = this._findActor(actor); let actorData = this._trackedActors[i]; actorData.isToplevel = (newParent == this.uiGroup); } }, _updateVisibility: function() { for (let i = 0; i < this._trackedActors.length; i++) { let actorData = this._trackedActors[i], visible; if (!actorData.trackFullscreen) continue; if (!actorData.isToplevel) continue; if (this._inOverview || !Main.sessionMode.hasWindows) visible = true; else if (this.findMonitorForActor(actorData.actor).inFullscreen) visible = false; else visible = true; actorData.actor.visible = visible; } }, getWorkAreaForMonitor: function(monitorIndex) { // Assume that all workspaces will have the same // struts and pick the first one. let ws = global.screen.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: function(actor) { let [x, y] = actor.get_transformed_position(); let [w, h] = actor.get_transformed_size(); let rect = new Meta.Rectangle({ x: x, y: y, width: w, height: h }); return global.screen.get_monitor_index_for_rect(rect); }, findMonitorForActor: function(actor) { return this.monitors[this.findIndexForActor(actor)]; }, _queueUpdateRegions: function() { if (Main.sessionMode.isGreeter) return; if (this._startingUp) return; if (!this._updateRegionIdle) this._updateRegionIdle = Mainloop.idle_add(Lang.bind(this, this._updateRegions), Meta.PRIORITY_BEFORE_REDRAW); }, _getWindowActorsForWorkspace: function(workspace) { return global.get_window_actors().filter(function (actor) { let win = actor.meta_window; return win.located_on_workspace(workspace); }); }, _disconnectTopActor: function(window) { /* O-R status is immutable in Mutter */ if (window.metaWindow.is_override_redirect()) { window.disconnect(window._topActorPositionChangedId); delete window._topActorPositionChangedId; window.disconnect(window._topActorSizeChangedId); delete window._topActorSizeChangedId; } }, _disconnectTopWindow: function(metaWindow) { metaWindow.disconnect(metaWindow._topActorUnmanagedId); delete window._topActorUnmanagedId; metaWindow.disconnect(metaWindow._topActorNotifyFullscreenId); delete window._topActorNotifyFullscreenId; }, _topActorUnmanaged: function(metaWindow, window) { /* Actor is already destroyed, so don't disconnect it */ this._disconnectTopWindow(metaWindow); let i = this._topActors.indexOf(window); this._topActors.splice(i, 1); }, _topActorChanged: function() { this._windowsRestacked(); }, _updateFullscreen: function() { // Ordinary chrome should be visible unless the top window in // the stack is monitor sized. We allow override-redirect // windows to be above this "top window" because there can be // O-R windows that are offscreen, or otherwise have no // semantic meaning to the user. // // If we wanted to be extra clever, we could figure out when // an OVERRIDE_REDIRECT window was trying to partially overlap // us, and then adjust the input region and our clip region // accordingly... // @windows is sorted bottom to top. let windows = this._getWindowActorsForWorkspace(global.screen.get_active_workspace()); // Reset all monitors to not fullscreen for (let i = 0; i < this.monitors.length; i++) this.monitors[i].inFullscreen = false; let oldTopActors = this._topActors; let newTopActors = []; for (let i = windows.length - 1; i > -1; i--) { let window = windows[i]; let metaWindow = window.meta_window; // Because we are working with the list of actors, not the list of // windows, we have an uncomfortable corner case to deal with. // If a window is being hidden, it's actor will be left on top // of the actor stack until any animation is done. (See // meta_compositor_sync_stack()). These windows are actually at // the bottom of the window stack, so if they return and become // relevant again, we'll get another ::restacked signal, so we // can just ignore them. This check *DOES NOT* handle destroyed // windows correctly, but we don't currently animate such windows. // If bugs show up here, instead of making this more complex, // add a function to get the window stack from MetaStackTracker. if (!metaWindow.showing_on_its_workspace ()) continue; newTopActors.push(window); if (metaWindow.is_monitor_sized()) { if (metaWindow.is_screen_sized()) { for (let i = 0; i < this.monitors.length; i++) this.monitors[i].inFullscreen = true; } else { let monitors = metaWindow.get_all_monitors(); for (let i = 0; i < monitors.length; i++) { let index = monitors[i]; this.monitors[index].inFullscreen = true; } } break; } if (!window.is_override_redirect()) break; } // Deal with windows being added or removed from the "top actors" set. // These are the actors that a change to could cause a change in // our computed fullscreen status without a change in the stack. for (let i = 0; i < oldTopActors.length; i++) { let window = oldTopActors[i]; if (newTopActors.indexOf(window) < 0) { this._disconnectTopActor(window); this._disconnectTopWindow(window.metaWindow); } } for (let i = 0; i < newTopActors.length; i++) { let window = newTopActors[i]; if (oldTopActors.indexOf(window) < 0) { window.metaWindow._topActorUnmanagedId = window.metaWindow.connect('unmanaged', Lang.bind(this, this._topActorUnmanaged, window)); window.metaWindow._topActorNotifyFullscreenId = window.metaWindow.connect('notify::fullscreen', Lang.bind(this, this._topActorChanged)); /* In almost all cases, meta_window_is_monitor_size() depends * on position only for O-R windows. The remaining case is a non-OR, * non-fullscreen window which is screen sized. That is highly * unlikely and probably should be excluded in * meta_window_is_monitor_size(). */ if (window.metaWindow.is_override_redirect()) { window._topActorPositionChangedId = window.connect('position-changed', Lang.bind(this, this._topActorChanged)); window._topActorSizeChangedId = window.connect('size-changed', Lang.bind(this, this._topActorChanged)); } } } this._topActors = newTopActors; }, _windowsRestacked: function() { let wasInFullscreen = []; for (let i = 0; i < this.monitors.length; i++) wasInFullscreen[i] = this.monitors[i].inFullscreen; let primaryWasInFullscreen = this.primaryMonitor.inFullscreen; this._updateFullscreen(); let changed = false; for (let i = 0; i < wasInFullscreen.length; i++) { if (wasInFullscreen[i] != this.monitors[i].inFullscreen) { changed = true; break; } } if (!changed && (this._isPopupWindowVisible != global.top_window_group.get_children().some(isPopupMetaWindow))) changed = true; if (changed) { this._updateVisibility(); this._queueUpdateRegions(); } if (primaryWasInFullscreen != this.primaryMonitor.inFullscreen) { let windows = this._getWindowActorsForWorkspace(global.screen.get_active_workspace()); for (let i = 0; i < windows.length; i++) { let window = windows[i]; let metaWindow = window.meta_window; // Skip minimized windows if (!metaWindow.showing_on_its_workspace()) continue; // Skip windows that aren't on the primary monitor if (!metaWindow.is_on_primary_monitor()) continue; // Minimize monitor sized windows that are not focused if (metaWindow.is_monitor_sized() && !metaWindow.appears_focused) metaWindow.minimize(); } } if (changed) this.emit('fullscreen-changed'); }, _updateRegions: function() { let rects = [], struts = [], i; if (this._updateRegionIdle) { Mainloop.source_remove(this._updateRegionIdle); delete this._updateRegionIdle; } 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) { let rect = new Meta.Rectangle({ x: x, y: y, width: w, height: h}); if (actorData.actor.get_paint_visibility() && !this.uiGroup.get_skip_paint(actorData.actor)) rects.push(rect); } if (actorData.affectsStruts) { // 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); // NetWM struts are not really powerful enought to handle // a multi-monitor scenario, they only describe what happens // around the outer sides of the full display region. However // it can describe a partial region along each side, so // we can support having the struts only affect the // primary monitor. This should be enough as we only have // chrome affecting the struts on the primary monitor so // far. // // Metacity wants to know what side of the screen the // strut is considered to be attached to. If the actor is // only touching one edge, or is touching the entire // border of the primary 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; let primary = this.primaryMonitor; if (x1 <= primary.x && x2 >= primary.x + primary.width) { if (y1 <= primary.y) side = Meta.Side.TOP; else if (y2 >= primary.y + primary.height) side = Meta.Side.BOTTOM; else continue; } else if (y1 <= primary.y && y2 >= primary.y + primary.height) { if (x1 <= 0) side = Meta.Side.LEFT; else if (x2 >= primary.x + primary.width) side = Meta.Side.RIGHT; else continue; } else if (x1 <= 0) side = Meta.Side.LEFT; else if (y1 <= 0) side = Meta.Side.TOP; else if (x2 >= global.screen_width) side = Meta.Side.RIGHT; else if (y2 >= global.screen_height) side = Meta.Side.BOTTOM; else continue; // Ensure that the strut rects goes all the way to the screen edge, // as this really what mutter expects. switch (side) { case Meta.Side.TOP: y1 = 0; break; case Meta.Side.BOTTOM: y2 = global.screen_height; break; case Meta.Side.LEFT: x1 = 0; break; case Meta.Side.RIGHT: x2 = global.screen_width; break; } let strutRect = new Meta.Rectangle({ x: x1, y: y1, width: x2 - x1, height: y2 - y1}); let strut = new Meta.Strut({ rect: strutRect, side: side }); struts.push(strut); } } global.set_stage_input_region(rects); this._isPopupWindowVisible = isPopupMenuVisible; let screen = global.screen; for (let w = 0; w < screen.n_workspaces; w++) { let workspace = screen.get_workspace_by_index(w); workspace.set_builtin_struts(struts); } return false; } }); Signals.addSignalMethods(LayoutManager.prototype); // HotCorner: // // This class manages a "hot corner" that can toggle switching to // overview. const HotCorner = new Lang.Class({ Name: 'HotCorner', _init : function(layoutManager, monitor, x, y) { // 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.KeyBindingMode.NORMAL | Shell.KeyBindingMode.OVERVIEW); this._pressureBarrier.connect('trigger', Lang.bind(this, this._toggleOverview)); // Cache the three ripples instead of dynamically creating and destroying them. this._ripple1 = new St.BoxLayout({ style_class: 'ripple-box', opacity: 0, visible: false }); this._ripple2 = new St.BoxLayout({ style_class: 'ripple-box', opacity: 0, visible: false }); this._ripple3 = new St.BoxLayout({ style_class: 'ripple-box', opacity: 0, visible: false }); layoutManager.uiGroup.add_actor(this._ripple1); layoutManager.uiGroup.add_actor(this._ripple2); layoutManager.uiGroup.add_actor(this._ripple3); }, setBarrierSize: function(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) { 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: function(layoutManager) { if (!global.display.supports_extended_barriers()) { this.actor = new Clutter.Actor({ name: 'hot-corner-environs', x: this._x, y: this._y, width: 3, height: 3, reactive: true }); this._corner = new Clutter.Rectangle({ name: 'hot-corner', width: 1, height: 1, opacity: 0, reactive: true }); this._corner._delegate = this; this.actor.add_child(this._corner); layoutManager.addChrome(this.actor); if (Clutter.get_default_text_direction() == Clutter.TextDirection.RTL) { this._corner.set_position(this.actor.width - this._corner.width, 0); this.actor.set_anchor_point_from_gravity(Clutter.Gravity.NORTH_EAST); } else { this._corner.set_position(0, 0); } this.actor.connect('leave-event', Lang.bind(this, this._onEnvironsLeft)); this._corner.connect('enter-event', Lang.bind(this, this._onCornerEntered)); this._corner.connect('leave-event', Lang.bind(this, this._onCornerLeft)); } }, destroy: function() { this.setBarrierSize(0); this._pressureBarrier.destroy(); this._pressureBarrier = null; if (this.actor) this.actor.destroy(); }, _animRipple : function(ripple, delay, time, startScale, startOpacity, finalScale) { // We draw a ripple by using a source image and animating it scaling // outwards and fading away. We want the ripples to move linearly // or it looks unrealistic, but if the opacity of the ripple goes // linearly to zero it fades away too quickly, so we use Tweener's // 'onUpdate' to give a non-linear curve to the fade-away and make // it more visible in the middle section. ripple._opacity = startOpacity; if (ripple.get_text_direction() == Clutter.TextDirection.RTL) ripple.set_anchor_point_from_gravity(Clutter.Gravity.NORTH_EAST); ripple.visible = true; ripple.opacity = 255 * Math.sqrt(startOpacity); ripple.scale_x = ripple.scale_y = startScale; ripple.x = this._x; ripple.y = this._y; Tweener.addTween(ripple, { _opacity: 0, scale_x: finalScale, scale_y: finalScale, delay: delay, time: time, transition: 'linear', onUpdate: function() { ripple.opacity = 255 * Math.sqrt(ripple._opacity); }, onComplete: function() { ripple.visible = false; } }); }, _rippleAnimation: function() { // Show three concentric ripples expanding outwards; the exact // parameters were found by trial and error, so don't look // for them to make perfect sense mathematically // delay time scale opacity => scale this._animRipple(this._ripple1, 0.0, 0.83, 0.25, 1.0, 1.5); this._animRipple(this._ripple2, 0.05, 1.0, 0.0, 0.7, 1.25); this._animRipple(this._ripple3, 0.35, 1.0, 0.0, 0.3, 1); }, _toggleOverview: function() { if (this._monitor.inFullscreen) return; if (Main.overview.shouldToggleByCornerOrButton()) { this._rippleAnimation(); Main.overview.toggle(); } }, handleDragOver: function(source, actor, x, y, time) { if (source != Main.xdndHandler) return DND.DragMotionResult.CONTINUE; return DND.DragMotionResult.CONTINUE; }, _onCornerEntered : function() { if (!this._entered) { this._entered = true; this._toggleOverview(); } return false; }, _onCornerLeft : function(actor, event) { if (event.get_related() != this.actor) this._entered = false; // Consume event, otherwise this will confuse onEnvironsLeft return true; }, _onEnvironsLeft : function(actor, event) { if (event.get_related() != this._corner) this._entered = false; return false; } }); const PressureBarrier = new Lang.Class({ Name: 'PressureBarrier', _init: function(threshold, timeout, keybindingMode) { this._threshold = threshold; this._timeout = timeout; this._keybindingMode = keybindingMode; this._barriers = []; this._eventFilter = null; this._isTriggered = false; this._reset(); }, addBarrier: function(barrier) { barrier._pressureHitId = barrier.connect('hit', Lang.bind(this, this._onBarrierHit)); barrier._pressureLeftId = barrier.connect('left', Lang.bind(this, this._onBarrierLeft)); this._barriers.push(barrier); }, _disconnectBarrier: function(barrier) { barrier.disconnect(barrier._pressureHitId); barrier.disconnect(barrier._pressureLeftId); }, removeBarrier: function(barrier) { this._disconnectBarrier(barrier); this._barriers.splice(this._barriers.indexOf(barrier), 1); }, destroy: function() { this._barriers.forEach(Lang.bind(this, this._disconnectBarrier)); this._barriers = []; }, setEventFilter: function(filter) { this._eventFilter = filter; }, _reset: function() { this._barrierEvents = []; this._currentPressure = 0; this._lastTime = 0; }, _isHorizontal: function(barrier) { return barrier.y1 == barrier.y2; }, _getDistanceAcrossBarrier: function(barrier, event) { if (this._isHorizontal(barrier)) return Math.abs(event.dy); else return Math.abs(event.dx); }, _getDistanceAlongBarrier: function(barrier, event) { if (this._isHorizontal(barrier)) return Math.abs(event.dx); else return Math.abs(event.dy); }, _trimBarrierEvents: function() { // 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: function(barrier, event) { this._reset(); this._isTriggered = false; }, _trigger: function() { this._isTriggered = true; this.emit('trigger'); this._reset(); }, _onBarrierHit: function(barrier, event) { // 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._keybindingMode & Main.keybindingMode)) 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);