diff --git a/files/usr/share/cinnamon/applets/cornerbar@cinnamon.org/applet.js b/files/usr/share/cinnamon/applets/cornerbar@cinnamon.org/applet.js index d1439f50d3..0afa175d7b 100644 --- a/files/usr/share/cinnamon/applets/cornerbar@cinnamon.org/applet.js +++ b/files/usr/share/cinnamon/applets/cornerbar@cinnamon.org/applet.js @@ -278,13 +278,11 @@ class CinnamonBarApplet extends Applet.Applet { } expo() { - if (!Main.expo.animationInProgress) - Main.expo.toggle(); + Main.expo.toggle(); } scale() { - if (!Main.overview.animationInProgress) - Main.overview.toggle(); + Main.overview.toggle(); } } diff --git a/files/usr/share/cinnamon/applets/expo@cinnamon.org/applet.js b/files/usr/share/cinnamon/applets/expo@cinnamon.org/applet.js index f37227e102..6706dcd551 100644 --- a/files/usr/share/cinnamon/applets/expo@cinnamon.org/applet.js +++ b/files/usr/share/cinnamon/applets/expo@cinnamon.org/applet.js @@ -36,8 +36,7 @@ class CinnamonExpoApplet extends Applet.IconApplet { } doAction() { - if (!Main.expo.animationInProgress) - Main.expo.toggle(); + Main.expo.toggle(); } } diff --git a/files/usr/share/cinnamon/applets/scale@cinnamon.org/applet.js b/files/usr/share/cinnamon/applets/scale@cinnamon.org/applet.js index 40dd04ec64..d947585967 100644 --- a/files/usr/share/cinnamon/applets/scale@cinnamon.org/applet.js +++ b/files/usr/share/cinnamon/applets/scale@cinnamon.org/applet.js @@ -36,8 +36,7 @@ class CinnamonScaleApplet extends Applet.IconApplet { } doAction() { - if (!Main.overview.animationInProgress) - Main.overview.toggle(); + Main.overview.toggle(); } } diff --git a/files/usr/share/cinnamon/applets/workspace-switcher@cinnamon.org/applet.js b/files/usr/share/cinnamon/applets/workspace-switcher@cinnamon.org/applet.js index 384f38ad1a..c1dc9844ed 100644 --- a/files/usr/share/cinnamon/applets/workspace-switcher@cinnamon.org/applet.js +++ b/files/usr/share/cinnamon/applets/workspace-switcher@cinnamon.org/applet.js @@ -515,8 +515,7 @@ class CinnamonWorkspaceSwitcher extends Applet.Applet { let expoMenuItem = new PopupMenu.PopupIconMenuItem(_("Manage workspaces (Expo)"), "xsi-view-grid-symbolic", St.IconType.SYMBOLIC); expoMenuItem.connect('activate', Lang.bind(this, function() { - if (!Main.expo.animationInProgress) - Main.expo.toggle(); + Main.expo.toggle(); })); this._applet_context_menu.addMenuItem(expoMenuItem); diff --git a/js/misc/signalTracker.js b/js/misc/signalTracker.js index e82be875c0..d17971645b 100644 --- a/js/misc/signalTracker.js +++ b/js/misc/signalTracker.js @@ -1,14 +1,19 @@ /* exported addObjectSignalMethods */ const GObject = imports.gi.GObject; +const Clutter = imports.gi.Clutter; /** * @private * @param {Object} obj - an object - * @returns {bool} - true if obj has a 'destroy' GObject signal + * @returns {bool} - true if obj's 'destroy' signal indicates its own lifecycle + * end (i.e. obj is a Clutter.Actor or subclass). + * + * Some classes (e.g. Cinnamon.WM) declare a 'destroy' signal with + * domain-specific semantics unrelated to the emitter's own lifetime, so we + * only treat the signal as lifecycle for Clutter actors. */ -function _hasDestroySignal(obj) { - return obj instanceof GObject.Object && - GObject.signal_lookup('destroy', obj); +function _hasLifecycleDestroy(obj) { + return obj instanceof Clutter.Actor; } var TransientSignalHolder = GObject.registerClass( @@ -20,7 +25,7 @@ class TransientSignalHolder extends GObject.Object { constructor(owner) { super(); - if (_hasDestroySignal(owner)) + if (_hasLifecycleDestroy(owner)) owner.connectObject('destroy', () => this.destroy(), this); } @@ -84,7 +89,7 @@ class SignalTracker { * @param {Object=} owner - object that owns the tracker */ constructor(owner) { - if (_hasDestroySignal(owner)) + if (_hasLifecycleDestroy(owner)) this._ownerDestroyId = owner.connect_after('destroy', () => this.clear()); this._owner = owner; @@ -152,7 +157,7 @@ class SignalTracker { * @returns {void} */ track(obj, ...handlerIds) { - if (_hasDestroySignal(obj)) + if (_hasLifecycleDestroy(obj)) this._trackDestroy(obj); this._getSignalData(obj).ownerSignals.push(...handlerIds); diff --git a/js/ui/cinnamonDBus.js b/js/ui/cinnamonDBus.js index 086e3ef837..0c866e0644 100644 --- a/js/ui/cinnamonDBus.js +++ b/js/ui/cinnamonDBus.js @@ -429,13 +429,11 @@ var CinnamonDBus = class { } ShowExpo() { - if (!Main.expo.animationInProgress) - Main.expo.toggle(); + Main.expo.toggle(); } ShowOverview() { - if (!Main.overview.animationInProgress) - Main.overview.toggle(); + Main.overview.toggle(); } PushSubprocessResult(process_id, result, success) { diff --git a/js/ui/environment.js b/js/ui/environment.js index 1a2b915e4a..11895062d8 100644 --- a/js/ui/environment.js +++ b/js/ui/environment.js @@ -427,6 +427,21 @@ function init() { _easeActorProperty(this, 'value', target, params); }; + // Cinnamon.get_event_state() is typed against the ClutterEvent union, + // which GJS delivers to signal callbacks. Vfunc overrides instead get + // a boxed subtype struct (ClutterButtonEvent, ClutterKeyEvent, etc.) + // that GJS refuses to marshal into the union — so wrap the call and + // fall back to masking event.modifier_state directly in that case. + const _origGetEventState = Cinnamon.get_event_state; + const _modMask = Clutter.ModifierType.MODIFIER_MASK + & ~Clutter.ModifierType.MOD2_MASK + & ~Clutter.ModifierType.LOCK_MASK; + Cinnamon.get_event_state = function (event) { + if (event instanceof Clutter.Event) + return _origGetEventState(event); + return event.modifier_state & _modMask; + }; + // Work around https://bugzilla.mozilla.org/show_bug.cgi?id=508783 Date.prototype.toLocaleFormat = function(format) { return Cinnamon.util_format_date(format, this.getTime()); diff --git a/js/ui/expo.js b/js/ui/expo.js index a63d778644..eebd8b58b5 100644 --- a/js/ui/expo.js +++ b/js/ui/expo.js @@ -1,15 +1,13 @@ // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- const Clutter = imports.gi.Clutter; +const GObject = imports.gi.GObject; const Meta = imports.gi.Meta; -const Signals = imports.signals; -const Lang = imports.lang; const St = imports.gi.St; const Cinnamon = imports.gi.Cinnamon; const DND = imports.ui.dnd; const Main = imports.ui.main; -const Tweener = imports.ui.tweener; const ExpoThumbnail = imports.ui.expoThumbnail; // *************** @@ -19,20 +17,27 @@ const ExpoThumbnail = imports.ui.expoThumbnail; // Time for initial animation going into Overview mode const ANIMATION_TIME = 200; -function Expo() { - this._init.apply(this, arguments); -} - -Expo.prototype = { - _init : function() { +var Expo = GObject.registerClass({ + Signals: { + 'showing': {}, + 'shown': {}, + 'hiding': {}, + 'hidden': {}, + }, +}, class Expo extends GObject.Object { + _init() { + super._init(); this.visible = false; // animating to overview, in overview, animating out this._shown = false; // show() and not hide() this._modal = false; // have a modal grab + this.animationInProgress = false; + this._hideInProgress = false; + this._activeAnim = null; // { items, direction } when animating clones - Main.layoutManager.connect('monitors-changed', Lang.bind(this, this._relayout)); - }, + Main.layoutManager.connect('monitors-changed', this._relayout.bind(this)); + } - beforeShow: function() { + beforeShow() { // The main BackgroundActor is inside global.window_group which is // hidden when displaying the overview, so we create a new // one. Instances of this class share a single CoglTexture behind the @@ -47,21 +52,21 @@ Expo.prototype = { this._group = new St.Widget({ name: 'expo', reactive: true }); this._group._delegate = this; - this._group.connect('style-changed', - Lang.bind(this, function() { - let node = this._group.get_theme_node(); - let spacing = node.get_length('spacing'); - if (spacing != this._spacing) { - this._spacing = spacing; - this._relayout(); - } - })); + this._group.connect('style-changed', () => { + let node = this._group.get_theme_node(); + let spacing = node.get_length('spacing'); + if (spacing != this._spacing) { + this._spacing = spacing; + this._relayout(); + } + }); - this.visible = false; // animating to overview, in overview, animating out - this._shown = false; // show() and not hide() - this._modal = false; // have a modal grab + this.visible = false; + this._shown = false; + this._modal = false; this.animationInProgress = false; this._hideInProgress = false; + this._activeAnim = null; // During transitions, we raise this to the top to avoid having the overview // area be reactive; it causes too many issues such as double clicks on @@ -73,11 +78,11 @@ Expo.prototype = { this._coverPane = new Clutter.Rectangle({ opacity: 0, reactive: true }); this._group.add_actor(this._coverPane); - this._coverPane.connect('event', Lang.bind(this, function (actor, event) { return true; })); + this._coverPane.connect('event', (actor, event) => Clutter.EVENT_STOP); this._addWorkspaceButton = new St.Button({style_class: 'workspace-add-button'}); this._group.add_actor(this._addWorkspaceButton); - this._addWorkspaceButton.connect('clicked', Lang.bind(this, function () { Main._addWorkspace();})); + this._addWorkspaceButton.connect('clicked', () => { Main._addWorkspace(); }); this._addWorkspaceButton.handleDragOver = function(source, actor, x, y, time) { return source.metaWindow ? DND.DragMotionResult.MOVE_DROP : DND.DragMotionResult.CONTINUE; }; @@ -121,69 +126,64 @@ Expo.prototype = { this._windowCloseArea.hide(); let ctrlAltMask = Clutter.ModifierType.CONTROL_MASK | Clutter.ModifierType.MOD1_MASK; - this._group.connect('key-press-event', - Lang.bind(this, function(actor, event) { - if (this._shown) { - if (this._expo.handleKeyPressEvent(actor, event)) { - return true; - } - let symbol = event.get_key_symbol(); - if (symbol === Clutter.KEY_plus || symbol === Clutter.KEY_Insert) { - this._workspaceOperationPending = true; - } - let modifiers = Cinnamon.get_event_state(event); - if ((symbol === Clutter.KEY_Delete && (modifiers & ctrlAltMask) !== ctrlAltMask) - || symbol === Clutter.KEY_w && modifiers & Clutter.ModifierType.CONTROL_MASK) - { - this._workspaceOperationPending = true; + this._group.connect('key-press-event', (actor, event) => { + if (this._shown) { + if (this._expo.handleKeyPressEvent(actor, event)) { + return Clutter.EVENT_STOP; + } + let symbol = event.get_key_symbol(); + if (symbol === Clutter.KEY_plus || symbol === Clutter.KEY_Insert) { + this._workspaceOperationPending = true; + } + let modifiers = Cinnamon.get_event_state(event); + if ((symbol === Clutter.KEY_Delete && (modifiers & ctrlAltMask) !== ctrlAltMask) + || symbol === Clutter.KEY_w && modifiers & Clutter.ModifierType.CONTROL_MASK) + { + this._workspaceOperationPending = true; + } + if (symbol === Clutter.KEY_Escape) { + if (!this._workspaceOperationPending) { + this.hide(); } - if (symbol === Clutter.KEY_Escape) { - if (!this._workspaceOperationPending) { - this.hide(); - } + this._workspaceOperationPending = false; + return Clutter.EVENT_STOP; + } + } + return Clutter.EVENT_PROPAGATE; + }); + this._group.connect('key-release-event', (actor, event) => { + if (this._shown) { + let symbol = event.get_key_symbol(); + if (symbol === Clutter.KEY_plus || symbol === Clutter.KEY_Insert) { + if (this._workspaceOperationPending) { this._workspaceOperationPending = false; - return true; + Main._addWorkspace(); } + return Clutter.EVENT_STOP; } - return false; - })); - this._group.connect('key-release-event', - Lang.bind(this, function(actor, event) { - if (this._shown) { - let symbol = event.get_key_symbol(); - if (symbol === Clutter.KEY_plus || symbol === Clutter.KEY_Insert) { - if (this._workspaceOperationPending) { - this._workspaceOperationPending = false; - Main._addWorkspace(); - } - return true; - } - let modifiers = Cinnamon.get_event_state(event); - if ((symbol === Clutter.KEY_Delete && (modifiers & ctrlAltMask) !== ctrlAltMask) - || symbol === Clutter.KEY_w && modifiers & Clutter.ModifierType.CONTROL_MASK) - { - if (this._workspaceOperationPending) { - this._workspaceOperationPending = false; - this._expo.removeSelectedWorkspace(); - } - return true; - } - if (symbol === Clutter.KEY_Super_L || symbol === Clutter.KEY_Super_R) { - this.hide(); - return true; + let modifiers = Cinnamon.get_event_state(event); + if ((symbol === Clutter.KEY_Delete && (modifiers & ctrlAltMask) !== ctrlAltMask) + || symbol === Clutter.KEY_w && modifiers & Clutter.ModifierType.CONTROL_MASK) + { + if (this._workspaceOperationPending) { + this._workspaceOperationPending = false; + this._expo.removeSelectedWorkspace(); } + return Clutter.EVENT_STOP; + } + if (symbol === Clutter.KEY_Super_L || symbol === Clutter.KEY_Super_R) { + this.hide(); + return Clutter.EVENT_STOP; } - return false; - })); + } + return Clutter.EVENT_PROPAGATE; + }); this._expo = new ExpoThumbnail.ExpoThumbnailsBox(); - this._group.add_actor(this._expo.actor); + this._group.add_actor(this._expo); this._relayout(); - }, - - init: function() { - }, + } - _relayout: function () { + _relayout() { if (!this._expo) { // This function can be called as a response to the monitors-changed event, // when we're not showing. @@ -222,8 +222,8 @@ Expo.prototype = { this._windowCloseArea.height = node.get_length('height'); this._windowCloseArea.width = node.get_length('width'); - this._expo.actor.set_position(0, 0); - this._expo.actor.set_size((monitorSetting.width - buttonWidth), monitorSetting.height); + this._expo.set_position(0, 0); + this._expo.set_size((monitorSetting.width - buttonWidth), monitorSetting.height); let buttonY = (monitorSetting.height - buttonHeight) / 2; @@ -235,9 +235,9 @@ Expo.prototype = { this._windowCloseArea.set_position((monitorSetting.width - this._windowCloseArea.width) / 2 , monitorSetting.height); this._windowCloseArea.set_size(this._windowCloseArea.width, this._windowCloseArea.height); this._windowCloseArea.raise_top(); - }, + } - _showCloseArea : function() { + _showCloseArea() { let monitorSetting = global.settings.get_boolean('workspace-expo-primary-monitor') ? Main.layoutManager.primaryMonitor : Main.layoutManager.currentMonitor; this._windowCloseArea.show(); this._windowCloseArea.ease({ @@ -245,36 +245,30 @@ Expo.prototype = { duration: Main.animations_enabled ? ANIMATION_TIME : 0, mode: Clutter.AnimationMode.EASE_OUT_QUAD }); - }, + } - _hideCloseArea : function() { + _hideCloseArea() { let monitorSetting = global.settings.get_boolean('workspace-expo-primary-monitor') ? Main.layoutManager.primaryMonitor : Main.layoutManager.currentMonitor; this._windowCloseArea.ease({ y: monitorSetting.height, duration: Main.animations_enabled ? ANIMATION_TIME : 0, mode: Clutter.AnimationMode.EASE_OUT_QUAD }); - }, - - //// Public methods //// + } - // show: - // - // Animates the overview visible and grabs mouse and keyboard input - show : function() { - if (this._shown) + show() { + if (this._shown || this.animationInProgress) return; this.beforeShow(); // Do this manually instead of using _syncInputMode, to handle failure if (!Main.pushModal(this._group, undefined, undefined, Cinnamon.ActionMode.EXPO)) return; this._modal = true; - this._animateVisible(); this._shown = true; + this._animateVisible(); + } - }, - - _animateVisible: function() { + _animateVisible() { if (this.visible || this.animationInProgress) return; @@ -297,50 +291,13 @@ Expo.prototype = { this._addWorkspaceButton.show(); this._expo.show(); - this._expo.connect('drag-begin', Lang.bind(this, this._showCloseArea)); - this._expo.connect('drag-end', Lang.bind(this, this._hideCloseArea)); + this._expo.connectObject( + 'drag-begin', this._showCloseArea.bind(this), + 'drag-end', this._hideCloseArea.bind(this), this); let activeWorkspace = this._expo.lastActiveWorkspace; - let activeWorkspaceActor = activeWorkspace.actor; let monitorSetting = global.settings.get_boolean('workspace-expo-primary-monitor') ? Main.layoutManager.primaryMonitor : Main.layoutManager.currentMonitor; - //We need to allocate activeWorkspace before we begin its clone animation - let allocateID = this._expo.connect('allocated', Lang.bind(this, function() { - this._expo.disconnect(allocateID); - - let clones = []; - Main.layoutManager.monitors.forEach(function(monitor,index) { - let clone = new Clutter.Clone({source: activeWorkspaceActor}); - global.overlay_group.add_actor(clone); - clone.set_clip(monitor.x, monitor.y, monitor.width, monitor.height); - clones.push(clone); - }, this); - - Main.layoutManager.monitors.forEach(function(monitor,index) { - let clone = clones[index]; - clone.ease({ - x: monitorSetting.x + activeWorkspaceActor.allocation.x1, - y: monitorSetting.y + activeWorkspaceActor.allocation.y1, - scale_x: activeWorkspaceActor.get_scale()[0] , - scale_y: activeWorkspaceActor.get_scale()[1], - duration: Main.animations_enabled ? ANIMATION_TIME : 0, - mode: Clutter.AnimationMode.EASE_OUT_QUAD, - onUpdate: (t, timeIndex) => { - clone.get_transition("x")?.set_to(monitorSetting.x + activeWorkspaceActor.allocation.x1); - clone.get_transition("y")?.set_to(monitorSetting.y + activeWorkspaceActor.allocation.y1); - clone.get_transition("scale-x")?.set_to(activeWorkspaceActor.get_scale()[0]); - clone.get_transition("scale-y")?.set_to(activeWorkspaceActor.get_scale()[1]); - }, - onComplete: () => { - global.overlay_group.remove_actor(clone); - clone.destroy(); - if (index == Main.layoutManager.monitors.length - 1) { - this._showDone(); - } - } - }); - }, this); - })); this._gradient.show(); Main.panelManager.disablePanels(); @@ -349,30 +306,61 @@ Expo.prototype = { this._coverPane.raise_top(); this._coverPane.show(); this.emit('showing'); - }, - // hide: - // - // Reverses the effect of show() - hide: function(options) { + if (!Main.animations_enabled) { + this._showDone(); + return; + } + + //We need to allocate activeWorkspace before we begin its clone animation + this._allocateID = this._expo.connect('allocated', () => { + this._expo.disconnect(this._allocateID); + this._allocateID = 0; + + let items = Main.layoutManager.monitors.map(monitor => { + let clone = new Clutter.Clone({source: activeWorkspace}); + global.overlay_group.add_actor(clone); + clone.set_clip(monitor.x, monitor.y, monitor.width, monitor.height); + return { cleanupActor: clone, clone }; + }); + + this._activeAnim = { items, direction: 'show' }; + this._runAnimation(true); + }); + } + + // Return a 0..1 progress value for the currently-running transition on + // one of the active clones, or 1 if nothing is running. Progress is + // unitless so it's invariant to slow-down-factor (which would double- + // scale an elapsed-ms value). + _sampleAnimProgress() { + for (let {clone} of this._activeAnim.items) { + let t = clone.get_transition('x'); + if (t) { + let total = t.get_duration(); + return total > 0 ? Math.min(1, t.get_elapsed_time() / total) : 1; + } + } + return 1; + } + + hide(options) { if (!this._shown) return; - this._animateNotVisible(options); this._shown = false; + this._animateNotVisible(options); this._syncInputMode(); - }, + } - toggle: function() { + toggle() { if (this._shown) this.hide(); else this.show(); - }, - - //// Private methods //// + } - _syncInputMode: function() { + _syncInputMode() { // We delay input mode changes during animation so that when removing the // overview we don't have a problem with the release of a press/release // going to an application. @@ -395,12 +383,29 @@ Expo.prototype = { else if (global.stage_input_mode == Cinnamon.StageInputMode.FULLSCREEN) global.stage_input_mode = Cinnamon.StageInputMode.NORMAL; } - }, + } - _animateNotVisible: function(options) { - if (!this.visible || this.animationInProgress) + _animateNotVisible(options) { + // Cancel-before-allocated: _animateVisible set up the expo but is still + // waiting on _expo::allocated to build clones. Disconnect the pending + // handler and fall through to the snap-hide path below — we can't run + // a hide animation because the workspace actor isn't allocated yet. + let snapToHidden = false; + if (this._allocateID) { + this._expo.disconnect(this._allocateID); + this._allocateID = 0; + this.animationInProgress = false; + snapToHidden = true; + } + + // Cancel-during-show: reverse the in-progress show instead of bailing. + if (this.animationInProgress && !this._hideInProgress && this._activeAnim) { + this._reverseShowToHide(options); return; + } + if (!this.visible || this.animationInProgress) + return; let activeWorkspace = this._expo.lastActiveWorkspace; @@ -409,9 +414,7 @@ Expo.prototype = { activeWorkspace.overviewModeOff(true, true); } - let animate = Main.animations_enabled; - - if (!animate) { + if (snapToHidden || !Main.animations_enabled) { this._group.hide(); this._hideDone(); return; @@ -420,63 +423,142 @@ Expo.prototype = { this.animationInProgress = true; this._hideInProgress = true; - let activeWorkspaceActor = activeWorkspace.actor; let monitorSetting = global.settings.get_boolean('workspace-expo-primary-monitor') ? Main.layoutManager.primaryMonitor : Main.layoutManager.currentMonitor; - Main.layoutManager.monitors.forEach(function(monitor,index) { + let items = Main.layoutManager.monitors.map(monitor => { let cover = new Clutter.Group(); global.overlay_group.add_actor(cover); cover.set_position(0, 0); cover.set_clip(monitor.x, monitor.y, monitor.width, monitor.height); - let clone = new Clutter.Clone({source: activeWorkspaceActor}); + let clone = new Clutter.Clone({source: activeWorkspace}); cover.add_actor(clone); - clone.set_position(monitorSetting.x + activeWorkspaceActor.allocation.x1, monitorSetting.y + activeWorkspaceActor.allocation.y1); + clone.set_position(monitorSetting.x + activeWorkspace.allocation.x1, monitorSetting.y + activeWorkspace.allocation.y1); clone.set_clip(monitor.x, monitor.y, monitor.width, monitor.height); - clone.set_scale(activeWorkspaceActor.get_scale()[0], activeWorkspaceActor.get_scale()[1]); - - clone.ease({ - x: 0, - y: 0, - scale_x: 1, - scale_y: 1, - duration: Main.animations_enabled ? ANIMATION_TIME : 0, + clone.set_scale(activeWorkspace.get_scale()[0], activeWorkspace.get_scale()[1]); + + return { cleanupActor: cover, clone }; + }); + + this._activeAnim = { items, direction: 'hide' }; + this._runAnimation(false); + + this.emit('hiding'); + } + + _reverseShowToHide(options) { + let activeWorkspace = this._expo.lastActiveWorkspace; + + // Sample the current show's progress so the reversed hide unwinds + // in proportion to how far the show actually got. + let progress = this._sampleAnimProgress(); + + if (!options || !options.toScale) { + Main.panelManager.enablePanels(); + activeWorkspace.overviewModeOff(true, true); + } + + this._hideInProgress = true; + this._activeAnim = { items: this._activeAnim.items, direction: 'hide' }; + + this.emit('hiding'); + + this._runAnimation(false, progress); + } + + _runAnimation(toShow, reverseProgress = null) { + let activeWorkspace = this._expo.lastActiveWorkspace; + let monitorSetting = global.settings.get_boolean('workspace-expo-primary-monitor') ? Main.layoutManager.primaryMonitor : Main.layoutManager.currentMonitor; + + // On reversal, drop any items whose cleanupActor was already destroyed + // by a previously-completed ease — re-easing a destroyed clone would + // crash or no-op silently. + this._activeAnim.items = this._activeAnim.items.filter( + ({cleanupActor}) => cleanupActor.get_parent() !== null); + + let myAnim = this._activeAnim; + let items = myAnim.items; + let total = items.length; + let completed = 0; + + const finalize = () => { + if (this._activeAnim !== myAnim) + return; + this._activeAnim = null; + if (toShow) { + this._showDone(); + } else { + this._group.hide(); + this._hideDone(); + } + }; + + if (total === 0) { + finalize(); + return; + } + + let duration = reverseProgress !== null + ? Math.max(1, ANIMATION_TIME * reverseProgress) + : ANIMATION_TIME; + + items.forEach(({cleanupActor, clone}) => { + let easeParams = { + duration, mode: Clutter.AnimationMode.EASE_OUT_QUAD, - onComplete: () => { - global.overlay_group.remove_actor(cover); - cover.destroy(); - if (index == Main.layoutManager.monitors.length - 1) { - this._group.hide(); - this._hideDone(); + // onStopped (not onComplete) so an interrupted transition still + // advances the counter. + onStopped: (isFinished) => { + if (!isFinished && this._activeAnim !== myAnim) + return; + if (cleanupActor.get_parent() !== null) { + global.overlay_group.remove_actor(cleanupActor); + cleanupActor.destroy(); } + completed++; + if (completed === total) + finalize(); } - }); - }, this); + }; - this.emit('hiding'); - }, + if (toShow) { + easeParams.x = monitorSetting.x + activeWorkspace.allocation.x1; + easeParams.y = monitorSetting.y + activeWorkspace.allocation.y1; + easeParams.scale_x = activeWorkspace.get_scale()[0]; + easeParams.scale_y = activeWorkspace.get_scale()[1]; + easeParams.onUpdate = () => { + clone.get_transition("x")?.set_to(monitorSetting.x + activeWorkspace.allocation.x1); + clone.get_transition("y")?.set_to(monitorSetting.y + activeWorkspace.allocation.y1); + clone.get_transition("scale-x")?.set_to(activeWorkspace.get_scale()[0]); + clone.get_transition("scale-y")?.set_to(activeWorkspace.get_scale()[1]); + }; + } else { + easeParams.x = 0; + easeParams.y = 0; + easeParams.scale_x = 1; + easeParams.scale_y = 1; + } - _showDone: function() { + clone.ease(easeParams); + }); + } + + _showDone() { this.animationInProgress = false; this._coverPane.hide(); this.emit('shown'); - // Handle any calls to hide* while we were showing - if (!this._shown) - this._animateNotVisible(); this._syncInputMode(); global.sync_pointer(); - }, + } - _hideDone: function() { - // Re-enable unredirection + _hideDone() { Meta.enable_unredirect_for_display(global.display); global.window_group.show(); this._expo.hide(); - this._expo = null; this._addWorkspaceButton.hide(); this._windowCloseArea.hide(); @@ -490,15 +572,13 @@ Expo.prototype = { this._coverPane.hide(); this.emit('hidden'); - // Handle any calls to show* while we were hiding - // if (this._shown) - // this._animateVisible(); this._syncInputMode(); global.overlay_group.remove_actor(this._group); this._group.destroy(); this._group = null; + this._expo = null; global.overlay_group.remove_actor(this._background); this._background.destroy(); @@ -506,5 +586,4 @@ Expo.prototype = { Main.layoutManager._chrome.updateRegions(); } -}; -Signals.addSignalMethods(Expo.prototype); +}); diff --git a/js/ui/expoThumbnail.js b/js/ui/expoThumbnail.js index 86581f27b1..7db14b20a1 100644 --- a/js/ui/expoThumbnail.js +++ b/js/ui/expoThumbnail.js @@ -1,19 +1,17 @@ // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- const Clutter = imports.gi.Clutter; -const Lang = imports.lang; -const Mainloop = imports.mainloop; +const GLib = imports.gi.GLib; +const GObject = imports.gi.GObject; const Meta = imports.gi.Meta; const Cinnamon = imports.gi.Cinnamon; -const Signals = imports.signals; const St = imports.gi.St; const DND = imports.ui.dnd; const Main = imports.ui.main; -const Tweener = imports.ui.tweener; const ModalDialog = imports.ui.modalDialog; const Tooltips = imports.ui.tooltips; const PointerTracker = imports.misc.pointerTracker; -const SignalManager = imports.misc.signalManager; + const GridNavigator = imports.misc.gridNavigator; const WindowUtils = imports.misc.windowUtils; @@ -22,7 +20,7 @@ let MAX_THUMBNAIL_SCALE = 0.9; const POINTER_LEAVE_MILLISECONDS_GRACE = 500; const POINTER_ENTER_MILLISECONDS_GRACE = 150; -const RESCALE_ANIMATION_TIME = 0.2; +const RESCALE_ANIMATION_TIME = 200; const SLIDE_ANIMATION_TIME = 300; const INACTIVE_OPACITY = 120; const REARRANGE_TIME_ON = 100; @@ -39,68 +37,47 @@ const DEMANDS_ATTENTION_CLASS_NAME = "window-list-item-demands-attention"; // persistent throughout session var forceOverviewMode = false; -function ExpoWindowClone() { - this._init.apply(this, arguments); -} - -ExpoWindowClone.prototype = { - _init : function(realWindow) { - this.actor = new Clutter.Group({reactive: true}); - this.actor._delegate = this; +var ExpoWindowClone = GObject.registerClass({ + Signals: { + 'hovering': { param_types: [GObject.TYPE_BOOLEAN] }, + 'selected': { param_types: [GObject.TYPE_UINT] }, + 'middle-button-release': { param_types: [GObject.TYPE_UINT] }, + 'demanding-attention': {}, + 'drag-begin': {}, + 'drag-end': {}, + 'drag-cancelled': {}, + }, +}, class ExpoWindowClone extends Clutter.Actor { + _init(realWindow) { + super._init({ reactive: true, layout_manager: new Clutter.FixedLayout() }); + this._delegate = this; this.realWindow = realWindow; this.metaWindow = realWindow.meta_window; this.refreshClone(); - this._signalManager = new SignalManager.SignalManager(null); - this._signalManager.connect(this.realWindow, 'notify::size', this.onSizeChanged, this); - this._signalManager.connect(this.metaWindow, 'workspace-changed', function(w, oldws) { - this.emit('workspace-changed', oldws); - }, this); + this.realWindow.connectObject( + 'notify::size', this.onSizeChanged.bind(this), this); this.onPositionChanged(); this.onSizeChanged(); - let lastButtonPressActor = null; - let lastButtonPressTime = 0; - this.actor.connect('button-press-event', Lang.bind(this, function(actor, event) { - lastButtonPressActor = actor; - lastButtonPressTime = event.get_time(); - })); - this.actor.connect('button-release-event', Lang.bind(this, function(actor, event) { - if (lastButtonPressActor===actor && (event.get_time()-lastButtonPressTime) < 500) { - this.onButtonRelease.apply(this, arguments); - } - return true; - })); + this._lastButtonPressTime = 0; + this._pointerTracker = new PointerTracker.PointerTracker(); - let pointerTracker = new PointerTracker.PointerTracker(); + this.connect('destroy', this.onDestroy.bind(this)); - this.actor.connect('motion-event', Lang.bind(this, function (actor, event) { - if (pointerTracker.hasMoved()) { - this.emit('hovering', true); - } - return false; - })); - this.actor.connect('leave-event', Lang.bind(this, function (actor, event) { - if (pointerTracker.hasMoved()) { - this.emit('hovering', false); - } - return false; - })); - this.actor.connect('destroy', Lang.bind(this, this.onDestroy)); - - this._draggable = DND.makeDraggable(this.actor, + this._draggable = DND.makeDraggable(this, { restoreOnSuccess: false, dragActorMaxSize: WINDOW_DND_SIZE, dragActorOpacity: DRAGGING_WINDOW_OPACITY}); - this._draggable.connect('drag-begin', Lang.bind(this, this.onDragBegin)); - this._draggable.connect('drag-end', Lang.bind(this, this.onDragEnd)); - this._draggable.connect('drag-cancelled', Lang.bind(this, this.onDragCancelled)); + this._draggable.connect('drag-begin', this.onDragBegin.bind(this)); + this._draggable.connect('drag-end', this.onDragEnd.bind(this)); + this._draggable.connect('drag-cancelled', this.onDragCancelled.bind(this)); this.inDrag = false; this.dragCancelled = false; this.icon = new St.Widget(); - this.actor.add_actor(this.icon); + this.add_child(this.icon); this.icon.hide(); let iconActor = null; @@ -123,19 +100,16 @@ ExpoWindowClone.prototype = { this.icon.add_actor(iconActor); iconActor.opacity = ICON_OPACITY; - let attentionId = global.display.connect('window-demands-attention', Lang.bind(this, this.onWindowDemandsAttention)); - let urgentId = global.display.connect('window-marked-urgent', Lang.bind(this, this.onWindowDemandsAttention)); - this.disconnectAttentionSignals = function() { - global.display.disconnect(attentionId); - global.display.disconnect(urgentId); - }; + global.display.connectObject( + 'window-demands-attention', this.onWindowDemandsAttention.bind(this), + 'window-marked-urgent', this.onWindowDemandsAttention.bind(this), this); this.urgencyTimeout = 0; - }, + } - refreshClone: function(withTransients) { + refreshClone(withTransients) { if (this.clone) {this.clone.destroy();} this.clone = new St.Widget({ reactive: false }); - this.actor.add_actor(this.clone); + this.add_child(this.clone); let [pwidth, pheight] = [this.realWindow.width, this.realWindow.height]; let clones = WindowUtils.createWindowClone(this.metaWindow, 0, 0, withTransients); for (let i in clones) { @@ -144,16 +118,16 @@ ExpoWindowClone.prototype = { let [width, height] = clone.get_size(); clone.set_position(Math.round((pwidth - width) / 2), Math.round((pheight - height) / 2)); } - }, + } - killUrgencyTimeout: function() { + killUrgencyTimeout() { if (this.urgencyTimeout != 0) { - Mainloop.source_remove(this.urgencyTimeout); + GLib.source_remove(this.urgencyTimeout); this.urgencyTimeout = 0; } - }, + } - showUrgencyState: function(params) { + showUrgencyState(params) { if (params && params.reps === 0) { // probably the easiest way to just show the current state and stop repeating this.showUrgencyState(); @@ -187,100 +161,115 @@ ExpoWindowClone.prototype = { if (params && params.reps > 0) { this.killUrgencyTimeout(); - this.urgencyTimeout = Mainloop.timeout_add(750, Lang.bind(this, function() { - this.showUrgencyState({showUrgent:!force, reps: params.reps - (force ? 0 : 1)}); + this.urgencyTimeout = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 750, () => { + // Null the id before the recursive call: the callback might + // schedule a new timer, and we must not clobber its id when + // this scope returns. this.urgencyTimeout = 0; - })); + this.showUrgencyState({showUrgent:!force, reps: params.reps - (force ? 0 : 1)}); + return GLib.SOURCE_REMOVE; + }); } - }, + } - demandAttention: function() { + demandAttention() { this.demanding_attention = true; this.showUrgencyState({showUrgent:true, reps: 50}); this.emit('demanding-attention'); - }, + } - onWindowDemandsAttention: function(display, metaWindow) { + onWindowDemandsAttention(display, metaWindow) { if (metaWindow != this.metaWindow) {return;} this.demandAttention(); - }, + } - setStackAbove: function (actor) { - if (actor.get_parent() !== this.actor.get_parent()) { + setStackAbove(actor) { + if (actor.get_parent() !== this.get_parent()) { return; } this.stackAbove = actor; if (this.stackAbove == null) - this.actor.lower_bottom(); + this.lower_bottom(); else - this.actor.raise(this.stackAbove); - }, - - destroy: function () { - this.killUrgencyTimeout(); - this.disconnectAttentionSignals(); - this.actor.destroy(); - this.icon = null; - }, + this.raise(this.stackAbove); + } - onPositionChanged: function() { - this.actor.set_position(this.origX = this.realWindow.x, this.origY = this.realWindow.y); - this.actor.set_size(this.realWindow.width, this.realWindow.height); - }, + onPositionChanged() { + this.set_position(this.origX = this.realWindow.x, this.origY = this.realWindow.y); + this.set_size(this.realWindow.width, this.realWindow.height); + } - onSizeChanged: function() { - this.actor.set_size(this.realWindow.width, this.realWindow.height); - }, + onSizeChanged() { + this.set_size(this.realWindow.width, this.realWindow.height); + } - onDestroy: function() { - this._signalManager.disconnectAllSignals(); - this.actor._delegate = null; + onDestroy() { + this.killUrgencyTimeout(); + this.icon = null; + this._delegate = null; if (this.inDrag) { this.inDrag = false; this.emit('drag-end'); } + } - this.disconnectAll(); - }, + vfunc_button_press_event(event) { + this._lastButtonPressTime = event.time; + return Clutter.EVENT_PROPAGATE; + } - onButtonRelease : function (actor, event) { - const state = Cinnamon.get_event_state(event); + vfunc_button_release_event(event) { + if ((event.time - this._lastButtonPressTime) < 500) + this.onButtonRelease(event); + return Clutter.EVENT_STOP; + } - if (state !== 0) { - return true; - } + vfunc_motion_event(event) { + if (this._pointerTracker.hasMoved()) + this.emit('hovering', true); + return Clutter.EVENT_PROPAGATE; + } + + vfunc_leave_event(event) { + if (this._pointerTracker.hasMoved()) + this.emit('hovering', false); + return Clutter.EVENT_PROPAGATE; + } + + onButtonRelease(event) { + if (Cinnamon.get_event_state(event) !== 0) + return Clutter.EVENT_STOP; - const button = event.get_button(); + const button = event.button; if ([Clutter.BUTTON_PRIMARY, Clutter.BUTTON_SECONDARY].includes(button)) { - this.emit('selected', event.get_time()); + this.emit('selected', event.time); } else if (button == Clutter.BUTTON_MIDDLE) { - this.emit('middle-button-release', event.get_time()); + this.emit('middle-button-release', event.time); } - return true; - }, + return Clutter.EVENT_STOP; + } - onDragBegin : function (draggable, time) { + onDragBegin(draggable, time) { this.inDrag = true; this.dragCancelled = false; this.emit('drag-begin'); - }, + } - onDragCancelled : function (draggable, time) { + onDragCancelled(draggable, time) { this.dragCancelled = true; this.emit('drag-cancelled'); - }, + } - onDragEnd : function (draggable, time, snapback) { + onDragEnd(draggable, time, snapback) { this.inDrag = false; this.emit('drag-end'); } -}; -Signals.addSignalMethods(ExpoWindowClone.prototype); +}); const ThumbnailState = { @@ -297,83 +286,56 @@ const ThumbnailState = { /** * @metaWorkspace: a #Meta.Workspace */ -function ExpoWorkspaceThumbnail(metaWorkspace, box) { - this._init(metaWorkspace, box); -} - -ExpoWorkspaceThumbnail.prototype = { - _init : function(metaWorkspace, box) { +var ExpoWorkspaceThumbnail = GObject.registerClass({ + Properties: { + 'slide-position': GObject.ParamSpec.double( + 'slide-position', 'slide-position', 'slide-position', + GObject.ParamFlags.READWRITE, + 0, 1, 0), + }, + Signals: { + 'drag-over': {}, + 'drag-end': {}, + 'drag-begin': {}, + }, +}, class ExpoWorkspaceThumbnail extends St.Widget { + _init(metaWorkspace, box) { + super._init({ reactive: true, + clip_to_allocation: true, + style_class: 'workspace-thumbnail' }); + this._delegate = this; this.box = box; this.metaWorkspace = metaWorkspace; + this._addWindowIdleIds = new Set(); this.overviewMode = false; this.frame = new St.Widget({ clip_to_allocation: true, style_class: 'expo-workspace-thumbnail-frame' }); - this.actor = new St.Widget({ reactive: true, - clip_to_allocation: true, - style_class: 'workspace-thumbnail' }); - this.actor._delegate = this; - this.actor.set_size(global.screen_width, global.screen_height); + this.set_size(global.screen_width, global.screen_height); this.contents = new Clutter.Group(); - this.actor.add_actor(this.contents); - - this.actor.connect('destroy', Lang.bind(this, this.onDestroy)); + this.add_child(this.contents); - let lastButtonPressTimeStamp = 0; - let lastButtonPressActor = null; - this.actor.connect('button-press-event', Lang.bind(this, function(actor, event) { - lastButtonPressTimeStamp = event.get_time(); - lastButtonPressActor = actor; - return true; - })); - this.actor.connect('button-release-event', Lang.bind(this, - function(actor, event) { - if (lastButtonPressActor !== actor) { - return true; - } - let timeElapsed = event.get_time() - lastButtonPressTimeStamp; - // A long time elapsed is probably due to a failed dnd attempt, - // or some other mishap, so we'll ignore those. - if (timeElapsed > 500) { - return true; - } - - const state = Cinnamon.get_event_state(event); - if (state !== 0) { - return false; - } + this.connect('destroy', this.onDestroy.bind(this)); - const button = event.get_button(); - if ([Clutter.BUTTON_PRIMARY, Clutter.BUTTON_SECONDARY].includes(button)) - { - this.activate(null, event.get_time()); - return true; - } else if (button === Clutter.BUTTON_MIDDLE) { - this.remove(); - return true; - } - return false; - })); + this._lastButtonPressTimeStamp = 0; - this.actor.connect('scroll-event', Lang.bind(this, this.onScrollEvent)); - - this.title = new St.Entry({ style_class: 'expo-workspaces-name-entry', + this.title = new St.Entry({ style_class: 'expo-workspaces-name-entry', track_hover: true, - can_focus: true }); - this.title._spacing = 0; - this.titleText = this.title.clutter_text; + can_focus: true }); + this.title._spacing = 0; + this.titleText = this.title.clutter_text; this.titleText.editable = false; - this.titleText.connect('key-press-event', Lang.bind(this, this.onTitleKeyPressEvent)); - this.titleText.connect('key-focus-in', Lang.bind(this, function() { + this.titleText.connect('key-press-event', this.onTitleKeyPressEvent.bind(this)); + this.titleText.connect('key-focus-in', () => { this.titleText.editable = true; this.origTitle = Main.getWorkspaceName(this.metaWorkspace.index()); - })); - this.titleText.connect('key-focus-out', Lang.bind(this, function() { + }); + this.titleText.connect('key-focus-out', () => { if (this.doomed) { // user probably deleted workspace while editing - global.stage.set_key_focus(this.box.actor); + global.stage.set_key_focus(this.box); return; } if (!this.undoTitleEdit) { @@ -383,10 +345,10 @@ ExpoWorkspaceThumbnail.prototype = { } } this.title.set_text(Main.getWorkspaceName(this.metaWorkspace.index())); - })); - + }); + this.title.set_text(Main.getWorkspaceName(this.metaWorkspace.index())); - + this.background = new Clutter.Group(); this.contents.add_actor(this.background); @@ -399,7 +361,7 @@ ExpoWorkspaceThumbnail.prototype = { this.shader = new St.Bin(); this.shader.set_style('background-color: black;'); - this.actor.add_actor(this.shader); + this.add_child(this.shader); this.shader.set_size(global.screen_width, global.screen_height); this.shader.opacity = INACTIVE_OPACITY; @@ -418,58 +380,54 @@ ExpoWorkspaceThumbnail.prototype = { } } - let windowAddedId = this.metaWorkspace.connect('window-added', - Lang.bind(this, this.windowAdded)); - let windowRemovedId = this.metaWorkspace.connect('window-removed', - Lang.bind(this, this.windowRemoved)); - let windowEnteredMonitorId = global.display.connect('window-entered-monitor', - Lang.bind(this, this.windowEnteredMonitor)); - let windowLeftMonitorId = global.display.connect('window-left-monitor', - Lang.bind(this, this.windowLeftMonitor)); + this.metaWorkspace.connectObject( + 'window-added', this.windowAdded.bind(this), + 'window-removed', this.windowRemoved.bind(this), this); + global.display.connectObject( + 'window-entered-monitor', this.windowEnteredMonitor.bind(this), + 'window-left-monitor', this.windowLeftMonitor.bind(this), + 'restacked', this.onRestack.bind(this), this); + box.connectObject( + 'set-overview-mode', (box, turnOn) => { + this.setOverviewMode(turnOn); + this.hovering = false; + }, this); - let setOverviewModeId = box.connect('set-overview-mode', Lang.bind(this, function(box, turnOn) { - this.setOverviewMode(turnOn); - this.hovering = false; - })); - let stickyAddedId = box.connect('sticky-detected', Lang.bind(this, function(box, metaWindow) { - this.doAddWindow(metaWindow); - })); - let restackedNotifyId = global.display.connect('restacked', Lang.bind(this, this.onRestack)); - - this.disconnectOtherSignals = function() { - global.display.disconnect(restackedNotifyId); - this.box.disconnect(setOverviewModeId); - this.box.disconnect(stickyAddedId); - this.metaWorkspace.disconnect(windowAddedId); - this.metaWorkspace.disconnect(windowRemovedId); - global.display.disconnect(windowEnteredMonitorId); - global.display.disconnect(windowLeftMonitorId); - }; - this.isActive = false; this.state = ThumbnailState.NORMAL; this.restack(); - this._slidePosition = 0; // Fully slid in this.setOverviewMode(forceOverviewMode); - }, + } + + set slide_position(slidePosition) { + if (this._slidePosition === slidePosition) + return; + this._slidePosition = slidePosition; + this.notify('slide-position'); + this.queue_relayout(); + } - setOverviewMode: function(turnOn) { + get slide_position() { + return this._slidePosition || 0; + } + + setOverviewMode(turnOn) { if (turnOn) {this.overviewModeOn();} else {this.overviewModeOff();} - }, + } - refresh: function() { + refresh() { this.refreshTitle(); this.resetCloneHover(); this.setOverviewMode(this.overviewMode); - }, + } - onRestack: function() { + onRestack() { this.restack(); this.refresh(); - }, + } - restack: function(force) { + restack(force) { if (this.state > ThumbnailState.NORMAL) { return; } @@ -483,20 +441,20 @@ ExpoWorkspaceThumbnail.prototype = { } this.syncStacking(this.stackIndices); } - }, + } - setActive: function(isActive) { + setActive(isActive) { this.isActive = isActive; this.frame.name = isActive ? 'active' : ''; - }, + } - refreshTitle: function() { + refreshTitle() { if (!this.doomed) { // better safe than sorry this.title.set_text(Main.getWorkspaceName(this.metaWorkspace.index())); } - }, - - onTitleKeyPressEvent: function(actor, event) { + } + + onTitleKeyPressEvent(actor, event) { this.undoTitleEdit = false; let symbol = event.get_key_symbol(); if (symbol === Clutter.KEY_Return || @@ -505,19 +463,19 @@ ExpoWorkspaceThumbnail.prototype = { if (symbol === Clutter.KEY_Escape) { this.undoTitleEdit = true; } - global.stage.set_key_focus(this.actor); - return true; + global.stage.set_key_focus(this); + return Clutter.EVENT_STOP; } - return false; - }, - - activateWorkspace: function() { + return Clutter.EVENT_PROPAGATE; + } + + activateWorkspace() { if (this.metaWorkspace != global.workspace_manager.get_active_workspace()) this.metaWorkspace.activate(global.get_current_time()); Main.expo.hide(); - }, - - showKeyboardSelectedState: function(selected) { + } + + showKeyboardSelectedState(selected) { this.isSelected = selected; this.title.name = selected ? "selected" : ""; if (selected) { @@ -529,29 +487,29 @@ ExpoWorkspaceThumbnail.prototype = { this.overviewModeOff(); this.shade(); } - }, - - lookupIndex: function (metaWindow) { + } + + lookupIndex(metaWindow) { for (let i = 0; i < this.windows.length; i++) { if (this.windows[i].metaWindow == metaWindow) { return i; } } return -1; - }, + } - syncStacking: function(stackIndices) { - this.windows.sort(Lang.bind(this, function (a, b) { + syncStacking(stackIndices) { + this.windows.sort((a, b) => { let minimizedDiff = function(a, b) { let minimizedA = a.metaWindow.minimized ? -1 : 0; let minimizedB = b.metaWindow.minimized ? -1 : 0; return minimizedA - minimizedB; }; - let noOverviewDiff = Lang.bind(this, function(a, b) { + let noOverviewDiff = (a, b) => { let noOverviewA = !this.isOverviewWindow(a.metaWindow) ? -1 : 0; let noOverviewB = !this.isOverviewWindow(b.metaWindow) ? -1 : 0; return noOverviewA - noOverviewB; - }); + }; let transientRelation = function(a, b) { let overviewDifference = noOverviewDiff(a,b); if (overviewDifference) { @@ -564,7 +522,7 @@ ExpoWorkspaceThumbnail.prototype = { return transientRelation(a,b) || minimizedDiff(a,b) || stackIndices[a.metaWindow.get_stable_sequence()] - stackIndices[b.metaWindow.get_stable_sequence()]; - })); + }); for (let i = 0; i < this.windows.length; i++) { let clone = this.windows[i]; @@ -573,21 +531,12 @@ ExpoWorkspaceThumbnail.prototype = { clone.setStackAbove(this.background); } else { let previousClone = this.windows[i - 1]; - clone.setStackAbove(previousClone.actor); + clone.setStackAbove(previousClone); } } - }, - - set slidePosition(slidePosition) { - this._slidePosition = slidePosition; - this.actor.queue_relayout(); - }, - - get slidePosition() { - return this._slidePosition; - }, + } - doRemoveWindow : function(metaWin) { + doRemoveWindow(metaWin) { let win = metaWin.get_compositor_private(); // find the position of the window in our list @@ -606,21 +555,23 @@ ExpoWorkspaceThumbnail.prototype = { clone.destroy(); if (this.overviewMode) this.overviewModeOn(); - }, + } - doAddWindow : function(metaWin) { + doAddWindow(metaWin) { let win = metaWin.get_compositor_private(); if (!win) { // Newly-created windows are added to a workspace before // the compositor finds out about them... - Mainloop.idle_add(Lang.bind(this, function () { - if (this.windows /*will be null if we're closing down*/ && + let id = GLib.idle_add(GLib.PRIORITY_DEFAULT_IDLE, () => { + this._addWindowIdleIds.delete(id); + if (this.windows && metaWin.get_compositor_private()) { this.doAddWindow(metaWin); } - return false; - })); + return GLib.SOURCE_REMOVE; + }); + this._addWindowIdleIds.add(id); return; } @@ -638,141 +589,141 @@ ExpoWorkspaceThumbnail.prototype = { if (!this.isMyWindow(win) || !this.isExpoWindow(win)) return; - let clone = this.addWindowClone(win); + let clone = this.addWindowClone(win); this.overviewModeOn(); - }, + } - windowAdded : function(metaWorkspace, metaWin) { + windowAdded(metaWorkspace, metaWin) { this.doAddWindow(metaWin); this.restack(); - }, + } - windowRemoved : function(metaWorkspace, metaWin) { + windowRemoved(metaWorkspace, metaWin) { this.doRemoveWindow(metaWin); - }, + } - windowEnteredMonitor : function(metaDisplay, monitorIndex, metaWin) { + windowEnteredMonitor(metaDisplay, monitorIndex, metaWin) { // important if workspaces-only-on-primary is in effect this.doAddWindow(metaWin); - }, + } - windowLeftMonitor : function(metaDisplay, monitorIndex, metaWin) { + windowLeftMonitor(metaDisplay, monitorIndex, metaWin) { // important if workspaces-only-on-primary is in effect this.doRemoveWindow(metaWin); - }, - - destroy : function() { - this.actor.destroy(); - this.frame.destroy(); - }, + } - onDestroy: function(actor) { - this.disconnectOtherSignals(); + onDestroy(actor) { + actor.remove_all_transitions(); + if (this._collapseId) { + GLib.source_remove(this._collapseId); + this._collapseId = 0; + } + if (this.buttonTimeoutId) { + GLib.source_remove(this.buttonTimeoutId); + this.buttonTimeoutId = 0; + } + if (this._addWindowIdleIds) { + this._addWindowIdleIds.forEach(id => GLib.source_remove(id)); + this._addWindowIdleIds.clear(); + } this.resetCloneHover(); for (let i = 0; i < this.windows.length; i++) { this.windows[i].destroy(); } this.windows = null; - }, + } // Tests if @win belongs to this workspace and monitor - isMyWindow : function (win) { + isMyWindow(win) { return Main.isWindowActorDisplayedOnWorkspace(win, this.metaWorkspace.index()); - }, + } // Tests if @win should be shown in the Expo - isExpoWindow : function (win) { + isExpoWindow(win) { let metaWindow = win.get_meta_window(); if (metaWindow.is_override_redirect()) { return false; } let type = metaWindow.get_window_type(); return type !== Meta.WindowType.DESKTOP && type !== Meta.WindowType.DOCK; - }, + } // Tests if @win should be shown in overview mode - isOverviewWindow : function (metaWindow) { + isOverviewWindow(metaWindow) { return Main.isInteresting(metaWindow); - }, + } // Create a clone of a (non-desktop) window and add it to the window list - addWindowClone : function(win) { + addWindowClone(win) { let clone = new ExpoWindowClone(win); - clone.connect('workspace-changed', Lang.bind(this, function() { - this.doRemoveWindow(clone.metaWindow); - if (clone.metaWindow.is_on_all_workspaces()) { - // Muffin appears not to broadcast when a window turns sticky - this.box.emit('sticky-detected', clone.metaWindow); - } - })); - clone.connect('middle-button-release', Lang.bind(this, function(sender, time) { + clone.connect('middle-button-release', (sender, time) => { clone.metaWindow.delete(time); - })); - clone.connect('hovering', Lang.bind(this, this.onCloneHover)); - clone.connect('demanding-attention', Lang.bind(this, function() {this.overviewModeOn();})); - clone.connect('selected', Lang.bind(this, this.activate)); - clone.connect('remove-workspace', Lang.bind(this, this.remove)); - clone.connect('drag-begin', Lang.bind(this, function(clone) { + }); + clone.connect('hovering', this.onCloneHover.bind(this)); + clone.connect('demanding-attention', () => { this.overviewModeOn(); }); + clone.connect('selected', this.activate.bind(this)); + clone.connect('drag-begin', (clone) => { this.box.emit('drag-begin'); this.resetCloneHover(); - })); - clone.connect('drag-end', Lang.bind(this, function(clone) { + }); + clone.connect('drag-end', (clone) => { this.box.emit('drag-end'); if (clone.dragCancelled) { // stacking order may have been disturbed this.restack(); } this.overviewModeOn(); - })); - this.contents.add_actor(clone.actor); + }); + this.contents.add_actor(clone); if (this.windows.length == 0) clone.setStackAbove(this.background); else - clone.setStackAbove(this.windows[this.windows.length - 1].actor); + clone.setStackAbove(this.windows[this.windows.length - 1]); this.windows.push(clone); return clone; - }, + } - resetCloneHover : function () { + resetCloneHover() { this.lastHoveredClone = null; if (this.tooltip) { this.tooltip.destroy(); this.tooltip = null; } - }, + } - onCloneHover : function (clone, hovering) { + onCloneHover(clone, hovering) { if (!this.overviewMode) { this.resetCloneHover(); return; } if (hovering && clone !== this.lastHoveredClone) { - if (this.buttonTimeoutId) {Mainloop.source_remove(this.buttonTimeoutId);} - this.buttonTimeoutId = Mainloop.idle_add(Lang.bind(this,function() { - this.buttonTimeoutId = null; - if (!this.windows) {return;} /* being destroyed */ + if (this.buttonTimeoutId) {GLib.source_remove(this.buttonTimeoutId);} + this.buttonTimeoutId = GLib.idle_add(GLib.PRIORITY_DEFAULT_IDLE, () => { + this.buttonTimeoutId = 0; + if (!this.windows) {return GLib.SOURCE_REMOVE;} /* being destroyed */ let [x, y, mask] = global.get_pointer(); let target = this.contents.get_stage().get_actor_at_pos(Clutter.PickMode.REACTIVE, x, y); - if (target !== clone.actor) { + if (target !== clone) { this.resetCloneHover(); - return; + return GLib.SOURCE_REMOVE; } if (this.tooltip) { this.tooltip.destroy(); } - this.tooltip = new Tooltips.Tooltip(clone.actor, clone.metaWindow.title); - })); + this.tooltip = new Tooltips.Tooltip(clone, clone.metaWindow.title); + return GLib.SOURCE_REMOVE; + }); this.lastHoveredClone = clone; } - }, + } - overviewModeOn : function () { - if (!this.box.scale) {return;} + overviewModeOn() { + if (!this.box.thumbnail_scale) {return;} this.overviewMode = true; this.resetCloneHover(); @@ -782,12 +733,12 @@ ExpoWorkspaceThumbnail.prototype = { windows.push(window); } else { - window.actor.ease({ + window.ease({ scale_x: 0, scale_y: 0, duration: Main.animations_enabled ? REARRANGE_TIME_ON : 0, mode: Clutter.AnimationMode.EASE_OUT_QUAD, - onComplete: () => window.actor.hide() + onComplete: () => window.hide() }); } }, this); @@ -797,7 +748,7 @@ ExpoWorkspaceThumbnail.prototype = { let monitorWindows = windows.filter(function(window) { return window.metaWindow.get_monitor() === monitorIndex; }, this); - + let spacing = 14; let nWindows = monitorWindows.length; let [nCols, nRows] = [Math.ceil(Math.sqrt(nWindows)), Math.round(Math.sqrt(nWindows))] @@ -810,7 +761,7 @@ ExpoWorkspaceThumbnail.prototype = { monitorWindows.forEach(function(window, i) { if (window.inDrag) {return;} - + window.refreshClone(true); window.showUrgencyState(); if (row == nRows) @@ -822,11 +773,11 @@ ExpoWorkspaceThumbnail.prototype = { window.icon.raise_top(); // all icons should be the same size! - let iconScale = (0.25/this.box.scale/scale); + let iconScale = (0.25/this.box.thumbnail_scale/scale); window.icon.set_scale(iconScale, iconScale); - let [iconX, iconY] = [ICON_OFFSET / this.box.scale/scale, ICON_OFFSET / this.box.scale/scale]; + let [iconX, iconY] = [ICON_OFFSET / this.box.thumbnail_scale/scale, ICON_OFFSET / this.box.thumbnail_scale/scale]; window.icon.set_position(iconX, iconY); - window.actor.ease({ + window.ease({ x: x, y: y, scale_x: scale, @@ -835,7 +786,7 @@ ExpoWorkspaceThumbnail.prototype = { duration: Main.animations_enabled ? REARRANGE_TIME_ON : 0, mode: Clutter.AnimationMode.EASE_OUT_QUAD, onComplete: () => { - window.actor.show(); + window.show(); window.icon.show(); } }); @@ -846,14 +797,14 @@ ExpoWorkspaceThumbnail.prototype = { } }, this); }, this); - }, + } - overviewModeOff : function(force, override) { - if (!this.box.scale) {return;} + overviewModeOff(force, override) { + if (!this.box.thumbnail_scale) {return;} this.resetCloneHover(); if (this.overviewMode === false && !force) {return;} if (forceOverviewMode && !override) {return;} - + this.overviewMode = false; const iconSpacing = ICON_SIZE/4; let rearrangeTime = force ? REARRANGE_TIME_OFF/2 : REARRANGE_TIME_OFF; @@ -865,12 +816,12 @@ ExpoWorkspaceThumbnail.prototype = { if (monitorIndex !== window.metaWindow.get_monitor()) { return; } - + window.refreshClone(false); window.showUrgencyState(); window.icon.hide(); - window.actor.show(); - window.actor.ease({ + window.show(); + window.ease({ x: window.origX, y: window.origY, scale_x: 1, scale_y: 1, @@ -880,13 +831,39 @@ ExpoWorkspaceThumbnail.prototype = { }); }, this); }, this); - }, + } + + vfunc_button_press_event(event) { + this._lastButtonPressTimeStamp = event.time; + return Clutter.EVENT_STOP; + } + + vfunc_button_release_event(event) { + // A long time elapsed is probably due to a failed dnd attempt, + // or some other mishap, so we'll ignore those. + let timeElapsed = event.time - this._lastButtonPressTimeStamp; + if (timeElapsed > 500) + return Clutter.EVENT_STOP; + + if (Cinnamon.get_event_state(event) !== 0) + return Clutter.EVENT_PROPAGATE; + + const button = event.button; + if ([Clutter.BUTTON_PRIMARY, Clutter.BUTTON_SECONDARY].includes(button)) { + this.activate(null, event.time); + return Clutter.EVENT_STOP; + } else if (button === Clutter.BUTTON_MIDDLE) { + this.removeWorkspace(); + return Clutter.EVENT_STOP; + } + return Clutter.EVENT_PROPAGATE; + } - onScrollEvent: function (actor, event) { + vfunc_scroll_event(event) { if (Main.expo.animationInProgress) - return; + return Clutter.EVENT_PROPAGATE; - switch ( event.get_scroll_direction() ) { + switch (event.direction) { case Clutter.ScrollDirection.UP: Main.wm.actionMoveWorkspaceLeft(); break; @@ -894,9 +871,10 @@ ExpoWorkspaceThumbnail.prototype = { Main.wm.actionMoveWorkspaceRight(); break; } - }, + return Clutter.EVENT_PROPAGATE; + } - activate : function (clone, time) { + activate(clone, time) { if (this.state > ThumbnailState.NORMAL) return; @@ -906,9 +884,9 @@ ExpoWorkspaceThumbnail.prototype = { if (this.metaWorkspace != global.workspace_manager.get_active_workspace()) this.metaWorkspace.activate(time); Main.expo.hide(); - }, + } - shade : function (force){ + shade(force) { if (!this.isSelected || force) { this.shader.ease({ opacity: INACTIVE_OPACITY, @@ -916,17 +894,17 @@ ExpoWorkspaceThumbnail.prototype = { mode: Clutter.AnimationMode.EASE_OUT_QUAD }); } - }, + } - highlight : function (){ + highlight() { this.shader.ease({ opacity: 0, duration: Main.animations_enabled ? SLIDE_ANIMATION_TIME : 0, mode: Clutter.AnimationMode.EASE_OUT_QUAD }); - }, + } - remove : function (){ + removeWorkspace() { if (this.doomed) { // this workspace is already being removed return; @@ -934,10 +912,10 @@ ExpoWorkspaceThumbnail.prototype = { if (global.workspace_manager.n_workspaces <= 1) { return; } - let removeAction = Lang.bind(this, function() { + let removeAction = () => { this.doomed = true; Main._removeWorkspace(this.metaWorkspace); - }); + }; if (!Main.hasDefaultWorkspaceName(this.metaWorkspace.index())) { this.overviewModeOn(); this.highlight(); @@ -949,27 +927,27 @@ ExpoWorkspaceThumbnail.prototype = { else { removeAction(); } - }, + } - coordinateToMonitor : function(x, y) { + coordinateToMonitor(x, y) { let indexOne = 0; Main.layoutManager.monitors.forEach(function(monitor, mindex) { let [xX, yY] = [x - monitor.x, y - monitor.y]; indexOne = indexOne || (xX >= 0 && xX < monitor.width && yY > 0 && yY < monitor.height ? mindex + 1 : 0); }, this); return indexOne - 1; - }, + } // Draggable target interface - handleDragOver : function(source, actor, x, y, time) { + handleDragOver(source, actor, x, y, time) { this.emit('drag-over'); if (!this.overviewMode) { this.overviewModeOn(); } return this.handleDragOverOrDrop(false, source, actor, x, y, time); - }, + } - handleDragOverOrDrop : function(dropping, source, actor, x, y, time) { + handleDragOverOrDrop(dropping, source, actor, x, y, time) { this.hovering = false; // normal hover logic is off during dnd if (dropping) { let draggable = source._draggable; @@ -989,7 +967,7 @@ ExpoWorkspaceThumbnail.prototype = { let win = source.realWindow; let metaWindow = source.metaWindow; - + let targetMonitor = this.coordinateToMonitor(x, y); let fromMonitor = metaWindow.get_monitor(); @@ -1030,9 +1008,9 @@ ExpoWorkspaceThumbnail.prototype = { } return canDrop ? DND.DragMotionResult.MOVE_DROP : DND.DragMotionResult.CONTINUE; - }, + } - acceptDrop : function(source, actor, x, y, time) { + acceptDrop(source, actor, x, y, time) { if (this.handleDragOverOrDrop(false, source, actor, x, y, time) != DND.DragMotionResult.CONTINUE) { if (this.handleDragOverOrDrop(true, source, actor, x, y, time) != DND.DragMotionResult.CONTINUE) { this.restack(true); @@ -1042,21 +1020,29 @@ ExpoWorkspaceThumbnail.prototype = { } return false; } -}; - -Signals.addSignalMethods(ExpoWorkspaceThumbnail.prototype); - -function ExpoThumbnailsBox() { - this._init(); -} - -ExpoThumbnailsBox.prototype = { - _init: function() { - this.actor = new Cinnamon.GenericContainer({ style_class: 'workspace-thumbnails', - reactive: true, - request_mode: Clutter.RequestMode.WIDTH_FOR_HEIGHT }); - this.actor.connect('get-preferred-width', Lang.bind(this, this.getPreferredWidth)); - this.actor.connect('get-preferred-height', Lang.bind(this, this.getPreferredHeight)); +}); + + +var ExpoThumbnailsBox = GObject.registerClass({ + Properties: { + 'thumbnail-scale': GObject.ParamSpec.double( + 'thumbnail-scale', 'thumbnail-scale', 'thumbnail-scale', + GObject.ParamFlags.READWRITE, + 0, MAX_THUMBNAIL_SCALE, 0), + }, + Signals: { + 'set-overview-mode': { param_types: [GObject.TYPE_BOOLEAN] }, + 'allocated': {}, + 'drag-begin': {}, + 'drag-end': {}, + }, +}, class ExpoThumbnailsBox extends St.Widget { + _init() { + super._init({ + style_class: 'workspace-thumbnails', + reactive: true, + request_mode: Clutter.RequestMode.WIDTH_FOR_HEIGHT, + }); // When we animate the scale, we don't animate the requested size of the thumbnails, rather // we ask for our final size and then animate within that size. This slightly simplifies the @@ -1069,13 +1055,13 @@ ExpoThumbnailsBox.prototype = { // an actor underneath the content and adjust the allocation of our children to leave space // for the border and padding of the background actor. this.background = new St.Bin({reactive:true}); - this.actor.add_actor(this.background); + this.add_child(this.background); this.background.handleDragOver = function(source, actor, x, y, time) { return source.metaWindow && !source.metaWindow.is_on_all_workspaces() ? DND.DragMotionResult.MOVE_DROP : DND.DragMotionResult.CONTINUE; }; - this.background.acceptDrop = Lang.bind(this, function(source, actor, x, y, time) { - if (this.background.handleDragOver.apply(this, arguments) === DND.DragMotionResult.MOVE_DROP) { + this.background.acceptDrop = (source, actor, x, y, time) => { + if (this.background.handleDragOver(source, actor, x, y, time) === DND.DragMotionResult.MOVE_DROP) { let draggable = source._draggable; actor.get_parent().remove_actor(actor); draggable._dragOrigParent.add_actor(actor); @@ -1084,21 +1070,18 @@ ExpoThumbnailsBox.prototype = { return true; } return false; - }); + }; this.background._delegate = this.background; this.button = new St.Button({ style_class: 'workspace-close-button' }); - this.actor.add_actor(this.button); - - this.button.connect('enter-event', Lang.bind(this, function () {this.button.show();})); - this.button.connect('leave-event', Lang.bind(this, function () {this.button.hide();})); - this.button.connect('clicked', Lang.bind(this, function () { this.lastHovered.remove(); this.button.hide();})); + this.add_child(this.button); + + this.button.connect('enter-event', () => { this.button.show(); }); + this.button.connect('leave-event', () => { this.button.hide(); }); + this.button.connect('clicked', () => { this.lastHovered.removeWorkspace(); this.button.hide(); }); this.button.hide(); - - this.actor.connect('scroll-event', this.onScrollEvent); this.targetScale = 0; - this._scale = 0; this.pendingScaleUpdate = false; this.stateUpdateQueued = false; @@ -1116,68 +1099,97 @@ ExpoThumbnailsBox.prototype = { }; this.kbThumbnailIndex = global.workspace_manager.get_active_workspace_index(); - - // apparently we get no direct call to show the initial - // view, so we must force an explicit overviewMode On/Off display - // after it has been allocated - let allocId = this.connect('notify::allocation', Lang.bind(this, function() { + + this._initialAllocTimeoutId = 0; + this._updateStatesLaterId = 0; + + let allocId = this.connect('notify::allocation', () => { this.disconnect(allocId); - Mainloop.timeout_add(100, Lang.bind(this, function() { - this.emit('set-overview-mode', forceOverviewMode === 1); - this.thumbnails[this.kbThumbnailIndex].showKeyboardSelectedState(true); - })); - })); + this._initialAllocTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 100, () => { + this._initialAllocTimeoutId = 0; + if (this.thumbnails.length) { + this.emit('set-overview-mode', forceOverviewMode); + this.thumbnails[this.kbThumbnailIndex].showKeyboardSelectedState(true); + } + return GLib.SOURCE_REMOVE; + }); + }); + + this.connect('destroy', () => { + if (this._initialAllocTimeoutId) { + GLib.source_remove(this._initialAllocTimeoutId); + this._initialAllocTimeoutId = 0; + } + if (this._updateStatesLaterId) { + Meta.later_remove(this._updateStatesLaterId); + this._updateStatesLaterId = 0; + } + }); this.toggleGlobalOverviewMode = function() { - forceOverviewMode = (forceOverviewMode + 1) % 2; - this.emit('set-overview-mode', forceOverviewMode === 1); + forceOverviewMode = !forceOverviewMode; + this.emit('set-overview-mode', forceOverviewMode); }; - this.actor.connect('button-release-event', Lang.bind(this, function(actor, event) { - if (Cinnamon.get_event_state(event) === 0 && - event.get_button() == Clutter.BUTTON_MIDDLE) { - this.toggleGlobalOverviewMode(); - } - })); - }, + } - show: function() { - this.switchWorkspaceNotifyId = - global.window_manager.connect('switch-workspace', - Lang.bind(this, this.activeWorkspaceChanged)); + vfunc_button_release_event(event) { + if (Cinnamon.get_event_state(event) === 0 && + event.button == Clutter.BUTTON_MIDDLE) { + this.toggleGlobalOverviewMode(); + } + return Clutter.EVENT_PROPAGATE; + } - this.workspaceAddedId = global.workspace_manager.connect('workspace-added', Lang.bind(this, function(ws_manager, index) { - this.addThumbnails(index, 1); - })); - this.workspaceRemovedId = global.workspace_manager.connect('workspace-removed', Lang.bind(this, function() { - this.button.hide(); + vfunc_scroll_event(event) { + if (Main.expo.animationInProgress) + return Clutter.EVENT_PROPAGATE; + + switch (event.direction) { + case Clutter.ScrollDirection.UP: + Main.wm.actionMoveWorkspaceUp(); + break; + case Clutter.ScrollDirection.DOWN: + Main.wm.actionMoveWorkspaceDown(); + break; + } + return Clutter.EVENT_PROPAGATE; + } - // just handling the single workspace removed is not enough - let removedCount = 0; - this.thumbnails.forEach(function(thumbnail, i) { - let metaWorkspace = global.workspace_manager.get_workspace_by_index(i-removedCount); - if (thumbnail.metaWorkspace != metaWorkspace) { - ++removedCount; - if (thumbnail.state <= ThumbnailState.NORMAL) { - this.setThumbnailState(thumbnail, ThumbnailState.REMOVING); + show() { + global.window_manager.connectObject( + 'switch-workspace', this.activeWorkspaceChanged.bind(this), this); + global.workspace_manager.connectObject( + 'workspace-added', (ws_manager, index) => { + this.addThumbnails(index, 1); + }, + 'workspace-removed', () => { + this.button.hide(); + + let removedCount = 0; + this.thumbnails.forEach((thumbnail, i) => { + let metaWorkspace = global.workspace_manager.get_workspace_by_index(i-removedCount); + if (thumbnail.metaWorkspace != metaWorkspace) { + ++removedCount; + if (thumbnail.state <= ThumbnailState.NORMAL) { + this.setThumbnailState(thumbnail, ThumbnailState.REMOVING); + } } - } + }); + this.updateStates(); }, this); - this.updateStates(); - })); this.stateCounts = {}; for (let key in ThumbnailState) this.stateCounts[ThumbnailState[key]] = 0; this.addThumbnails(0, global.workspace_manager.n_workspaces); - this.actor.connect('allocate', Lang.bind(this, this.allocate)); this.button.raise_top(); - global.stage.set_key_focus(this.actor); - }, + global.stage.set_key_focus(this); + } - handleKeyPressEvent: function(actor, event) { + handleKeyPressEvent(actor, event) { let modifiers = Cinnamon.get_event_state(event); let ctrlAltMask = Clutter.ModifierType.CONTROL_MASK | Clutter.ModifierType.MOD1_MASK; let symbol = event.get_key_symbol(); @@ -1186,40 +1198,40 @@ ExpoThumbnailsBox.prototype = { symbol === Clutter.KEY_space) { this.activateSelectedWorkspace(); - return true; + return Clutter.EVENT_STOP; } if (symbol === Clutter.KEY_F2) { this.editWorkspaceTitle(); - return true; + return Clutter.EVENT_STOP; } if ((symbol === Clutter.KEY_o || symbol === Clutter.KEY_O) && modifiers & Clutter.ModifierType.CONTROL_MASK) { this.toggleGlobalOverviewMode(); - return true; + return Clutter.EVENT_STOP; } if (modifiers & ctrlAltMask) { - return false; + return Clutter.EVENT_PROPAGATE; } return this.selectNextWorkspace(symbol); - }, + } - editWorkspaceTitle: function() { + editWorkspaceTitle() { this.thumbnails[this.kbThumbnailIndex].title.grab_key_focus(); - }, + } - activateSelectedWorkspace: function() { + activateSelectedWorkspace() { this.thumbnails[this.kbThumbnailIndex].activateWorkspace(); - }, + } - removeSelectedWorkspace: function() { - this.thumbnails[this.kbThumbnailIndex].remove(); - }, + removeSelectedWorkspace() { + this.thumbnails[this.kbThumbnailIndex].removeWorkspace(); + } // returns true if symbol was understood, false otherwise - selectNextWorkspace: function(symbol) { + selectNextWorkspace(symbol) { let prevIndex = this.kbThumbnailIndex; let lastIndex = this.thumbnails.length - 1; - + let [nColumns, nRows] = this.getNumberOfColumnsAndRows(this.thumbnails.length); let nextIndex = GridNavigator.nextIndex(this.thumbnails.length, nColumns, prevIndex, symbol); if (nextIndex >= 0) { @@ -1233,79 +1245,78 @@ ExpoThumbnailsBox.prototype = { else { index = symbol - Clutter.KEY_KP_1; // convert Num-pad '1' to index 0, etc if (index < 0 || index > 9) { - return false; // not handled + return Clutter.EVENT_PROPAGATE; // not handled } } if (index > lastIndex) { - return true; // handled, but out of range + return Clutter.EVENT_STOP; // handled, but out of range } this.kbThumbnailIndex = index; this.activateSelectedWorkspace(); Main.wm.showWorkspaceOSD(); - return true; // handled + return Clutter.EVENT_STOP; // handled } if (prevIndex != this.kbThumbnailIndex) { this.thumbnails[prevIndex].showKeyboardSelectedState(false); this.thumbnails[this.kbThumbnailIndex].showKeyboardSelectedState(true); } - return true; // handled - }, + return Clutter.EVENT_STOP; // handled + } - hide: function() { - global.window_manager.disconnect(this.switchWorkspaceNotifyId); - global.workspace_manager.disconnect(this.workspaceAddedId); - global.workspace_manager.disconnect(this.workspaceRemovedId); + hide() { + global.window_manager.disconnectObject(this); + global.workspace_manager.disconnectObject(this); for (let w = 0; w < this.thumbnails.length; w++) { this.thumbnails[w].destroy(); } this.thumbnails = []; - }, + } - showButton: function(){ + showButton() { if (global.workspace_manager.n_workspaces <= 1) return false; - this.actor.queue_relayout(); + this.queue_relayout(); this.button.raise_top(); this.button.show(); return true; - }, + } - addThumbnails: function(start, count) { + addThumbnails(start, count) { function isInternalEvent(thumbnail, actor, event) { return actor === event.get_related() || - thumbnail.actor.contains(event.get_related()); + thumbnail.contains(event.get_related()); } for (let k = start; k < start + count; k++) { let metaWorkspace = global.workspace_manager.get_workspace_by_index(k); let thumbnail = new ExpoWorkspaceThumbnail(metaWorkspace, this); - + this.thumbnails.push(thumbnail); if (metaWorkspace == global.workspace_manager.get_active_workspace()) { this.lastActiveWorkspace = thumbnail; thumbnail.setActive(true); } let overviewTimeoutId = null; - let setOverviewTimeout = function(timeout, func) { - if (overviewTimeoutId) Mainloop.source_remove(overviewTimeoutId); + let setOverviewTimeout = (timeout, func) => { + if (overviewTimeoutId) GLib.source_remove(overviewTimeoutId); overviewTimeoutId = null; if (timeout && func) { - overviewTimeoutId = Mainloop.timeout_add(timeout, func); + overviewTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, timeout, func); } }; - thumbnail.actor.connect('destroy', Lang.bind(this, function(actor) { - setOverviewTimeout(0, function() { - overviewTimeoutId = 0; - }); - this.actor.remove_actor(thumbnail.frame); - this.actor.remove_actor(actor); - this.actor.remove_actor(thumbnail.title); + thumbnail.connect('destroy', () => { + setOverviewTimeout(0); + if (this.lastHovered === thumbnail) + this.lastHovered = null; + this.remove_child(thumbnail.title); + this.remove_child(thumbnail.frame); thumbnail.title.destroy(); - })); - this.actor.add_actor(thumbnail.frame); - this.actor.add_actor(thumbnail.actor); - this.actor.add_actor(thumbnail.title); + thumbnail.frame.destroy(); + }); + this.add_child(thumbnail.title); + this.add_child(thumbnail.frame); + this.add_child(thumbnail); // We use this as a flag to minimize the number of enter and leave events we really // have to deal with, since we get many spurious events when the mouse moves @@ -1313,22 +1324,22 @@ ExpoThumbnailsBox.prototype = { // jumping icons if there are minimized windows in a thumbnail. thumbnail.hovering = false; - thumbnail.connect('drag-over', Lang.bind(this, function () { + thumbnail.connect('drag-over', () => { thumbnail.highlight(); if (this.lastHovered && this.lastHovered != thumbnail) { this.lastHovered.shade(); } this.lastHovered = thumbnail; - })); + }); // We want to ignore spurious events caused by animations // (when the contents are moving and not the pointer). let pointerTracker = new PointerTracker.PointerTracker(); - thumbnail.actor.connect('motion-event', Lang.bind(this, function (actor, event) { + thumbnail.connect('motion-event', (actor, event) => { if (!pointerTracker.hasMoved()) {return;} if (!thumbnail.hovering) { thumbnail.hovering = true; - this.lastHovered = thumbnail; + this.lastHovered = thumbnail; this.showButton(); thumbnail.highlight(); setOverviewTimeout(POINTER_ENTER_MILLISECONDS_GRACE, function() { @@ -1336,11 +1347,12 @@ ExpoThumbnailsBox.prototype = { thumbnail.overviewModeOn(); } overviewTimeoutId = 0; + return GLib.SOURCE_REMOVE; }); } - })); - - thumbnail.actor.connect('leave-event', Lang.bind(this, function (actor, event) { + }); + + thumbnail.connect('leave-event', (actor, event) => { if (!pointerTracker.hasMoved()) {return;} if (this.isShowingModalDialog()) {return;} if (thumbnail.hovering && !isInternalEvent(thumbnail, actor, event)) { @@ -1352,13 +1364,14 @@ ExpoThumbnailsBox.prototype = { thumbnail.overviewModeOff(); } overviewTimeoutId = 0; + return GLib.SOURCE_REMOVE; }); } - })); + }); if (start > 0) { // not the initial fill thumbnail.state = ThumbnailState.NEW; - thumbnail.slidePosition = 1; // start slid out + thumbnail.slide_position = 1; } else { thumbnail.state = ThumbnailState.NORMAL; } @@ -1372,24 +1385,27 @@ ExpoThumbnailsBox.prototype = { else { this.queueUpdateStates(); } - }, + } - set scale(scale) { - this._scale = scale; - this.actor.queue_relayout(); - }, + set thumbnail_scale(scale) { + if (this._thumbnailScale === scale) + return; + this._thumbnailScale = scale; + this.notify('thumbnail-scale'); + this.queue_relayout(); + } - get scale() { - return this._scale; - }, + get thumbnail_scale() { + return this._thumbnailScale || 0; + } - setThumbnailState: function(thumbnail, state) { + setThumbnailState(thumbnail, state) { this.stateCounts[thumbnail.state]--; thumbnail.state = state; this.stateCounts[thumbnail.state]++; - }, + } - iterateStateThumbnails: function(state, callback) { + iterateStateThumbnails(state, callback) { if (this.stateCounts[state] == 0) return; @@ -1397,36 +1413,36 @@ ExpoThumbnailsBox.prototype = { if (this.thumbnails[i].state == state) callback.call(this, this.thumbnails[i]); } - }, + } - tweenScale: function() { - Tweener.addTween(this, - { scale: this.targetScale, - time: RESCALE_ANIMATION_TIME, - transition: 'easeOutQuad', - onComplete: this.queueUpdateStates, - onCompleteScope: this }); - }, + _animateThumbnailScale() { + this.ease_property('thumbnail-scale', this.targetScale, { + duration: RESCALE_ANIMATION_TIME, + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + onComplete: () => this.queueUpdateStates(), + }); + } - updateStates: function() { + updateStates() { this.stateUpdateQueued = false; + if (!this.thumbnails.length) + return; + // Then slide out any thumbnails that have been destroyed this.iterateStateThumbnails(ThumbnailState.REMOVING, function(thumbnail) { thumbnail.title.hide(); this.setThumbnailState(thumbnail, ThumbnailState.ANIMATING_OUT); - Tweener.addTween(thumbnail, - { slidePosition: 1, - time: SLIDE_ANIMATION_TIME / 1000, - transition: 'linear', - onComplete: function() { - this.setThumbnailState(thumbnail, ThumbnailState.ANIMATED_OUT); - this.queueUpdateStates(); - }, - onCompleteScope: this - }); + thumbnail.ease_property('slide-position', 1, { + duration: SLIDE_ANIMATION_TIME, + mode: Clutter.AnimationMode.LINEAR, + onComplete: () => { + this.setThumbnailState(thumbnail, ThumbnailState.ANIMATED_OUT); + this.queueUpdateStates(); + }, + }); }); // As long as things are sliding out, don't proceed @@ -1436,93 +1452,96 @@ ExpoThumbnailsBox.prototype = { // Once that's complete, we can start scaling to the new size and collapse any removed thumbnails this.iterateStateThumbnails(ThumbnailState.ANIMATED_OUT, function(thumbnail) { - this.actor.set_skip_paint(thumbnail.actor, true); - //this.title.set_skip_paint(thumbnail.title, true); + thumbnail.hide(); this.setThumbnailState(thumbnail, ThumbnailState.COLLAPSING); - Tweener.addTween(thumbnail, - { time: RESCALE_ANIMATION_TIME, - transition: 'easeOutQuad', - onComplete: function() { - this.stateCounts[thumbnail.state]--; - thumbnail.state = ThumbnailState.DESTROYED; - - let index = this.thumbnails.indexOf(thumbnail); - this.thumbnails.splice(index, 1); - thumbnail.destroy(); - - if (index < this.kbThumbnailIndex || - (index === this.kbThumbnailIndex && - index === this.thumbnails.length)) - { - --this.kbThumbnailIndex; - } - - this.queueUpdateStates(); - }, - onCompleteScope: this - }); + thumbnail._collapseId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, RESCALE_ANIMATION_TIME, () => { + thumbnail._collapseId = 0; + this.stateCounts[thumbnail.state]--; + thumbnail.state = ThumbnailState.DESTROYED; + + let index = this.thumbnails.indexOf(thumbnail); + this.thumbnails.splice(index, 1); + thumbnail.destroy(); + + if (index < this.kbThumbnailIndex || + (index === this.kbThumbnailIndex && + index === this.thumbnails.length)) + { + --this.kbThumbnailIndex; + } + + this.queueUpdateStates(); + return GLib.SOURCE_REMOVE; }); + }); if (this.pendingScaleUpdate) { - this.tweenScale(); + this._animateThumbnailScale(); this.pendingScaleUpdate = false; } // Wait until that's done - if (this._scale != this.targetScale || this.stateCounts[ThumbnailState.COLLAPSING] > 0) + if (this.thumbnail_scale != this.targetScale || this.stateCounts[ThumbnailState.COLLAPSING] > 0) return; // And then slide in any new thumbnails this.iterateStateThumbnails(ThumbnailState.NEW, function(thumbnail) { this.setThumbnailState(thumbnail, ThumbnailState.ANIMATING_IN); - Tweener.addTween(thumbnail, - { slidePosition: 0, - time: SLIDE_ANIMATION_TIME / 1000, - transition: 'easeOutQuad', - onComplete: function() { - this.setThumbnailState(thumbnail, ThumbnailState.NORMAL); - }, - onCompleteScope: this - }); + thumbnail.ease_property('slide-position', 0, { + duration: SLIDE_ANIMATION_TIME, + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + onComplete: () => { + this.setThumbnailState(thumbnail, ThumbnailState.NORMAL); + }, + }); }); this.iterateStateThumbnails(ThumbnailState.NORMAL, function(thumbnail) { thumbnail.refresh(); }); - this.thumbnails[this.kbThumbnailIndex].showKeyboardSelectedState(true); - if (!this.isShowingModalDialog()) { - // we may inadvertently have lost keyboard focus during the reshuffling - global.stage.set_key_focus(this.actor); + // Skip the keyboard-selected-state update (which triggers + // overviewModeOn on the active thumbnail) if expo is hiding or + // being cancelled mid-show. Otherwise this deferred later_add + // callback can reapply the spread layout right before tear-down. + if (!Main.expo._hideInProgress) { + this.thumbnails[this.kbThumbnailIndex].showKeyboardSelectedState(true); + if (!this.isShowingModalDialog()) { + // we may inadvertently have lost keyboard focus during the reshuffling + global.stage.set_key_focus(this); + } } - }, + } - isShowingModalDialog: function() { + isShowingModalDialog() { // the normal value is 1 while Expo is active return Main.modalCount > 1; - }, + } - queueUpdateStates: function() { + queueUpdateStates() { if (this.stateUpdateQueued) return; - Meta.later_add(Meta.LaterType.BEFORE_REDRAW, - Lang.bind(this, this.updateStates)); + this._updateStatesLaterId = Meta.later_add(Meta.LaterType.BEFORE_REDRAW, () => { + this._updateStatesLaterId = 0; + this.updateStates(); + return GLib.SOURCE_REMOVE; + }); this.stateUpdateQueued = true; - }, + } - getNumberOfColumnsAndRows: function(nWorkspaces) { + getNumberOfColumnsAndRows(nWorkspaces) { let asGrid = global.settings.get_boolean("workspace-expo-view-as-grid"); let nColumns = asGrid ? Math.ceil(Math.sqrt(nWorkspaces)) : nWorkspaces; let nRows = Math.ceil(nWorkspaces/nColumns); - - // in case of a very wide screen, we can try and optimize the screen + + // in case of a very wide screen, we can try and optimize the screen // utilization by switching the columns and rows, but only if there's a // big difference. If the user doesn't want a grid we are even more conservative. let divisor = 1.25; let screenRatio = global.screen_width / global.screen_height; - let boxRatio = this.box ? (this.box.x2 - this.box.x1) / (this.box.y2 - this.box.y1) : 1.6; + let boxRatio = this._allocBox ? (this._allocBox.x2 - this._allocBox.x1) / (this._allocBox.y2 - this._allocBox.y1) : 1.6; if (nWorkspaces <= Math.floor(screenRatio)) { return [1, nWorkspaces]; @@ -1531,82 +1550,62 @@ ExpoThumbnailsBox.prototype = { } else { return [nRows, nColumns]; } - }, + } - getPreferredHeight: function(actor, forWidth, alloc) { + vfunc_get_preferred_height(forWidth) { // See comment about this.background in _init() let themeNode = this.background.get_theme_node(); - forWidth = themeNode.adjust_for_width(forWidth); - // Note that for getPreferredWidth/Height we cheat a bit and skip propagating // the size request to our children because we know how big they are and know // that the actors aren't depending on the virtual functions being called. if (this.thumbnails.length == 0) - return; - - let spacing = this.actor.get_theme_node().get_length('spacing'); - let nWorkspaces = global.workspace_manager.n_workspaces; - let totalSpacing = (nWorkspaces - 1) * spacing; + return [0, 0]; - let avail = Main.layoutManager.primaryMonitor.width - totalSpacing; - - let [nColumns, nRows] = this.getNumberOfColumnsAndRows(nWorkspaces); - let scale = (avail / nColumns) / this.porthole.width; - - let height = Math.round(this.porthole.height * scale); - [alloc.min_size, alloc.natural_size] = - themeNode.adjust_preferred_height(400, + return themeNode.adjust_preferred_height(400, Main.layoutManager.primaryMonitor.height); - }, + } - getPreferredWidth: function(actor, forHeight, alloc) { + vfunc_get_preferred_width(forHeight) { // See comment about this.background in _init() let themeNode = this.background.get_theme_node(); if (this.thumbnails.length == 0) - return; + return [0, 0]; // We don't animate our preferred width, which is always reported according // to the actual number of current workspaces, we just animate within that - let spacing = this.actor.get_theme_node().get_length('spacing'); + let spacing = this.get_theme_node().get_length('spacing'); let nWorkspaces = global.workspace_manager.n_workspaces; let totalSpacing = (nWorkspaces - 1) * spacing; - let avail = Main.layoutManager.primaryMonitor.width - totalSpacing; - - let [nColumns, nRows] = this.getNumberOfColumnsAndRows(nWorkspaces); - let scale = (avail / nColumns) / this.porthole.width; - - let width = Math.round(this.porthole.width * scale); - let maxWidth = (width) * nWorkspaces; - [alloc.min_size, alloc.natural_size] = - themeNode.adjust_preferred_width(totalSpacing, Main.layoutManager.primaryMonitor.width); - }, + return themeNode.adjust_preferred_width(totalSpacing, Main.layoutManager.primaryMonitor.width); + } - allocate: function(actor, box, flags) { - this.box = box; + vfunc_allocate(box, flags) { + this.set_allocation(box, flags); + this._allocBox = box; let rtl = (St.Widget.get_default_direction () == St.TextDirection.RTL); - if (this.thumbnails.length == 0) // not visible + if (this.thumbnails.length == 0) return; let portholeWidth = this.porthole.width; let portholeHeight = this.porthole.height; - let spacing = this.actor.get_theme_node().get_length('spacing'); + let spacing = this.get_theme_node().get_length('spacing'); - // We must find out every setting that may affect the height of + // We must find out every setting that may affect the height of // the workspace title: let firstThumbnailTitleThemeNode = this.thumbnails[0].title.get_theme_node(); - let thTitleHeight = firstThumbnailTitleThemeNode.get_length('height'); + let thTitleHeight = firstThumbnailTitleThemeNode.get_length('height'); let thTitleTopPadding = firstThumbnailTitleThemeNode.get_padding(St.Side.TOP); let thTitleBottomPadding = firstThumbnailTitleThemeNode.get_padding(St.Side.BOTTOM); let thTitleMargin = thTitleBottomPadding; let thTitleBorderHeight = firstThumbnailTitleThemeNode.get_border_width(St.Side.BOTTOM) * 2; let extraHeight = thTitleHeight + thTitleTopPadding + thTitleBottomPadding + thTitleMargin + thTitleBorderHeight; - + // Compute the scale we'll need once everything is updated let nWorkspaces = this.thumbnails.length; let [nColumns, nRows] = this.getNumberOfColumnsAndRows(nWorkspaces); @@ -1621,23 +1620,24 @@ ExpoThumbnailsBox.prototype = { if (newScale != this.targetScale) { if (this.targetScale > 0) { - // We don't do the tween immediately because we need to observe the ordering + // We defer the scale animation because we need to observe the ordering // in queueUpdateStates - if workspaces have been removed we need to slide them // out as the first thing. this.targetScale = newScale; this.pendingScaleUpdate = true; } else { - this.targetScale = this._scale = newScale; + this.targetScale = newScale; + this.thumbnail_scale = newScale; } this.queueUpdateStates(); } - let thumbnailHeight = Math.round(portholeHeight * this._scale); - let thumbnailWidth = Math.round(portholeWidth * this._scale); + let thumbnailHeight = Math.round(portholeHeight * this.thumbnail_scale); + let thumbnailWidth = Math.round(portholeWidth * this.thumbnail_scale); let childBox = new Clutter.ActorBox(); - + let calcPaddingX = function(nCols) { let neededX = (thumbnailWidth * nCols) + (spacing * (nCols + 1)); let extraSpaceX = (box.x2 - box.x1) - neededX; @@ -1669,11 +1669,11 @@ ExpoThumbnailsBox.prototype = { // pixels. To make this work and not end up with a gap at the bottom, // we need some thumbnails to be 99 pixels and some 100 pixels height; // we compute an actual scale separately for each thumbnail. - let x1 = Math.round(x + (thumbnailWidth * thumbnail.slidePosition / 2)); + let x1 = Math.round(x + (thumbnailWidth * thumbnail.slide_position / 2)); let x2 = Math.round(x + thumbnailWidth); let y1, y2; - + y1 = y; y2 = y1 + thumbnailHeight; @@ -1684,9 +1684,9 @@ ExpoThumbnailsBox.prototype = { childBox.y1 = y1; childBox.y2 = y1 + portholeHeight; - let scale = this._scale * (1 - thumbnail.slidePosition); - thumbnail.actor.set_scale(scale, scale); - thumbnail.actor.allocate(childBox, flags); + let scale = this.thumbnail_scale * (1 - thumbnail.slide_position); + thumbnail.set_scale(scale, scale); + thumbnail.allocate(childBox, flags); let framethemeNode = thumbnail.frame.get_theme_node(); let borderWidth = framethemeNode.get_border_width(St.Side.BOTTOM); @@ -1694,10 +1694,10 @@ ExpoThumbnailsBox.prototype = { childBox.x2 = x2 + borderWidth; childBox.y1 = y1 - borderWidth; childBox.y2 = y2 + borderWidth; - thumbnail.frame.set_scale((1 - thumbnail.slidePosition), (1 - thumbnail.slidePosition)); + thumbnail.frame.set_scale((1 - thumbnail.slide_position), (1 - thumbnail.slide_position)); thumbnail.frame.allocate(childBox, flags); - let thumbnailx = Math.round(x + (thumbnailWidth * thumbnail.slidePosition / 2)); + let thumbnailx = Math.round(x + (thumbnailWidth * thumbnail.slide_position / 2)); childBox.x1 = Math.max(thumbnailx, thumbnailx + Math.round(thumbnailWidth/2) - Math.round(thumbnail.title.width/2)); childBox.x2 = Math.min(thumbnailx + thumbnailWidth, childBox.x1 + thumbnail.title.width); childBox.y1 = y + thumbnailHeight + thTitleMargin; @@ -1714,24 +1714,24 @@ ExpoThumbnailsBox.prototype = { let buttonHeight = this.button.get_theme_node().get_length('height'); let buttonOverlap = this.button.get_theme_node().get_length('-cinnamon-close-overlap'); - if (this.lastHovered && this.lastHovered.actor != null && !this.lastHovered.doomed){ - x = this.lastHovered.actor.allocation.x1 + ((this.lastHovered.actor.allocation.x2 - this.lastHovered.actor.allocation.x1) * this.lastHovered.actor.get_scale()[0]) - buttonOverlap; - y = this.lastHovered.actor.allocation.y1 - (buttonHeight - buttonOverlap); + if (this.lastHovered && !this.lastHovered.doomed){ + x = this.lastHovered.allocation.x1 + ((this.lastHovered.allocation.x2 - this.lastHovered.allocation.x1) * this.lastHovered.get_scale()[0]) - buttonOverlap; + y = this.lastHovered.allocation.y1 - (buttonHeight - buttonOverlap); } else { - this.button.hide(); + this.button.hide(); } childBox.x1 = x; childBox.x2 = childBox.x1 + buttonWidth; childBox.y1 = y; childBox.y2 = childBox.y1 + buttonHeight; - + this.button.allocate(childBox, flags); this.emit('allocated'); - }, + } - activeWorkspaceChanged: function(wm, from, to, direction) { + activeWorkspaceChanged(wm, from, to, direction) { this.thumbnails[this.kbThumbnailIndex].showKeyboardSelectedState(false); this.kbThumbnailIndex = global.workspace_manager.get_active_workspace_index(); this.thumbnails[this.kbThumbnailIndex].showKeyboardSelectedState(true); @@ -1750,20 +1750,6 @@ ExpoThumbnailsBox.prototype = { } thumbnail.setActive(true); this.lastActiveWorkspace = thumbnail; - }, - - onScrollEvent: function (actor, event) { - if (Main.expo.animationInProgress) - return; - - switch ( event.get_scroll_direction() ) { - case Clutter.ScrollDirection.UP: - Main.wm.actionMoveWorkspaceUp(); - break; - case Clutter.ScrollDirection.DOWN: - Main.wm.actionMoveWorkspaceDown(); - break; - } } -}; -Signals.addSignalMethods(ExpoThumbnailsBox.prototype); + +}); diff --git a/js/ui/hotCorner.js b/js/ui/hotCorner.js index cad855eac5..918faf23c2 100755 --- a/js/ui/hotCorner.js +++ b/js/ui/hotCorner.js @@ -158,12 +158,10 @@ class HotCorner extends Clutter.Actor { runAction(timestamp) { switch (this.action) { case 'expo': - if (!Main.expo.animationInProgress) - Main.expo.toggle(); + Main.expo.toggle(); break; case 'scale': - if (!Main.overview.animationInProgress) - Main.overview.toggle(); + Main.overview.toggle(); break; case 'desktop': global.workspace_manager.toggle_desktop(timestamp); diff --git a/js/ui/layout.js b/js/ui/layout.js index fd9fa6e688..64b4cd3e83 100644 --- a/js/ui/layout.js +++ b/js/ui/layout.js @@ -307,9 +307,6 @@ var LayoutManager = GObject.registerClass({ } _toggleExpo() { - if (Main.expo.animationInProgress) - return; - if (Main.overview.visible) { this._activationTime = Date.now() / 1000; Main.overview.hide(); diff --git a/js/ui/main.js b/js/ui/main.js index c7e0731821..455631ed4d 100644 --- a/js/ui/main.js +++ b/js/ui/main.js @@ -345,7 +345,7 @@ function start() { GioUnix.DesktopAppInfo.set_desktop_env('X-Cinnamon'); - // Clutter.get_default_backend().set_input_method(new InputMethod.InputMethod()); + lockdownSettings = new Gio.Settings({ schema_id: 'org.cinnamon.desktop.lockdown' }); new CinnamonPortalHandler(); cinnamonAudioSelectionDBusService = new AudioDeviceSelection.AudioDeviceSelectionDBus(); @@ -439,7 +439,7 @@ function start() { xdndHandler = new XdndHandler.XdndHandler(); osdWindowManager = new OsdWindow.OsdWindowManager(); - // This overview object is just a stub for non-user sessions + overview = new Overview.Overview(); expo = new Expo.Expo(); @@ -487,10 +487,6 @@ function start() { locatePointer = new LocatePointer.LocatePointer(); layoutManager.init(); - lockdownSettings = new Gio.Settings({ schema_id: 'org.cinnamon.desktop.lockdown' }); - - overview.init(); - expo.init(); _addXletDirectoriesToSearchPath(); _initUserSession(); diff --git a/js/ui/overview.js b/js/ui/overview.js index 5051273c51..dc50a3e376 100644 --- a/js/ui/overview.js +++ b/js/ui/overview.js @@ -1,10 +1,8 @@ // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- const Clutter = imports.gi.Clutter; +const GObject = imports.gi.GObject; const Gtk = imports.gi.Gtk; const Meta = imports.gi.Meta; -const Mainloop = imports.mainloop; -const Signals = imports.signals; -const Lang = imports.lang; const St = imports.gi.St; const Cinnamon = imports.gi.Cinnamon; @@ -22,25 +20,31 @@ const SwipeScrollDirection = WorkspacesView.SwipeScrollDirection; const SwipeScrollResult = WorkspacesView.SwipeScrollResult; -function Overview() { - this._init.apply(this, arguments); -} - -Overview.prototype = { - _init : function() { +var Overview = GObject.registerClass({ + Signals: { + 'overview-background-button-press': {}, + 'swipe-scroll-begin': {}, + 'swipe-scroll-end': { param_types: [GObject.TYPE_INT] }, + 'showing': {}, + 'shown': {}, + 'hiding': {}, + 'hidden': {}, + }, +}, class Overview extends GObject.Object { + _init() { + super._init(); this._spacing = 0; this._group = new St.Widget({ name: 'overview', reactive: true }); this._group._delegate = this; - this._group.connect('style-changed', - Lang.bind(this, function() { - let node = this._group.get_theme_node(); - let spacing = node.get_length('spacing'); - if (spacing != this._spacing) { - this._spacing = spacing; - } - })); + this._group.connect('style-changed', () => { + let node = this._group.get_theme_node(); + let spacing = node.get_length('spacing'); + if (spacing != this._spacing) { + this._spacing = spacing; + } + }); this._group.hide(); global.overlay_group.add_actor(this._group); @@ -51,7 +55,6 @@ Overview.prototype = { this.visible = false; // animating to overview, in overview, animating out this._shown = false; // show() and not hide() - this._shownTemporarily = false; // showTemporarily() and not hideTemporarily() this._modal = false; // have a modal grab this.animationInProgress = false; this._hideInProgress = false; @@ -60,29 +63,23 @@ Overview.prototype = { this._windowSwitchTimestamp = 0; this._lastActiveWorkspaceIndex = -1; this._lastHoveredWindow = null; - }, - // The members we construct that are implemented in JS might - // want to access the overview as Main.overview to connect - // signal handlers and so forth. So we create them after - // construction in this init() method. - init: function() { - Main.layoutManager.connect('monitors-changed', Lang.bind(this, this.hide)); - }, + Main.layoutManager.connect('monitors-changed', this.hide.bind(this)); + } - setScrollAdjustment: function(adjustment, direction) { + setScrollAdjustment(adjustment, direction) { this._scrollAdjustment = adjustment; if (this._scrollAdjustment == null) this._scrollDirection = SwipeScrollDirection.NONE; else this._scrollDirection = direction; - }, + } - _onButtonPress: function(actor, event) { - this.emit('overview-background-button-press', actor, event); + _onButtonPress(actor, event) { + this.emit('overview-background-button-press'); if (this._scrollDirection == SwipeScrollDirection.NONE || event.get_button() != 1) - return false; + return Clutter.EVENT_PROPAGATE; let [stageX, stageY] = event.get_coords(); this._dragStartX = this._dragX = stageX; @@ -90,12 +87,12 @@ Overview.prototype = { this._dragStartValue = this._scrollAdjustment.value; this._lastMotionTime = -1; // used to track "stopping" while swipe-scrolling this._capturedEventId = global.stage.connect('captured-event', - Lang.bind(this, this._onCapturedEvent)); + this._onCapturedEvent.bind(this)); this.emit('swipe-scroll-begin'); - return true; - }, + return Clutter.EVENT_STOP; + } - _onCapturedEvent: function(actor, event) { + _onCapturedEvent(actor, event) { let stageX, stageY; let threshold = Gtk.Settings.get_default().gtk_dnd_drag_threshold; @@ -166,8 +163,7 @@ Overview.prototype = { this._coverPane.raise_top(); this._coverPane.show(); - this._scrollAdjustment.ease({ - value: newValue, + this._scrollAdjustment.ease(newValue, { duration: ANIMATION_TIME, mode: Clutter.AnimationMode.EASE_OUT_QUAD, onComplete: () => { @@ -180,7 +176,9 @@ Overview.prototype = { global.stage.disconnect(this._capturedEventId); this._capturedEventId = 0; - return result != SwipeScrollResult.CLICK; + return result != SwipeScrollResult.CLICK + ? Clutter.EVENT_STOP + : Clutter.EVENT_PROPAGATE; case Clutter.EventType.MOTION: [stageX, stageY] = event.get_coords(); @@ -196,7 +194,7 @@ Overview.prototype = { // a drag if (Math.abs(stageX - this._dragStartX) < threshold && Math.abs(stageY - this._dragStartY) < threshold) - return true; + return Clutter.EVENT_STOP; if (this._scrollDirection == SwipeScrollDirection.HORIZONTAL) { if (St.Widget.get_default_direction() == St.TextDirection.RTL) @@ -207,25 +205,20 @@ Overview.prototype = { this._scrollAdjustment.value += (dy / primary.height) * this._scrollAdjustment.page_size; } - return true; + return Clutter.EVENT_STOP; // Block enter/leave events to avoid prelights // during swipe-scroll case Clutter.EventType.ENTER: case Clutter.EventType.LEAVE: - return true; + return Clutter.EVENT_STOP; } - return false; - }, - - //// Public methods //// + return Clutter.EVENT_PROPAGATE; + } - // show: - // - // Animates the overview visible and grabs mouse and keyboard input - show : function() { - if (this._shown) + show() { + if (this._shown || this.animationInProgress) return; // Do this manually instead of using _syncInputMode, to handle failure if (!Main.pushModal(this._group, undefined, undefined, Cinnamon.ActionMode.OVERVIEW)) @@ -235,10 +228,10 @@ Overview.prototype = { this._animateVisible(); this._buttonPressId = this._group.connect('button-press-event', - Lang.bind(this, this._onButtonPress)); - }, + this._onButtonPress.bind(this)); + } - _animateVisible: function() { + _animateVisible() { if (this.visible || this.animationInProgress) return; @@ -265,15 +258,14 @@ Overview.prototype = { this._group.add_actor(this._coverPane); this._coverPane.set_position(0, 0); this._coverPane.set_size(global.screen_width, global.screen_height); - this._coverPane.connect('event', () => true); + this._coverPane.connect('event', () => Clutter.EVENT_STOP); this._coverPane.hide(); - // Disable unredirection while in the overview Meta.disable_unredirect_for_display(global.display); this._group.show(); this.workspacesView = new WorkspacesView.WorkspacesView(); - global.overlay_group.add_actor(this.workspacesView.actor); + global.overlay_group.add_actor(this.workspacesView); Main.panelManager.disablePanels(); this._coverPane.raise_top(); @@ -287,65 +279,30 @@ Overview.prototype = { mode: Clutter.AnimationMode.EASE_OUT_QUAD, onComplete: () => this._showDone() }); - }, - - // showTemporarily: - // - // Animates the overview visible without grabbing mouse and keyboard input; - // if show() has already been called, this has no immediate effect, but - // will result in the overview not being hidden until hideTemporarily() is - // called. - showTemporarily: function() { - if (this._shownTemporarily) - return; - - this._syncInputMode(); - this._animateVisible(); - this._shownTemporarily = true; - }, + } - // hide: - // - // Reverses the effect of show() - hide: function() { + hide() { if (!this._shown) return; this._shown = false; - if (!this._shownTemporarily) - this._animateNotVisible(); + this._animateNotVisible(); this._syncInputMode(); if (this._buttonPressId > 0) this._group.disconnect(this._buttonPressId); this._buttonPressId = 0; - }, - - // hideTemporarily: - // - // Reverses the effect of showTemporarily() - hideTemporarily: function() { - if (!this._shownTemporarily) - return; - - if (!this._shown) - this._animateNotVisible(); - - this._shownTemporarily = false; - this._syncInputMode(); - }, + } - toggle: function() { + toggle() { if (this._shown) this.hide(); else this.show(); - }, - - //// Private methods //// + } - _syncInputMode: function() { + _syncInputMode() { // We delay input mode changes during animation so that when removing the // overview we don't have a problem with the release of a press/release // going to an application. @@ -359,12 +316,6 @@ Overview.prototype = { else this.hide(); } - } else if (this._shownTemporarily) { - if (this._modal) { - Main.popModal(this._group); - this._modal = false; - } - global.stage_input_mode = Cinnamon.StageInputMode.FULLSCREEN; } else { if (this._modal) { Main.popModal(this._group); @@ -373,9 +324,26 @@ Overview.prototype = { else if (global.stage_input_mode == Cinnamon.StageInputMode.FULLSCREEN) global.stage_input_mode = Cinnamon.StageInputMode.NORMAL; } - }, + } + + _animateNotVisible() { + if (this.animationInProgress && !this._hideInProgress) { + this._hideInProgress = true; + Main.panelManager.enablePanels(); + this.workspacesView.hide(); + this._coverPane.raise_top(); + this._coverPane.show(); + this.emit('hiding'); + let progress = this._group.opacity / 255; + this._group.ease({ + opacity: 0, + duration: Math.max(1, ANIMATION_TIME * 0.45 * progress), + mode: Clutter.AnimationMode.EASE_IN_QUAD, + onComplete: () => this._hideDone() + }); + return; + } - _animateNotVisible: function() { if (!this.visible || this.animationInProgress) return; @@ -389,29 +357,25 @@ Overview.prototype = { this._coverPane.show(); this.emit('hiding'); - // Make other elements fade out. this._group.ease({ opacity: 0, duration: ANIMATION_TIME, mode: Clutter.AnimationMode.EASE_IN_QUAD, onComplete: () => this._hideDone() }); - }, + } - _showDone: function() { + _showDone() { this.animationInProgress = false; this._coverPane.hide(); this.emit('shown'); - // Handle any calls to hide* while we were showing - if (!this._shown && !this._shownTemporarily) - this._animateNotVisible(); this._syncInputMode(); global.sync_pointer(); - }, + } - _hideDone: function() { + _hideDone() { this._group.remove_actor(this._coverPane); this._coverPane.destroy(); this._coverPane = null; @@ -420,7 +384,6 @@ Overview.prototype = { this._background.destroy(); this._background = null; - // Re-enable unredirection Meta.enable_unredirect_for_display(global.display); this.workspacesView.destroy(); @@ -432,14 +395,9 @@ Overview.prototype = { this.animationInProgress = false; this._hideInProgress = false; - this.emit('hidden'); - // Handle any calls to show* while we were hiding - if (this._shown || this._shownTemporarily) - this._animateVisible(); this._syncInputMode(); Main.layoutManager._chrome.updateRegions(); } -}; -Signals.addSignalMethods(Overview.prototype); +}); diff --git a/js/ui/workspace.js b/js/ui/workspace.js index d57c2e0dc3..ea56cc6414 100644 --- a/js/ui/workspace.js +++ b/js/ui/workspace.js @@ -1,13 +1,14 @@ // -*- 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 Gio = imports.gi.Gio; -const Mainloop = imports.mainloop; + const Meta = imports.gi.Meta; const Pango = imports.gi.Pango; const Cinnamon = imports.gi.Cinnamon; const St = imports.gi.St; -const Signals = imports.signals; const DND = imports.ui.dnd; const Main = imports.ui.main; @@ -36,16 +37,19 @@ function closeContextMenu(requestor) { return requestorShowingMenu; } -function WindowClone() { - this._init.apply(this, arguments); -} - -WindowClone.prototype = { - _init : function(realWindow, myContainer) { +var WindowClone = GObject.registerClass({ + Signals: { + 'activated': { param_types: [GObject.TYPE_UINT] }, + 'context-menu-requested': {}, + 'size-changed': {}, + }, +}, class WindowClone extends Clutter.Actor { + _init(realWindow, myContainer) { + super._init({ reactive: true }); + this._delegate = this; this.myContainer = myContainer; this.realWindow = realWindow; this.metaWindow = realWindow.meta_window; - this.metaWindow._delegate = this; this.overlay = null; this.closedFromOverview = false; this._is_new_window = false; // Window opened while in the overview @@ -58,37 +62,21 @@ WindowClone.prototype = { // the invisible border; this is inconvenient; rather than trying // to compensate all over the place we insert a ClutterGroup into // the hierarchy that is sized to only the visible portion. - this.actor = new Clutter.Actor({ reactive: true }); this.refreshClone(true); - this.actor._delegate = this; - this._stackAbove = null; - let sizeChangedId = this.realWindow.connect('notify::size', - this._onRealWindowSizeChanged.bind(this)); - let workspaceChangedId = this.metaWindow.connect('workspace-changed', - (w, oldws) => this.emit('workspace-changed', oldws)); - let realWindowDestroyId = 0; - this._disconnectWindowSignals = function() { - this._disconnectWindowSignals = function() {}; - this.metaWindow.disconnect(workspaceChangedId); - this.realWindow.disconnect(sizeChangedId); - this.realWindow.disconnect(realWindowDestroyId); - }; - realWindowDestroyId = this.realWindow.connect('destroy', - this._disconnectWindowSignals.bind(this)); - - this.actor.connect('button-release-event', this._onButtonRelease.bind(this)); - this.actor.connect('button-press-event', this._onButtonPress.bind(this)); + this.realWindow.connectObject( + 'notify::size', this._onRealWindowSizeChanged.bind(this), + this); - this.actor.connect('destroy', this._onDestroy.bind(this)); + this.connect('destroy', this._onDestroy.bind(this)); this._selected = false; - }, + } - refreshClone: function(withTransients) { - this.actor.destroy_all_children(); + refreshClone(withTransients) { + this.destroy_all_children(); let {x, y, width, height} = this.metaWindow.get_frame_rect(); let clones = WindowUtils.createWindowClone(this.metaWindow, 0, 0, withTransients); @@ -100,17 +88,17 @@ WindowClone.prototype = { clone.actor.set_clip(leftGap, topGap, width, height); } clone.actor.set_position(-leftGap, -topGap); - this.actor.add_actor(clone.actor); + this.add_child(clone.actor); } - this.actor.set_size(width, height); - this.actor.set_position(x, y); + this.set_size(width, height); + this.set_position(x, y); this.origX = x; this.origY = y; this.realWindow.queue_redraw(); - }, + } - closeWindow: function() { + closeWindow() { let workspace = this.metaWindow.get_workspace(); if (this._disconnectWindowAdded) {this._disconnectWindowAdded();} @@ -119,9 +107,10 @@ WindowClone.prototype = { if (win.get_transient_for() === this.metaWindow) { // use an idle handler to avoid mapping problems - // see comment in Workspace._windowAdded - Mainloop.idle_add(() => { - this.emit('activated'); - return false; + this._activateIdleId = GLib.idle_add(GLib.PRIORITY_DEFAULT_IDLE, () => { + this._activateIdleId = 0; + this.emit('activated', global.get_current_time()); + return GLib.SOURCE_REMOVE; }); } }); @@ -134,65 +123,59 @@ WindowClone.prototype = { this.closedFromOverview = true; this.metaWindow.delete(global.get_current_time()); - }, + } - setStackAbove: function (actor) { + setStackAbove(actor) { this._stackAbove = actor; if (this._stackAbove == null) - this.actor.lower_bottom(); + this.lower_bottom(); else - this.actor.raise(this._stackAbove); - }, - - destroy: function () { - if (this.actor.is_finalized()) return; - - this.actor.destroy(); - }, + this.raise(this._stackAbove); + } - _onRealWindowSizeChanged: function() { + _onRealWindowSizeChanged() { this.refreshClone(true); this.emit('size-changed'); - }, + } - _onDestroy: function() { - this._disconnectWindowSignals(); + _onDestroy() { if (this._disconnectWindowAdded) this._disconnectWindowAdded(); + if (this._activateIdleId) { + GLib.source_remove(this._activateIdleId); + this._activateIdleId = 0; + } - this.metaWindow._delegate = null; - this.actor._delegate = null; - - this.disconnectAll(); - }, + this._delegate = null; + } - _onButtonPress: function(actor, event) { + vfunc_button_press_event(event) { // a button-press on a clone already showing a menu should // not open a new-menu, only close the current menu. this.menuCancelled = closeContextMenu(this); - }, + return Clutter.EVENT_PROPAGATE; + } - _onButtonRelease: function(actor, event) { - switch (event.get_button()) { + vfunc_button_release_event(event) { + switch (event.button) { case 1: this._selected = true; this.emit('activated', global.get_current_time()); - return true; + return Clutter.EVENT_STOP; case 2: this.closedFromOverview = true; this.closeWindow(); - return true; + return Clutter.EVENT_STOP; case 3: if (!this.menuCancelled) { this.emit('context-menu-requested'); } this.menuCancelled = false; - return true; + return Clutter.EVENT_STOP; } - return false; + return Clutter.EVENT_PROPAGATE; } -}; -Signals.addSignalMethods(WindowClone.prototype); +}); /** @@ -200,12 +183,13 @@ Signals.addSignalMethods(WindowClone.prototype); * @parentActor: The actor which will be the parent of all overlay items * such as app icon and window caption */ -function WindowOverlay(windowClone, parentActor) { - this._init(windowClone, parentActor); -} - -WindowOverlay.prototype = { - _init : function(windowClone, parentActor) { +var WindowOverlay = GObject.registerClass({ + Signals: { + 'selected': { param_types: [GObject.TYPE_UINT] }, + }, +}, class WindowOverlay extends GObject.Object { + _init(windowClone, parentActor) { + super._init(); let metaWindow = windowClone.metaWindow; this._windowClone = windowClone; @@ -225,11 +209,9 @@ WindowOverlay.prototype = { icon_size: WINDOWOVERLAY_ICON_SIZE }); } - // Window border this.border = new St.Widget({ style_class: 'window-border', important: true }); this.borderWidth = 0; - // Caption (icon + title) let caption = new St.BoxLayout({ style_class: 'window-caption' }); caption._spacing = 0; let title = new St.Label({ text: metaWindow.title, y_align: Clutter.ActorAlign.CENTER }); @@ -241,7 +223,6 @@ WindowOverlay.prototype = { caption.add_actor(icon); caption.add_actor(title); - // Close button let button = new St.Button({ style_class: 'window-close' }); button.connect('clicked', () => this._windowClone.closeWindow()); button._overlap = 0; @@ -262,32 +243,29 @@ WindowOverlay.prototype = { button.connect('style-changed', styleChangedCallback); this._pointerTracker = new PointerTracker.PointerTracker(); - windowClone.actor.connect('motion-event', this._onPointerMotion.bind(this)); - windowClone.actor.connect('leave-event', this._onPointerLeave.bind(this)); + windowClone.connect('motion-event', this._onPointerMotion.bind(this)); + windowClone.connect('leave-event', this._onPointerLeave.bind(this)); this._idleToggleCloseId = 0; - windowClone.actor.connect('destroy', this._onDestroy.bind(this)); + windowClone.connect('destroy', this._onDestroy.bind(this)); let demandsAttentionCallback = this._onWindowDemandsAttention.bind(this); - let attentionId = global.display.connect('window-demands-attention', demandsAttentionCallback); - let urgentId = global.display.connect('window-marked-urgent', demandsAttentionCallback); - this.disconnectAttentionSignals = function() { - global.display.disconnect(attentionId); - global.display.disconnect(urgentId); - }; + global.display.connectObject( + 'window-demands-attention', demandsAttentionCallback, + 'window-marked-urgent', demandsAttentionCallback, this); // force a style change if we are already on a stage - otherwise // the signal will be emitted normally when we are added if (parentActor.get_stage()) this._onStyleChanged(); - }, + } - _onWindowDemandsAttention: function(display, metaWindow) { + _onWindowDemandsAttention(display, metaWindow) { if (metaWindow === this._windowClone.metaWindow) this.caption.add_style_class_name(DEMANDS_ATTENTION_CLASS_NAME); - }, + } - setSelected: function(selected, timeout) { + setSelected(selected, timeout) { if (this._isSelected === selected) return; this._isSelected = selected; @@ -304,22 +282,22 @@ WindowOverlay.prototype = { this._hideCloseButton(); } } - }, + } - hide: function() { + hide() { this._hidden = true; this._isSelected = false; this.caption.hide(); this.border.hide(); this.closeButton.hide(); - }, + } - show: function() { + show() { this._hidden = false; this.caption.show(); - }, + } - fadeIn: function() { + fadeIn() { if (!this._hidden) return; this.show(); this._parentActor.raise_top(); @@ -329,25 +307,25 @@ WindowOverlay.prototype = { duration: CLOSE_BUTTON_FADE_TIME, mode: Clutter.AnimationMode.EASE_OUT_QUAD }); - }, + } - _idleHideCloseButton: function(timeout) { + _idleHideCloseButton(timeout) { if (this._idleToggleCloseId === 0) - this._idleToggleCloseId = Mainloop.timeout_add(timeout, this._idleToggleCloseButton.bind(this)); - }, + this._idleToggleCloseId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, timeout, this._idleToggleCloseButton.bind(this)); + } - _idleToggleCloseButton: function() { + _idleToggleCloseButton() { this._idleToggleCloseId = 0; - if (!this._windowClone.actor.has_pointer && !this.closeButton.has_pointer) { + if (!this._windowClone.has_pointer && !this.closeButton.has_pointer) { this._isSelected = false; this._hideCloseButton(); } - return false; - }, + return GLib.SOURCE_REMOVE; + } - _hideCloseButton: function() { + _hideCloseButton() { if (this._idleToggleCloseId > 0) { - Mainloop.source_remove(this._idleToggleCloseId); + GLib.source_remove(this._idleToggleCloseId); this._idleToggleCloseId = 0; } for (let item of [this.closeButton, this.border]) { @@ -360,9 +338,9 @@ WindowOverlay.prototype = { }); } this.caption.remove_style_pseudo_class('focus'); - }, + } - _showCloseButton: function() { + _showCloseButton() { this._parentActor.raise_top(); for (let item of [this.closeButton, this.border]) { item.show(); @@ -374,19 +352,19 @@ WindowOverlay.prototype = { }); } this.caption.add_style_pseudo_class('focus'); - }, + } - chromeWidths: function () { + chromeWidths() { /* Reserve space for the close button on both sides, because we don't know on which side it is. Also to horizontally center the window. */ let close_buttn = Math.max(this.borderWidth, this.closeButton.width - this.closeButton._overlap) return [close_buttn, close_buttn]; - }, + } - chromeHeights: function () { + chromeHeights() { return [Math.max(this.closeButton.height - this.closeButton._overlap, this.borderWidth), this.caption.height + this.caption._spacing]; - }, + } /** * @cloneX: x position of windowClone @@ -398,7 +376,7 @@ WindowOverlay.prototype = { // get_transformed_position() and get_transformed_size(), // as windowClone might be moving. // See Workspace._showWindowOverlay - updatePositions: function(cloneX, cloneY, cloneWidth, cloneHeight, maxWidth) { + updatePositions(cloneX, cloneY, cloneWidth, cloneHeight, maxWidth) { let border = this.border; let caption = this.caption; let button = this.closeButton; @@ -432,24 +410,23 @@ WindowOverlay.prototype = { let captionY = cloneY + cloneHeight + caption._spacing; caption.set_position(Math.round(captionX), Math.round(captionY)); caption.width = captionWidth; - }, + } - _onDestroy: function() { - if (this._disconnectWindowAdded) {this._disconnectWindowAdded();} + _onDestroy() { if (this._idleToggleCloseId > 0) { - Mainloop.source_remove(this._idleToggleCloseId); + GLib.source_remove(this._idleToggleCloseId); this._idleToggleCloseId = 0; } - this.disconnectAttentionSignals(); + global.display.disconnectObject(this); this._windowClone.metaWindow.disconnect(this._updateCaptionId); this.border.destroy(); this.caption.destroy(); this.closeButton.destroy(); this.border = this.caption = this.closeButton = null; - }, + } - _onPointerMotion: function() { + _onPointerMotion() { if (!this._pointerTracker.hasMoved()) {return;} // We might get motion events on the clone while the overlay is // hidden, e.g. during animations, we ignore these events, @@ -457,15 +434,15 @@ WindowOverlay.prototype = { // are shown again if (this._hidden) return; this.emit('selected', global.get_current_time()); - }, + } - _onPointerLeave: function() { + _onPointerLeave() { if (!this._pointerTracker.hasMoved()) {return;} this.setSelected(false, 750); - }, + } - _onStyleChanged: function() { + _onStyleChanged() { let titleNode = this.caption.get_theme_node(); this.caption._spacing = titleNode.get_length('-cinnamon-caption-spacing'); @@ -477,20 +454,18 @@ WindowOverlay.prototype = { this._parentActor.queue_relayout(); } -}; -Signals.addSignalMethods(WindowOverlay.prototype); +}); -const WindowPositionFlags = { +var WindowPositionFlags = { INITIAL: 1 << 0, ANIMATE: 1 << 1 }; -function WorkspaceMonitor() { - this._init.apply(this, arguments); -} - -WorkspaceMonitor.prototype = { - _init : function(metaWorkspace, monitorIndex, workspace) { +var WorkspaceMonitor = GObject.registerClass( +class WorkspaceMonitor extends Clutter.Actor { + _init(metaWorkspace, monitorIndex, workspace) { + super._init({ layout_manager: new Clutter.FixedLayout() }); + this.set_size(0, 0); this._myWorkspace = workspace; this.metaWorkspace = metaWorkspace; @@ -500,32 +475,31 @@ WorkspaceMonitor.prototype = { this._height = 0; this._margin = 0; this._slotWidth = 0; + this._addWindowIdleIds = new Set(); this.monitorIndex = monitorIndex; this._monitor = Main.layoutManager.monitors[this.monitorIndex]; this._windowOverlaysGroup = new Clutter.Group(); // Without this the drop area will be overlapped. this._windowOverlaysGroup.set_size(0, 0); - - this.actor = new Clutter.Group(); - this.actor.set_size(0, 0); + this._windowOverlaysGroup.connect('destroy', () => { + this._windowOverlaysGroup = null; + }); this._dropRect = new Clutter.Rectangle({ opacity: 0 }); this._dropRect._delegate = this; - this.actor.add_actor(this._dropRect); - this.actor.add_actor(this._windowOverlaysGroup); + this.add_child(this._dropRect); + this.add_child(this._windowOverlaysGroup); - this.actor.connect('destroy', this._onDestroy.bind(this)); - Main.overview.connect('overview-background-button-press', closeContextMenu); + this.connect('destroy', this._onDestroy.bind(this)); + + // TODO: Get rid of this, use push/popModal for the context menu. + Main.overview.connectObject( + 'overview-background-button-press', closeContextMenu, this); - this.stickyCallbackId = workspace.myView.connect('sticky-detected', (box, metaWindow) => { - this._doAddWindow(metaWindow); - }); let windows = global.get_window_actors().filter(this._isMyWindow, this); - // Create clones for windows that should be - // visible in the Overview this._windows = []; for (let i = 0; i < windows.length; i++) { if (this._isOverviewWindow(windows[i])) { @@ -538,27 +512,24 @@ WorkspaceMonitor.prototype = { child: new St.Label({ text: _("No open windows") }), important: true }); - this.actor.insert_child_below(this._emptyPlaceHolder, null); + this.insert_child_below(this._emptyPlaceHolder, null); - // Track window changes if (this.metaWorkspace) { - this._windowAddedId = this.metaWorkspace.connect('window-added', - this._windowAdded.bind(this)); - this._windowRemovedId = this.metaWorkspace.connect('window-removed', - this._windowRemoved.bind(this)); + this.metaWorkspace.connectObject( + 'window-added', this._windowAdded.bind(this), + 'window-removed', this._windowRemoved.bind(this), this); } - this._windowEnteredMonitorId = global.display.connect('window-entered-monitor', - this._windowEnteredMonitor.bind(this)); - this._windowLeftMonitorId = global.display.connect('window-left-monitor', - this._windowLeftMonitor.bind(this)); + global.display.connectObject( + 'window-entered-monitor', this._windowEnteredMonitor.bind(this), + 'window-left-monitor', this._windowLeftMonitor.bind(this), this); this._animating = false; // Indicate if windows are being repositioned this.leavingOverview = false; this._kbWindowIndex = 0; // index of the current keyboard-selected window - }, + } - selectAnotherWindow: function(symbol) { + selectAnotherWindow(symbol) { let numWindows = this._windows.length; if (numWindows === 0) { return false; @@ -572,85 +543,85 @@ WorkspaceMonitor.prototype = { this.selectIndex(nextIndex); return true; - }, + } - showActiveSelection: function() { + showActiveSelection() { this.selectIndex(this._kbWindowIndex); - }, + } - selectIndex: function(index) { + selectIndex(index) { this._kbWindowIndex = index; let activeClone = null; if (index > -1 && index < this._windows.length) { activeClone = this._windows[this._kbWindowIndex]; } this._myWorkspace.selectActiveClone(activeClone, this); - }, + } - selectClone: function(clone) { + selectClone(clone) { this.selectIndex(this._windows.indexOf(clone)); - }, + } - _onCloneContextMenuRequested: function(clone) { - menuShowing = new WindowContextMenu(clone.actor, clone.metaWindow, () => { + _onCloneContextMenuRequested(clone) { + menuShowing = new WindowContextMenu(clone, clone.metaWindow, () => { menuShowing = null; menuClone = null; this._myWorkspace.emit('focus-refresh-required'); }); menuClone = clone; menuShowing.toggle(); - }, + } - showMenuForSelectedWindow: function() { + showMenuForSelectedWindow() { if (this._kbWindowIndex > -1 && this._kbWindowIndex < this._windows.length) { let window = this._windows[this._kbWindowIndex]; this._onCloneContextMenuRequested(window); } return false; - }, + } - activateSelectedWindow: function() { + activateSelectedWindow() { if (this._kbWindowIndex > -1 && this._kbWindowIndex < this._windows.length) { this._onCloneActivated(this._windows[this._kbWindowIndex], global.get_current_time()); return true; } return false; - }, + } - closeSelectedWindow: function() { + closeSelectedWindow() { if (this._kbWindowIndex > -1 && this._kbWindowIndex < this._windows.length) { this._windows[this._kbWindowIndex].closeWindow(); } - }, + } - moveSelectedWindowToNextMonitor: function() { + moveSelectedWindowToNextMonitor() { if (this._kbWindowIndex > -1 && this._kbWindowIndex < this._windows.length) { let monitorCount = Main.layoutManager.monitors.length; if (monitorCount < 2) return; let nextIndex = (this._windows[this._kbWindowIndex].metaWindow.get_monitor() + monitorCount + 1) % monitorCount; this._windows[this._kbWindowIndex].metaWindow.move_to_monitor(nextIndex); } - }, + } - setGeometry: function(x, y, width, height, margin) { + setGeometry(x, y, width, height, margin) { this._x = x; this._y = y; this._width = width; this._height = height; this._margin = margin; - }, + } - _lookupIndex: function (metaWindow) { + _lookupIndex(metaWindow) { for (let i = 0; i < this._windows.length; i++) { if (this._windows[i].metaWindow == metaWindow) { return i; } } return -1; - }, + } - isEmpty: function() { + isEmpty() { return this._windows.length === 0; - }, + } /** * _getSlotGeometry: @@ -659,7 +630,7 @@ WorkspaceMonitor.prototype = { * Returns: the screen-relative [x, y, width, height] * of a given window layout slot. */ - _getSlotGeometry: function(slot) { + _getSlotGeometry(slot) { let [xCenter, yCenter, xFraction, yFraction] = slot; let width = (this._width - this._margin * 2) * xFraction; @@ -669,7 +640,7 @@ WorkspaceMonitor.prototype = { let y = this._y + this._margin + yCenter * (this._height - this._margin * 2) - height / 2; return [x, y, width, height]; - }, + } /** * _computeWindowLayout: @@ -680,7 +651,7 @@ WorkspaceMonitor.prototype = { * screen-relative [x, y, scale] where scale applies * to both X and Y directions. */ - _computeWindowLayout: function(metaWindow, slot) { + _computeWindowLayout(metaWindow, slot) { let [x, y, width, height] = this._getSlotGeometry(slot); let rect = metaWindow.get_frame_rect(); let topBorder = 0, bottomBorder = 0, leftBorder = 0, rightBorder = 0; @@ -696,7 +667,7 @@ WorkspaceMonitor.prototype = { x = Math.floor(x + (width - scale * rect.width - rightBorder + leftBorder) / 2); y = Math.floor(y + (height - scale * rect.height - bottomBorder + topBorder) / 2); return [x, y, scale]; - }, + } /** * positionWindows: @@ -704,7 +675,7 @@ WorkspaceMonitor.prototype = { * INITIAL - this is the initial positioning of the windows. * ANIMATE - Indicates that we need animate changing position. */ - positionWindows : function(flags) { + positionWindows(flags) { if (Main.expo.visible) return; @@ -739,28 +710,27 @@ WorkspaceMonitor.prototype = { * therefore we need to resize them now so they * can be scaled up later */ if (initialPositioning) { - clone.actor.opacity = 0; - clone.actor.scale_x = 0; - clone.actor.scale_y = 0; - clone.actor.x = this._width / 2; - clone.actor.y = this._height / 2; + clone.opacity = 0; + clone.scale_x = 0; + clone.scale_y = 0; + clone.x = this._width / 2; + clone.y = this._height / 2; } else if (clone._is_new_window) { - clone.actor.opacity = 0; - clone.actor.scale_x = 0; - clone.actor.scale_y = 0; - clone.actor.x = x + clone.actor.width * scale / 2; - clone.actor.y = y + clone.actor.height * scale / 2; + clone.opacity = 0; + clone.scale_x = 0; + clone.scale_y = 0; + clone.x = x + clone.width * scale / 2; + clone.y = y + clone.height * scale / 2; } - // Make the window slightly transparent to indicate it's hidden - clone.actor.ease({ + clone.ease({ opacity: 255, duration: Overview.ANIMATION_TIME, mode: Clutter.AnimationMode.EASE_IN_QUAD }); } - clone.actor.ease({ + clone.ease({ x: x, y: y, scale_x: scale, @@ -773,16 +743,16 @@ WorkspaceMonitor.prototype = { } }); } else { - clone.actor.set_position(x, y); - clone.actor.set_scale(scale, scale); + clone.set_position(x, y); + clone.set_scale(scale, scale); this._showWindowOverlay(clone, isOnCurrentWorkspace); } clone._is_new_window = false; } - }, + } - syncStacking: function(stackIndices) { + syncStacking(stackIndices) { // Only on the first invocation do we want to affect the // permanent sort order. After that, we don't want major // upheavals to the sort order. @@ -800,26 +770,19 @@ WorkspaceMonitor.prototype = { for (let i = clones.length - 1; i >= 0; i--) { let clone = clones[i]; clone.setStackAbove(below); - below = clone.actor; + below = clone; } - }, + } - _showWindowOverlay: function(clone, fade) { - // Prevent showing overlay now. Sometimes called during animations + _showWindowOverlay(clone, fade) { if (this._animating) return; if (this._slotWidth) { - // This is a little messy and complicated because when we - // start the fade-in we may not have done the final positioning - // of the workspaces. (Tweener doesn't necessarily finish - // all animations before calling onComplete callbacks.) - // So we need to manually compute where the window will - // be after the workspace animation finishes. - let [cloneX, cloneY] = clone.actor.get_position(); - let [cloneWidth, cloneHeight] = clone.actor.get_size(); - cloneWidth = clone.actor.scale_x * cloneWidth; - cloneHeight = clone.actor.scale_y * cloneHeight; + let [cloneX, cloneY] = clone.get_position(); + let [cloneWidth, cloneHeight] = clone.get_size(); + cloneWidth = clone.scale_x * cloneWidth; + cloneHeight = clone.scale_y * cloneHeight; clone.overlay.updatePositions(cloneX, cloneY, cloneWidth, cloneHeight, this._slotWidth); if (fade) @@ -828,29 +791,29 @@ WorkspaceMonitor.prototype = { clone.overlay.show(); this._myWorkspace.emit('focus-refresh-required'); } - }, + } - _showAllOverlays: function() { + _showAllOverlays() { let currentWorkspace = global.workspace_manager.get_active_workspace(); let fade = this.metaWorkspace == null || this.metaWorkspace === currentWorkspace; for (let clone of this._windows) { this._showWindowOverlay(clone, fade); } - }, + } - showWindowsOverlays: function() { - if (this.leavingOverview || this._windowOverlaysGroup.is_finalized()) + showWindowsOverlays() { + if (this.leavingOverview || !this._windowOverlaysGroup) return; this._windowOverlaysGroup.show(); this._showAllOverlays(); - }, + } - hideWindowsOverlays: function() { + hideWindowsOverlays() { this._windowOverlaysGroup.hide(); - }, + } - _updateEmptyPlaceholder: function() { + _updateEmptyPlaceholder() { let placeholder = this._emptyPlaceHolder; if (this._windows.length > 0) { placeholder.hide(); @@ -860,18 +823,16 @@ WorkspaceMonitor.prototype = { placeholder.set_position(x, y); placeholder.show(); } - }, + } - _doRemoveWindow : function(metaWin) { + _doRemoveWindow(metaWin) { let win = metaWin.get_compositor_private(); - // find the position of the window in our list let index = this._lookupIndex (metaWin); if (index == -1) return; - // Check if window still should be here if (win && this._isMyWindow(win)) return; @@ -898,9 +859,9 @@ WorkspaceMonitor.prototype = { this.positionWindows(animate ? WindowPositionFlags.ANIMATE : 0); } - }, + } - _doAddWindow : function(metaWin) { + _doAddWindow(metaWin) { if (this.leavingOverview) return; @@ -908,13 +869,14 @@ WorkspaceMonitor.prototype = { if (!win) { // Newly-created windows are added to a workspace before // the compositor finds out about them... - Mainloop.idle_add(() => { - if (this.actor && - metaWin.get_compositor_private() && + let id = GLib.idle_add(GLib.PRIORITY_DEFAULT_IDLE, () => { + this._addWindowIdleIds.delete(id); + if (metaWin.get_compositor_private() && metaWin.get_workspace() === this.metaWorkspace) this._doAddWindow(metaWin); - return false; + return GLib.SOURCE_REMOVE; }); + this._addWindowIdleIds.add(id); return; } @@ -931,35 +893,34 @@ WorkspaceMonitor.prototype = { this._updateEmptyPlaceholder(); - if (this.actor.get_stage()) { + if (this.get_stage()) { clone._is_new_window = true; let animate = Main.animations_enabled; this.positionWindows(animate ? WindowPositionFlags.ANIMATE : 0); } - }, + } - _windowAdded : function(metaWorkspace, metaWin) { + _windowAdded(metaWorkspace, metaWin) { this._doAddWindow(metaWin); - }, + } - _windowRemoved : function(metaWorkspace, metaWin) { + _windowRemoved(metaWorkspace, metaWin) { this._doRemoveWindow(metaWin); - }, + } - _windowEnteredMonitor : function(metaDisplay, monitorIndex, metaWin) { + _windowEnteredMonitor(metaDisplay, monitorIndex, metaWin) { if (monitorIndex === this.monitorIndex) { this._doAddWindow(metaWin); } - }, + } - _windowLeftMonitor : function(metaDisplay, monitorIndex, metaWin) { + _windowLeftMonitor(metaDisplay, monitorIndex, metaWin) { if (monitorIndex === this.monitorIndex) { this._doRemoveWindow(metaWin); } - }, + } - // check for maximized windows on the workspace - hasMaximizedWindows: function() { + hasMaximizedWindows() { for (let i = 0; i < this._windows.length; i++) { let metaWindow = this._windows[i].metaWindow; if (metaWindow.showing_on_its_workspace() && @@ -968,30 +929,30 @@ WorkspaceMonitor.prototype = { return true; } return false; - }, + } - // Animate the full-screen to Overview transition. - zoomToOverview : function() { + zoomToOverview() { let animate = Main.animations_enabled; - // Position and scale the windows. + if (Main.overview.animationInProgress && animate) this.positionWindows(WindowPositionFlags.ANIMATE | WindowPositionFlags.INITIAL); else this.positionWindows(WindowPositionFlags.INITIAL); this._updateEmptyPlaceholder(); - }, + } - // Animates the return from Overview mode - zoomFromOverview : function() { + zoomFromOverview() { let currentWorkspace = global.workspace_manager.get_active_workspace(); this.leavingOverview = true; this.hideWindowsOverlays(); - this._overviewHiddenId = Main.overview.connect('hidden', - this._doneLeavingOverview.bind(this)); + if (!this._overviewHiddenId) { + this._overviewHiddenId = Main.overview.connect('hidden', + this._doneLeavingOverview.bind(this)); + } if (this.metaWorkspace != null && this.metaWorkspace != currentWorkspace) return; @@ -1005,7 +966,7 @@ WorkspaceMonitor.prototype = { let clone = this._windows[i]; if (clone.metaWindow.showing_on_its_workspace()) { - clone.actor.ease({ + clone.ease({ x: clone.origX, y: clone.origY, scale_x: 1.0, @@ -1016,7 +977,7 @@ WorkspaceMonitor.prototype = { }); } else { // The window is hidden, make it shrink and fade it out - clone.actor.ease({ + clone.ease({ scale_x: 0, scale_y: 0, x: this._width / 2, @@ -1035,71 +996,49 @@ WorkspaceMonitor.prototype = { mode: Clutter.AnimationMode.EASE_OUT_QUAD }); } - }, - - destroy : function() { - this.actor.destroy(); - }, + } - _onDestroy: function(actor) { + _onDestroy(actor) { closeContextMenu(); if (this._overviewHiddenId) { Main.overview.disconnect(this._overviewHiddenId); this._overviewHiddenId = 0; } + if (this._addWindowIdleIds) { + this._addWindowIdleIds.forEach(id => GLib.source_remove(id)); + this._addWindowIdleIds.clear(); + } actor.remove_all_transitions(); + } - this._myWorkspace.myView.disconnect(this.stickyCallbackId); - if (this.metaWorkspace) { - this.metaWorkspace.disconnect(this._windowAddedId); - this.metaWorkspace.disconnect(this._windowRemovedId); - } - global.display.disconnect(this._windowEnteredMonitorId); - global.display.disconnect(this._windowLeftMonitorId); - - // Usually, the windows will be destroyed automatically with - // their parent (this.actor), but we might have a zoomed window - // which has been reparented to the stage - _windows[0] holds - // the desktop window, which is never reparented - for (let w = 0; w < this._windows.length; w++) - this._windows[w].destroy(); - this._windows = []; - }, - - // Sets this.leavingOverview flag to false. - _doneLeavingOverview : function() { + _doneLeavingOverview() { this.leavingOverview = false; - }, + } - // Tests if @win belongs to this workspace - _isMyWindow : function (win) { + _isMyWindow(win) { return (this.metaWorkspace == null || Main.isWindowActorDisplayedOnWorkspace(win, this.metaWorkspace.index()) && (!win.get_meta_window() || win.get_meta_window().get_monitor() == this.monitorIndex)); - }, + } - // Tests if @win should be shown in the Overview - _isOverviewWindow : function (win) { + _isOverviewWindow(win) { let tracker = Cinnamon.WindowTracker.get_default(); return Main.isInteresting(win.get_meta_window()); - }, + } // Create a clone of a (non-desktop) window and add it to the window list - _addWindowClone : function(win) { + _addWindowClone(win) { let clone = new WindowClone(win, this); let overlay = new WindowOverlay(clone, this._windowOverlaysGroup); - clone.connect('workspace-changed', () => { - this._doRemoveWindow(clone.metaWindow); - if (clone.metaWindow.is_on_all_workspaces()) { - // Muffin appears not to broadcast when a window turns sticky - this._myWorkspace.myView.emit('sticky-detected', clone.metaWindow); - } - }); - clone.connect('activated', this._onCloneActivated.bind(this)); clone.connect('context-menu-requested', this._onCloneContextMenuRequested.bind(this)); clone.connect('size-changed', () => { this.positionWindows(0) }); + clone.connect('destroy', () => { + let idx = this._windows.indexOf(clone); + if (idx >= 0) + this._windows.splice(idx, 1); + }); - this.actor.add_actor(clone.actor); + this.add_child(clone); overlay.connect('selected', this.selectClone.bind(this, clone)); @@ -1107,9 +1046,9 @@ WorkspaceMonitor.prototype = { clone.overlay = overlay; return clone; - }, + } - _computeAllWindowSlots: function(numberOfWindows) { + _computeAllWindowSlots(numberOfWindows) { if (numberOfWindows <= 0) return []; let gridWidth = Math.ceil(Math.sqrt(numberOfWindows)); @@ -1118,7 +1057,6 @@ WorkspaceMonitor.prototype = { let yFraction = DEFAULT_SLOT_FRACTION / gridHeight; this._slotWidth = Math.floor(xFraction * (this._width - this._margin * 2)); - // Arrange the windows in a grid pattern. let slots = []; for (let i = 0; i < numberOfWindows; i++) { let xCenter = (0.5 + i % gridWidth) / gridWidth; @@ -1135,27 +1073,19 @@ WorkspaceMonitor.prototype = { } return slots; - }, + } - _onCloneActivated : function (clone, time) { + _onCloneActivated(clone, time) { let wsIndex = undefined; if (this.metaWorkspace) wsIndex = this.metaWorkspace.index(); Main.activateWindow(clone.metaWindow, time, wsIndex); } -}; - -Signals.addSignalMethods(WorkspaceMonitor.prototype); +}); -function WindowContextMenu(actor, metaWindow, onClose) { - this._init(actor, metaWindow, onClose); -} - -WindowContextMenu.prototype = { - __proto__: PopupMenu.PopupComboMenu.prototype, - - _init: function(actor, metaWindow, onClose) { - PopupMenu.PopupComboMenu.prototype._init.call(this, actor); +var WindowContextMenu = class WindowContextMenu extends PopupMenu.PopupComboMenu { + constructor(actor, metaWindow, onClose) { + super(actor); this.name = 'scale-window-context-menu'; Main.uiGroup.add_actor(this.actor); this.actor.hide(); @@ -1222,14 +1152,14 @@ WindowContextMenu.prototype = { this.addMenuItem(item); }); this.setActiveItem(0); - }, + } - _onToggled: function(actor, opening){ - if (!opening) { + _onToggled(actor, opening) { + if (!opening) { this.onClose(); this.destroy(); return; - } + } if (this.metaWindow.is_on_all_workspaces()) { this.itemOnAllWorkspaces.label.set_text(_("Only on this workspace")); @@ -1251,81 +1181,80 @@ WindowContextMenu.prototype = { }else{ this.itemMaximizeWindow.label.set_text(_("Maximize")); } - }, + } - _onCloseWindowActivate: function(actor, event){ + _onCloseWindowActivate(actor, event) { this.metaWindow.delete(global.get_current_time()); - }, + } - _onMinimizeWindowActivate: function(actor, event){ + _onMinimizeWindowActivate(actor, event) { if (this.metaWindow.minimized) { this.metaWindow.unminimize(global.get_current_time()); } else { this.metaWindow.minimize(global.get_current_time()); } - }, + } - _onMaximizeWindowActivate: function(actor, event){ + _onMaximizeWindowActivate(actor, event) { if (this.metaWindow.get_maximized()){ this.metaWindow.unmaximize(Meta.MaximizeFlags.HORIZONTAL | Meta.MaximizeFlags.VERTICAL); }else{ this.metaWindow.maximize(Meta.MaximizeFlags.HORIZONTAL | Meta.MaximizeFlags.VERTICAL); } - }, + } - _onMoveToLeftWorkspace: function(actor, event){ + _onMoveToLeftWorkspace(actor, event) { let workspace = this.metaWindow.get_workspace().get_neighbor(Meta.MotionDirection.LEFT); if (workspace) { this.metaWindow.change_workspace(workspace); } - }, + } - _onMoveToRightWorkspace: function(actor, event){ + _onMoveToRightWorkspace(actor, event) { let workspace = this.metaWindow.get_workspace().get_neighbor(Meta.MotionDirection.RIGHT); if (workspace) { this.metaWindow.change_workspace(workspace); } - }, + } - _toggleOnAllWorkspaces: function(actor, event) { + _toggleOnAllWorkspaces(actor, event) { if (this.metaWindow.is_on_all_workspaces()) this.metaWindow.unstick(); else this.metaWindow.stick(); - }, + } - _onSourceKeyPress: function(actor, event) { + _onSourceKeyPress(actor, event) { let symbol = event.get_key_symbol(); if (symbol === Clutter.KEY_space || symbol === Clutter.KEY_Return || symbol === Clutter.KEY_KP_Enter) { - this.menu.toggle(); - return true; - } else if (symbol === Clutter.KEY_Escape && this.menu.isOpen) { - this.menu.close(); - return true; + this.toggle(); + return Clutter.EVENT_STOP; + } else if (symbol === Clutter.KEY_Escape && this.isOpen) { + this.close(); + return Clutter.EVENT_STOP; } else if (symbol === Clutter.KEY_Down) { - if (!this.menu.isOpen) - this.menu.toggle(); - this.menu.actor.navigate_focus(this.actor, Gtk.DirectionType.DOWN, false); - return true; + if (!this.isOpen) + this.toggle(); + this.actor.navigate_focus(this.actor, Gtk.DirectionType.DOWN, false); + return Clutter.EVENT_STOP; } else - return false; + return Clutter.EVENT_PROPAGATE; } - }; -function Workspace() { - this._init.apply(this, arguments); -} - -Workspace.prototype = { - _init : function(metaWorkspace, view) { +var Workspace = GObject.registerClass({ + Signals: { + 'focus-refresh-required': {}, + }, +}, class Workspace extends Clutter.Actor { + _init(metaWorkspace, view) { + super._init({ layout_manager: new Clutter.FixedLayout() }); + this.set_size(0, 0); this.metaWorkspace = metaWorkspace; this.myView = view; - this.actor = new Clutter.Group(); - this.actor.set_size(0, 0); this._monitors = []; this._activeClone = null; this.currentMonitorIndex = Main.layoutManager.primaryIndex; @@ -1333,14 +1262,14 @@ Workspace.prototype = { let m = new WorkspaceMonitor(metaWorkspace, ix, this); m.setGeometry(monitor.x, monitor.y, monitor.width, monitor.height, monitor.width * .01); this._monitors.push(m); - this.actor.add_actor(m.actor); + this.add_child(m); }); this.connect('focus-refresh-required', () => { this.selectNextNonEmptyMonitor(this.currentMonitorIndex - 1, 1); }); - }, + } - findNextNonEmptyMonitor: function(start, increment) { + findNextNonEmptyMonitor(start, increment) { let pos = start; for (let i = 0; i < this._monitors.length; ++i) { pos = (this._monitors.length + pos + increment) % this._monitors.length; @@ -1349,18 +1278,18 @@ Workspace.prototype = { } } return this.currentMonitorIndex || 0; - }, + } - selectNextNonEmptyMonitor: function(start, increment) { + selectNextNonEmptyMonitor(start, increment) { this.selectMonitor(this.findNextNonEmptyMonitor(start || 0, increment)); - }, + } - selectMonitor: function(index) { + selectMonitor(index) { this.currentMonitorIndex = index; this._monitors[this.currentMonitorIndex].showActiveSelection(); - }, + } - selectActiveClone: function(clone, wsMonitor) { + selectActiveClone(clone, wsMonitor) { let current = this._activeClone; if (clone) { this.currentMonitorIndex = wsMonitor.monitorIndex; @@ -1371,7 +1300,6 @@ Workspace.prototype = { if (current) { current.overlay.setSelected(false); } - wsMonitor.emit('selection-changed'); } // We might have left the focused clone, hiding the overlay, @@ -1379,12 +1307,12 @@ Workspace.prototype = { if (this._activeClone) { this._activeClone.overlay.setSelected(true); } - }, + } - _onKeyPress: function(actor, event) { + _onKeyPress(actor, event) { let modifiers = Cinnamon.get_event_state(event); - let symbol = event.get_key_symbol(); - let keycode = event.get_key_code(); + let symbol = event.keyval; + let keycode = event.hardware_keycode; let ctrlAltMask = Clutter.ModifierType.CONTROL_MASK | Clutter.ModifierType.MOD1_MASK; // This relies on the fact that Clutter.ModifierType is the same as Gdk.ModifierType let action = global.display.get_keybinding_action(keycode, modifiers); @@ -1392,7 +1320,7 @@ Workspace.prototype = { if ((symbol === Clutter.KEY_ISO_Left_Tab || symbol === Clutter.KEY_Tab) && !(modifiers & ctrlAltMask)) { let increment = symbol === Clutter.KEY_ISO_Left_Tab ? -1 : 1; this.selectNextNonEmptyMonitor(this.currentMonitorIndex, increment); - return true; + return Clutter.EVENT_STOP; } let activeMonitor = this._monitors[this.currentMonitorIndex]; @@ -1401,79 +1329,80 @@ Workspace.prototype = { (modifiers & Clutter.ModifierType.MOD1_MASK) && !(modifiers & Clutter.ModifierType.CONTROL_MASK)) { activeMonitor.showMenuForSelectedWindow(); - return true; + return Clutter.EVENT_STOP; } if (action === Meta.KeyBindingAction.CLOSE || symbol === Clutter.KEY_w && modifiers & Clutter.ModifierType.CONTROL_MASK) { activeMonitor.closeSelectedWindow(); - return true; + return Clutter.EVENT_STOP; } if ((symbol === Clutter.KEY_m || symbol === Clutter.KEY_M) && modifiers & Clutter.ModifierType.CONTROL_MASK) { activeMonitor.moveSelectedWindowToNextMonitor(); - return true; + return Clutter.EVENT_STOP; } if (symbol === Clutter.KEY_Return || symbol === Clutter.KEY_KP_Enter || symbol === Clutter.KEY_space) { if (activeMonitor.activateSelectedWindow()) { - return true; + return Clutter.EVENT_STOP; } Main.overview.hide(); - return true; + return Clutter.EVENT_STOP; } if (modifiers & ctrlAltMask) { - return false; + return Clutter.EVENT_PROPAGATE; } - return activeMonitor.selectAnotherWindow(symbol); - }, + return activeMonitor.selectAnotherWindow(symbol) + ? Clutter.EVENT_STOP + : Clutter.EVENT_PROPAGATE; + } - destroy: function() { + destroy() { this._monitors.forEach(monitor => monitor.destroy()); - this.actor.destroy(); - }, + super.destroy(); + } - selectAnotherWindow: function(symbol) { + selectAnotherWindow(symbol) { this._monitors[this.currentMonitorIndex].selectAnotherWindow(symbol); - }, + } - zoomFromOverview: function() { + zoomFromOverview() { this._monitors.forEach(monitor => monitor.zoomFromOverview()); - }, + } - zoomToOverview: function() { + zoomToOverview() { this._monitors.forEach(monitor => monitor.zoomToOverview()); - }, + } - hasMaximizedWindows: function() { + hasMaximizedWindows() { for(let monitor of this._monitors) { if (monitor.hasMaximizedWindows()) return true; } return false; - }, + } - isEmpty: function() { + isEmpty() { for(let monitor of this._monitors) { if (!monitor.isEmpty()) return false; } return true; - }, + } - showWindowsOverlays: function() { + showWindowsOverlays() { this._monitors.forEach(monitor => monitor.showWindowsOverlays()); - }, + } - hideWindowsOverlays: function() { + hideWindowsOverlays() { this._monitors.forEach(monitor => monitor.hideWindowsOverlays()); - }, + } - syncStacking: function(arg1) { + syncStacking(arg1) { this._monitors.forEach(monitor => monitor.syncStacking(arg1)); } -}; -Signals.addSignalMethods(Workspace.prototype); +}); diff --git a/js/ui/workspacesView.js b/js/ui/workspacesView.js index 64a488434c..4c55dc679b 100644 --- a/js/ui/workspacesView.js +++ b/js/ui/workspacesView.js @@ -1,10 +1,9 @@ // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- const Clutter = imports.gi.Clutter; -const Lang = imports.lang; +const GObject = imports.gi.GObject; const Cinnamon = imports.gi.Cinnamon; const St = imports.gi.St; -const Signals = imports.signals; const Main = imports.ui.main; const Workspace = imports.ui.workspace; @@ -23,29 +22,21 @@ var SwipeScrollResult = { CLICK: 2 }; -function WorkspacesView(workspaces) { - this._init(workspaces); -} +var WorkspacesView = GObject.registerClass( +class WorkspacesView extends St.Widget { + _init(workspaces) { + super._init({ style_class: 'workspaces-view' }); -WorkspacesView.prototype = { - _init: function(workspaces) { - this.actor = new St.Widget({ style_class: 'workspaces-view' }); + this.set_size(0, 0); - // The actor itself isn't a drop target, so we don't want to pick on its area - this.actor.set_size(0, 0); + this.connect('destroy', this._onDestroy.bind(this)); - this.actor.connect('destroy', Lang.bind(this, this._onDestroy)); - - // does not work: - // this.actor.connect('scroll-event', Lang.bind(this, this._onScrollEvent)); - - this.actor.connect('style-changed', Lang.bind(this, - function() { - let node = this.actor.get_theme_node(); - this._spacing = node.get_length('spacing'); - this._updateWorkspaceActors(false); - })); - this.actor.connect('notify::mapped', Lang.bind(this, this._onMappedChanged)); + this.connect('style-changed', () => { + let node = this.get_theme_node(); + this._spacing = node.get_length('spacing'); + this._updateWorkspaceActors(false); + }); + this.connect('notify::mapped', this._onMappedChanged.bind(this)); this._width = 0; this._height = 0; @@ -53,7 +44,7 @@ WorkspacesView.prototype = { this._y = 0; this._workspaceRatioSpacing = 0; this._spacing = 0; - this._animating = false; // tweening + this._animating = false; this._scrolling = false; // swipe-scrolling this._animatingScroll = false; // programmatically updating the adjustment @@ -62,11 +53,9 @@ WorkspacesView.prototype = { let activeWorkspaceIndex = global.workspace_manager.get_active_workspace_index(); this._workspaces = []; for (let i = 0; i < global.workspace_manager.n_workspaces; i++) { - let metaWorkspace = global.workspace_manager.get_workspace_by_index(i); - this._workspaces[i] = new Workspace.Workspace(metaWorkspace, this); - this.actor.add_actor(this._workspaces[i].actor); + this._addWorkspace(global.workspace_manager.get_workspace_by_index(i)); } - this._workspaces[activeWorkspaceIndex].actor.raise_top(); + this._workspaces[activeWorkspaceIndex].raise_top(); // Position/scale the desktop windows and their children after the // workspaces have been created. This cannot be done first because @@ -84,118 +73,93 @@ WorkspacesView.prototype = { page_size: 1, step_increment: 0, upper: this._workspaces.length }); - this._scrollAdjustment.connect('notify::value', - Lang.bind(this, this._onScroll)); - - - this._swipeScrollBeginId = 0; - this._swipeScrollEndId = 0; + this._scrollAdjustment.connect('notify::value', this._onScroll.bind(this)); - let restackedNotifyId = global.display.connect('restacked', Lang.bind(this, this._onRestacked)); - let switchWorkspaceNotifyId = global.window_manager.connect('switch-workspace', - Lang.bind(this, this._activeWorkspaceChanged)); - let nWorkspacesChangedId = global.workspace_manager.connect('notify::n-workspaces', Lang.bind(this, this._workspacesChanged)); - - this._disconnectHandlers = function() { - global.window_manager.disconnect(switchWorkspaceNotifyId); - global.workspace_manager.disconnect(nWorkspacesChangedId); - global.display.disconnect(restackedNotifyId); - }; + global.display.connectObject( + 'restacked', this._onRestacked.bind(this), this); + global.window_manager.connectObject( + 'switch-workspace', this._activeWorkspaceChanged.bind(this), this); + global.workspace_manager.connectObject( + 'notify::n-workspaces', this._workspacesChanged.bind(this), this); this._onRestacked(); - this.actor.connect('key-press-event', this._onStageKeyPress.bind(this)); - this.actor.connect('key-release-event', this._onStageKeyRelease.bind(this)); - global.stage.set_key_focus(this.actor); + global.stage.set_key_focus(this); let primary = Main.layoutManager.primaryMonitor; this.setGeometry(primary.x, primary.y, primary.width, primary.height, 0); - }, + } - _onStageKeyPress: function(actor, event) { + vfunc_key_press_event(event) { let activeWorkspaceIndex = global.workspace_manager.get_active_workspace_index(); let activeWorkspace = this._workspaces[activeWorkspaceIndex]; - this._keyIsHandled = activeWorkspace._onKeyPress(actor, event); + this._keyIsHandled = activeWorkspace._onKeyPress(this, event); return this._keyIsHandled; - }, + } - _onStageKeyRelease: function(actor, event) { + vfunc_key_release_event(event) { if (this._keyIsHandled) - return false; + return Clutter.EVENT_PROPAGATE; - let modifiers = Cinnamon.get_event_state(event); - let symbol = event.get_key_symbol(); - - switch (symbol) { + switch (event.keyval) { case Clutter.KEY_Escape: case Clutter.KEY_Super_L: case Clutter.KEY_Super_R: Main.overview.hide(); - return true; + return Clutter.EVENT_STOP; default: - return false; + return Clutter.EVENT_PROPAGATE; } - }, + } - setGeometry: function(x, y, width, height, spacing) { + setGeometry(x, y, width, height, spacing) { this._width = width; this._height = height; this._x = x; this._y = y; this._workspaceRatioSpacing = spacing; - }, + } - getActiveWorkspace: function() { + getActiveWorkspace() { let active = global.workspace_manager.get_active_workspace_index(); return this._workspaces[active]; - }, + } - getWorkspaceByIndex: function(index) { + getWorkspaceByIndex(index) { return this._workspaces[index]; - }, + } - hide: function() { + hide() { let activeWorkspaceIndex = global.workspace_manager.get_active_workspace_index(); let activeWorkspace = this._workspaces[activeWorkspaceIndex]; - activeWorkspace.actor.raise_top(); + activeWorkspace.raise_top(); activeWorkspace.zoomFromOverview(); - }, + } - destroy: function() { - if (this._swipeScrollBeginId > 0) { - Main.overview.disconnect(this._swipeScrollBeginId); - this._swipeScrollBeginId = 0; - } - if (this._swipeScrollEndId > 0) { - Main.overview.disconnect(this._swipeScrollEndId); - this._swipeScrollEndId = 0; - } + destroy() { + Main.overview.disconnectObject(this); - for (let w = 0; w < this._workspaces.length; w++) { - this._workspaces[w].disconnectAll(); - this._workspaces[w].destroy(); - } - this._workspaces = []; - this.actor.destroy(); - }, + this._workspaces.slice().forEach(w => w.destroy()); + super.destroy(); + } - updateWindowPositions: function() { + updateWindowPositions() { for (let w = 0; w < this._workspaces.length; w++) this._workspaces[w].positionWindows(Workspace.WindowPositionFlags.ANIMATE); - }, + } - _scrollToActive: function(showAnimation) { + _scrollToActive(showAnimation) { let active = global.workspace_manager.get_active_workspace_index(); this._updateWorkspaceActors(showAnimation); Main.wm.showWorkspaceOSD(); this._updateScrollAdjustment(active, showAnimation); - }, + } // Update workspace actors parameters // @showAnimation: iff %true, transition between states - _updateWorkspaceActors: function(showAnimation) { + _updateWorkspaceActors(showAnimation) { let active = global.workspace_manager.get_active_workspace_index(); // Animation is turned off in a multi-manager scenario till we fix @@ -205,7 +169,7 @@ WorkspacesView.prototype = { for (let w = 0; w < this._workspaces.length; w++) { let workspace = this._workspaces[w]; - workspace.actor.remove_all_transitions(); + workspace.remove_all_transitions(); let x = (w - active) * (this._width + this._spacing + this._workspaceRatioSpacing); @@ -216,7 +180,8 @@ WorkspacesView.prototype = { }; // we have to call _updateVisibility() once before the // animation and once afterwards - it does not really - // matter which tween we use, so we pick the first one ... + // matter which ease() call we attach the onComplete to, + // so we pick the first one ... if (w == 0) { this._updateVisibility(); params.onComplete = () => { @@ -225,39 +190,38 @@ WorkspacesView.prototype = { }; } - workspace.actor.ease(params); - } else if (!workspace.actor.is_finalized()) { - workspace.actor.set_position(x, 0); + workspace.ease(params); + } else { + workspace.set_position(x, 0); if (w == 0) this._updateVisibility(); } } - }, + } - _updateVisibility: function() { + _updateVisibility() { let active = global.workspace_manager.get_active_workspace_index(); for (let w = 0; w < this._workspaces.length; w++) { let workspace = this._workspaces[w]; if (this._animating || this._scrolling) { workspace.hideWindowsOverlays(); - workspace.actor.show(); - } else if (!workspace.actor.is_finalized()) { + workspace.show(); + } else { workspace.showWindowsOverlays(); - workspace.actor.visible = (w == active); + workspace.visible = (w == active); } } - }, + } - _updateScrollAdjustment: function(index, showAnimation) { + _updateScrollAdjustment(index, showAnimation) { if (this._scrolling) return; this._animatingScroll = true; if (showAnimation) { - this._scrollAdjustment.ease({ - value: index, + this._scrollAdjustment.ease(index, { duration: WORKSPACE_SWITCH_TIME, mode: Clutter.AnimationMode.EASE_OUT_QUAD, onComplete: () => { @@ -270,61 +234,71 @@ WorkspacesView.prototype = { } let active = global.workspace_manager.get_active_workspace_index(); this._workspaces[active].zoomToOverview(); - }, - - _workspacesChanged: function() { - let removedCount = 0; - this._workspaces.slice().forEach(function(workspace, i) { - let metaWorkspace = global.workspace_manager.get_workspace_by_index(i - removedCount); - if (workspace.metaWorkspace != metaWorkspace) { - workspace.actor.remove_all_transitions(); + } + + _workspacesChanged() { + let activeMetaWorkspaces = new Set(); + for (let i = 0; i < global.workspace_manager.n_workspaces; i++) + activeMetaWorkspaces.add(global.workspace_manager.get_workspace_by_index(i)); + + this._workspaces.slice().forEach(workspace => { + if (!activeMetaWorkspaces.has(workspace.metaWorkspace)) { + workspace.remove_all_transitions(); workspace.destroy(); - this._workspaces.splice(i - removedCount, 1); - ++removedCount; } - }, this); + }); while (global.workspace_manager.n_workspaces > this._workspaces.length) { let lastWs = global.workspace_manager.get_workspace_by_index(this._workspaces.length); - let workspace = new Workspace.Workspace(lastWs, this); - this._workspaces.push(workspace); - this.actor.add_actor(workspace.actor); + this._addWorkspace(lastWs); } this._animating = false; this._updateVisibility(); - }, + } - _activeWorkspaceChanged: function(wm, from, to, direction) { + _addWorkspace(metaWorkspace) { + let workspace = new Workspace.Workspace(metaWorkspace, this); + workspace.connect('destroy', () => { + let idx = this._workspaces.indexOf(workspace); + if (idx >= 0) + this._workspaces.splice(idx, 1); + }); + this._workspaces.push(workspace); + this.add_child(workspace); + } + + _activeWorkspaceChanged(wm, from, to, direction) { if (this._scrolling) return; this._keyIsHandled = true; this._scrollToActive(true); - }, + } - _onDestroy: function() { + _onDestroy() { this._scrollAdjustment.run_dispose(); - this._disconnectHandlers(); - }, + } - _onMappedChanged: function() { - if (this.actor.mapped) { + _onMappedChanged() { + if (this.mapped) { let direction = SwipeScrollDirection.HORIZONTAL; Main.overview.setScrollAdjustment(this._scrollAdjustment, direction); - this._swipeScrollBeginId = Main.overview.connect('swipe-scroll-begin', - Lang.bind(this, this._swipeScrollBegin)); - this._swipeScrollEndId = Main.overview.connect('swipe-scroll-end', - Lang.bind(this, this._swipeScrollEnd)); + Main.overview.connectObject( + 'swipe-scroll-begin', this._swipeScrollBegin.bind(this), + 'swipe-scroll-end', this._swipeScrollEnd.bind(this), + this); + } else { + Main.overview.disconnectObject(this); } - }, + } - _swipeScrollBegin: function() { + _swipeScrollBegin() { this._scrolling = true; - }, + } - _swipeScrollEnd: function(overview, result) { + _swipeScrollEnd(overview, result) { this._scrolling = false; // Close overview on click when there are no windows @@ -337,9 +311,9 @@ WorkspacesView.prototype = { this._updateVisibility(); } - }, + } - _onRestacked: function() { + _onRestacked() { let stack = global.get_window_actors().reverse(); let stackIndices = {}; @@ -350,11 +324,11 @@ WorkspacesView.prototype = { for (let j = 0; j < this._workspaces.length; j++) this._workspaces[j].syncStacking(stackIndices); - }, + } // sync the workspaces' positions to the value of the scroll adjustment // and change the active workspace if appropriate - _onScroll: function(adj) { + _onScroll(adj) { if (this._animatingScroll) return; @@ -367,8 +341,8 @@ WorkspacesView.prototype = { } let last = this._workspaces.length - 1; - let firstWorkspaceX = this._workspaces[0].actor.x; - let lastWorkspaceX = this._workspaces[last].actor.x; + let firstWorkspaceX = this._workspaces[0].x; + let lastWorkspaceX = this._workspaces[last].x; let workspacesWidth = lastWorkspaceX - firstWorkspaceX; if (adj.upper == 1) @@ -381,12 +355,12 @@ WorkspacesView.prototype = { for (let i = 0; i < this._workspaces.length; i++) { this._workspaces[i].hideWindowsOverlays(); - this._workspaces[i].actor.visible = Math.abs(i - adj.value) <= 1; - this._workspaces[i].actor.x += dx; + this._workspaces[i].visible = Math.abs(i - adj.value) <= 1; + this._workspaces[i].x += dx; } - }, + } - _onScrollEvent: function (actor, event) { + _onScrollEvent(actor, event) { switch ( event.get_scroll_direction() ) { case Clutter.ScrollDirection.UP: Main.wm.actionMoveWorkspaceUp(); @@ -396,5 +370,4 @@ WorkspacesView.prototype = { break; } } -}; -Signals.addSignalMethods(WorkspacesView.prototype); +});