From 0fd3f0f62f67df9cf1e4a36be83a001c7f0b8b21 Mon Sep 17 00:00:00 2001 From: Simon Schneegans Date: Tue, 4 Apr 2023 11:27:01 +0200 Subject: [PATCH] :wrench: Do not depend on Clutter.Transitions --- extension.js | 299 +++++++++++++++++--------------------------------- src/Shader.js | 60 +++++++++- 2 files changed, 159 insertions(+), 200 deletions(-) diff --git a/extension.js b/extension.js index 54e9c08..845fa01 100644 --- a/extension.js +++ b/extension.js @@ -133,49 +133,81 @@ class Extension { } // We will monkey-patch these methods. Let's store the original ones. + this._origShouldAnimateActor = WindowManager.prototype._shouldAnimateActor; + this._origWaitForOverviewToHide = WindowManager.prototype._waitForOverviewToHide; this._origAddWindowClone = Workspace.prototype._addWindowClone; this._origWindowRemoved = Workspace.prototype._windowRemoved; this._origDoRemoveWindow = Workspace.prototype._doRemoveWindow; - this._origShouldAnimateActor = WindowManager.prototype._shouldAnimateActor; - this._origWaitForOverviewToHide = WindowManager.prototype._waitForOverviewToHide; - this._origDestroyWindowDone = WindowManager.prototype._destroyWindowDone; - // ------------------------------------------------ patching the window-open animation + // ------------------------------- patching the window animations outside the overview - // Here we add an effect to the window-open animation. This is done whenever a new - // window is created. Usually, there is no real window animation for opening windows - // in the overview - only the window's clone is animated - but thanks to the hacks - // below, we can show the real animations in the overview. - - // If a window is created the transitions are set up in the async _mapWindow of the + // If a window is created, the transitions are set up in the async _mapWindow() of the // WindowManager: - // https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/main/js/ui/windowManager.js#L1478 - // AFAIK, overriding this method is not possible as it's called by a signal to - // which it is bound via the bind() method. To tweak the async transition - // anyways, we override the actors ease() method once - the next time it will be - // called by the _mapWindow(), we will intercept it! - this._windowCreatedConnection = - global.display.connect('window-created', (d, metaWin) => { - let actor = metaWin.get_compositor_private(); + // https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/main/js/ui/windowManager.js#L1487 + // AFAIK, overriding this method is not possible as it's called by a signal to which + // it is bound via the bind() method. To tweak the async transition anyways, we + // override the actors ease() method once. We do this in _shouldAnimateActor() which + // is called right before the ease() in _mapWindow: + // https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/main/js/ui/windowManager.js#L1472 - const orig = actor.ease; - actor.ease = function(...params) { - orig.apply(actor, params); - actor.ease = orig; + // The same trick is done for the window-close animation. This is set up in a similar + // fashion in the WindowManager's _destroyWindow(): + // https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/main/js/ui/windowManager.js#L1558. + // Here is _shouldAnimateActor() also called right before. So we use it again to + // monkey-patch the window actor's ease() once. - // There are cases where the ease() is called prior to mapping the actor. If - // the actor is not yet mapped, we defer the effect creation. - if (actor.mapped) { - extensionThis._setupEffect(actor, true); - } else { - const connectionID = actor.connect('notify::mapped', () => { - extensionThis._setupEffect(actor, true); - actor.disconnect(connectionID); - }); - } - }; + // We override WindowManager._shouldAnimateActor() also for another purpose: Usually, + // it returns false when we are in the overview. This prevents the window animations + // there. To enable animations in the overview, we check inside the method whether it + // was called by either _mapWindow or _destroyWindow. If so, we return true. Let's see + // if this breaks stuff left and right... + // https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/main/js/ui/windowManager.js#L1120 + WindowManager.prototype._shouldAnimateActor = function(actor, types) { + const caller = (new Error()).stack.split('\n')[1]; + const forClosing = caller.includes('_destroyWindow@'); + const forOpening = caller.includes('_mapWindow@'); + + // This is also called in other cases, for instance when minimizing windows. We are + // only interested in window opening and window closing for now. + if (forClosing || forOpening) { + + // If there is an applicable effect profile, we intercept the ease() method to + // setup our own effect. + const chosenEffect = extensionThis._chooseEffect(actor, forOpening); + + if (chosenEffect) { + // Store the original ease() method of the actor. + const orig = actor.ease; + + // Now intercept the next call to actor.ease(). + actor.ease = function(...params) { + // Quickly restore the original behavior. Nobody noticed, I guess :D + actor.ease = orig; + + // And then create the effect! + extensionThis._setupEffect(actor, forOpening, chosenEffect.effect, + chosenEffect.profile); + }; + + return true; + } + } + + return extensionThis._origShouldAnimateActor.apply(this, [actor, types]); + }; + + // Make sure to remove any effects if requested by the window manager. + this._killEffectsSignal = + global.window_manager.connect('kill-window-effects', (wm, actor) => { + const shader = actor.get_effect('burn-my-windows-effect'); + if (shader) { + shader.endAnimation(); + } }); + + // --------------------------------------------- fix window animations in the overview + // Some of the effects require that the window's actor is enlarged to provide a bigger // canvas to draw the effects. Outside the overview we can simply increase the scale // of the actor. However, if we are in the overview, we have to enlarge the clone of @@ -237,61 +269,11 @@ class Extension { return Promise.resolve(); }; - // Here comes the ULTRA-HACK: The method below is called (amongst others) by the - // _destroyWindow and _mapWindow methods of the WindowManager. Usually, it returns - // false when we are in the overview. This prevents the window animations. As we - // cannot monkey-patch the _destroyWindow or _mapWindow methods themselves, we check - // inside the method below whether it was called by either of those. If so, we return - // true. Let's see if this breaks stuff left and right... - // https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/main/js/ui/windowManager.js#L1124 - WindowManager.prototype._shouldAnimateActor = function(...params) { - const caller = (new Error()).stack.split('\n')[1]; - if (caller.includes('_destroyWindow@') || caller.includes('_mapWindow@')) { - return true; - } - return extensionThis._origShouldAnimateActor.apply(this, params); - }; - - - // ----------------------------------------------- patching the window-close animation - - // The signal handler below and the following patch are all which is required outside - // of the overview. All other hacks further below are just required to defer the - // window-hiding in the overview until the effect is finished. - - // The close animation is set up in WindowManager's _destroyWindow: - // https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/main/js/ui/windowManager.js#L1554 - // As we cannot monkey-patch the _destroyWindow itself, we connect to the 'destroy' - // signal of the window manager and tweak the animation to our needs. - this._destroyConnection = global.window_manager.connect('destroy', (wm, actor) => { - this._setupEffect(actor, false); - }); - - // Once the window-close animation is is finished, the window manager's - // _destroyWindowDone is called. We use this to free the effect so that it can be - // re-used in future. - // https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/main/js/ui/windowManager.js#L1564 - WindowManager.prototype._destroyWindowDone = function(shellwm, actor) { - if (this._destroying.has(actor)) { - const shader = actor.get_effect('burn-my-windows-effect'); - if (shader) { - actor.remove_effect(shader); - shader.endAnimation(); - shader.returnToFactory(); - } - } - - // Call the original method. - extensionThis._origDestroyWindowDone.apply(this, [shellwm, actor]); - }; - // These three method overrides are mega-hacky! Usually, windows are not faded when // closed from the overview (why?). With these overrides we make sure that they are // actually faded out. To do this, _windowRemoved and _doRemoveWindow now check // whether there is a transition ongoing (via extensionThis._shouldDestroy). If that's - // the case, these methods do nothing. Are the actors removed in the end? I hope so. - // The _destroyWindow of the WindowManager sets the transitions up and should take - // care of removing the actors at the end of the transitions. + // the case, these methods do nothing. // https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/main/js/ui/workspace.js#L1258 Workspace.prototype._windowRemoved = function(ws, metaWin) { if (extensionThis._shouldDestroy(this, metaWin)) { @@ -369,16 +351,14 @@ class Extension { // Disable the window-picking D-Bus API. this._windowPicker.unexport(); - // Restore the original window-open and window-close animations. - global.window_manager.disconnect(this._destroyConnection); - global.display.disconnect(this._windowCreatedConnection); + global.window_manager.disconnect(this._killEffectsSignal); + // Restore the original window-open and window-close animations. Workspace.prototype._addWindowClone = this._origAddWindowClone; Workspace.prototype._windowRemoved = this._origWindowRemoved; Workspace.prototype._doRemoveWindow = this._origDoRemoveWindow; WindowManager.prototype._shouldAnimateActor = this._origShouldAnimateActor; WindowManager.prototype._waitForOverviewToHide = this._origWaitForOverviewToHide; - WindowManager.prototype._destroyWindowDone = this._origDestroyWindowDone; if (WindowPreview) { WindowPreview.prototype._deleteAll = this._origDeleteAll; @@ -427,11 +407,10 @@ class Extension { this._profiles.sort((a, b) => b.priority - a.priority); } - // This method adds one of the configured effects to the given actor. First, a profile - // matching the current circumstances is chosen. Then a random effect from its enabled - // effects will be selected. This will also tweak the transitions of the given actor - // (e.g. scale it up if required). - _setupEffect(actor, forOpening) { + // This method selects an effect profile matching the current circumstances. Then a + // random effect from its enabled effects will be selected. It returns null if no + // profile is currently applicable. + _chooseEffect(actor, forOpening) { // For now, we only add effects to normal windows and dialog windows. const isNormalWindow = actor.meta_window.window_type == Meta.WindowType.NORMAL; @@ -440,17 +419,7 @@ class Extension { actor.meta_window.window_type == Meta.WindowType.DIALOG; if (!isNormalWindow && !isDialogWindow) { - return; - } - - // There is the weird case where an animation is already ongoing. This happens when a - // window is closed which has been created before the session was started (e.g. when - // GNOME Shell has been restarted in the meantime). - const oldShader = actor.get_effect('burn-my-windows-effect'); - if (oldShader) { - actor.remove_effect(oldShader); - oldShader.endAnimation(); - oldShader.returnToFactory(); + return null; } // ----------------------------------------------- choose a profile and then an effect @@ -556,11 +525,23 @@ class Extension { // If nothing was enabled, we have to do nothing :) if (!effect || !profile) { - return; + return null; } + return {effect: effect, profile: profile}; + } - // ----------------------------------------------------------- tweak actor transitions + // This method adds the given effect using the settings from the given profile to the + // given actor. + _setupEffect(actor, forOpening, effect, profile) { + + // There is the weird case where an animation is already ongoing. This happens when a + // window is closed which has been created before the session was started (e.g. when + // GNOME Shell has been restarted in the meantime). + const oldShader = actor.get_effect('burn-my-windows-effect'); + if (oldShader) { + oldShader.endAnimation(); + } // If we are currently performing integration test, all animations are set to a fixed // duration and show a fixed frame from the middle of the animation. @@ -572,97 +553,25 @@ class Extension { // actorScale. const actorScale = effect.getActorScale(profile.settings, forOpening, actor); + // All scaling is relative to the window's center. + actor.set_pivot_point(0.5, 0.5); + actor.opacity = 255; + actor.scale_x = actorScale.x; + actor.scale_y = actorScale.y; + + // Now add a cool shader to our window actor! + const shader = effect.shaderFactory.getShader(); + + // Assign the effect to the window actor! + actor.add_effect_with_name('burn-my-windows-effect', shader); + // To make things deterministic during testing, we set the effect duration to 5 // seconds. const duration = testMode ? 5000 : profile.settings.get_int(effect.getNick() + '-animation-time'); - // All animations are relative to the window's center. - actor.set_pivot_point(0.5, 0.5); - - // We tweak the opacity and scale of the actor. If there is no ongoing transition for - // a property, a new one is set up. - const config = {'opacity': 255, 'scale-x': actorScale.x, 'scale-y': actorScale.y}; - - for (const property in config) { - let transition = actor.get_transition(property); - - // If there is currently no ongoing transition, we create a new one. Clutter does - // not like to create transitions with the same start and end value - however, we - // need at least one transition for our progress value in the shader. So we trick - // Clutter by creating an arbitrary transition first and then modifying the start - // and end values according to our config object. - if (!transition) { - actor.set_property(property, 0); - actor.save_easing_state(); - actor.set_easing_duration(1000); - actor.set_property(property, 1); - actor.restore_easing_state(); - - // Now there should be a transition! - transition = actor.get_transition(property); - } - - // Tweak the transition according to the config object. For some reason, there are - // rare cases, where no transition is set up. This happens from time to time... - if (transition) { - transition.set_duration(duration); - transition.set_to(config[property]); - transition.set_from(config[property]); - transition.set_progress_mode(Clutter.AnimationMode.LINEAR); - } - } - - // Once the transitions are finished, we restore the original actor size. - if (forOpening) { - const connectionID = actor.connect('transitions-completed', () => { - actor.scale_x = 1.0; - actor.scale_y = 1.0; - actor.disconnect(connectionID); - }); - } - - - // -------------------------------------------------------------------- add the shader - - // Now add a cool shader to our window actor! - const shader = effect.shaderFactory.getShader(); - - // There should always be an opacity transition going on... - const transition = actor.get_transition('opacity'); - - if (!transition) { - utils.debug('Cannot setup shader without opacity transition.') - return; - } - - // Assign the effect to the window actor! - actor.add_effect_with_name('burn-my-windows-effect', shader); - - // Set one-time uniforms. - shader.beginAnimation(profile.settings, forOpening, testMode, duration * 0.001, - actor); - - // Set other uniforms each frame. - transition.connect('new-frame', (t) => { - if (testMode) { - shader.updateAnimation(0.5); - } else { - shader.updateAnimation(t.get_progress()); - } - }); - - // Remove the effect if the animation finished or was interrupted. - if (forOpening) { - transition.connect('stopped', () => { - const oldShader = actor.get_effect('burn-my-windows-effect'); - if (oldShader) { - actor.remove_effect(oldShader); - oldShader.endAnimation(); - oldShader.returnToFactory(); - } - }); - } + // Finally start the animation! + shader.beginAnimation(profile.settings, forOpening, testMode, duration, actor); } // This is required to enable window-close animations in the overview. See the comment @@ -676,11 +585,9 @@ class Extension { // This was called "realWindow" in GNOME 3.36. const propertyName = utils.shellVersionIs(3, 36) ? 'realWindow' : '_windowActor'; const actor = workspace._windows[index][propertyName]; - if (!actor.get_transition('scale-y')) { - return true; - } + const shader = actor.get_effect('burn-my-windows-effect'); - return false; + return shader == null; } } diff --git a/src/Shader.js b/src/Shader.js index bbd9b67..d76089d 100644 --- a/src/Shader.js +++ b/src/Shader.js @@ -17,6 +17,7 @@ const {Gio, Shell, GObject, Clutter} = imports.gi; const ByteArray = imports.byteArray; +const Main = imports.ui.main; const ExtensionUtils = imports.misc.extensionUtils; const Me = imports.misc.extensionUtils.getCurrentExtension(); const utils = Me.imports.src.utils; @@ -77,13 +78,39 @@ var Shader = GObject.registerClass( this._uDuration = this.get_uniform_location('uDuration'); this._uSize = this.get_uniform_location('uSize'); this._uPadding = this.get_uniform_location('uPadding'); + + // Create a timeline to drive the animation. + this._timeline = new Clutter.Timeline(); + + // Call updateAnimation() once a frame. + this._timeline.connect('new-frame', (t) => { + if (this._testMode) { + this.updateAnimation(0.5); + } else { + this.updateAnimation(t.get_progress()); + } + }); + + // Clean up if the animation finished or was interrupted. + this._timeline.connect('stopped', (t, finished) => { + this.endAnimation(); + }); } // This is called once each time the shader is used. beginAnimation(settings, forOpening, testMode, duration, actor) { + if (this._timeline.is_playing()) { + this._timeline.stop(); + } + + this._timeline.set_actor(actor); + this._timeline.set_duration(duration); + this._timeline.start(); // Reset progress value. - this._progress = 0; + this._progress = 0; + this._testMode = testMode; + this._forOpening = forOpening; // This is not necessarily symmetric, but I haven't figured out a way to // get the actual values... @@ -91,7 +118,7 @@ var Shader = GObject.registerClass( this.set_uniform_float(this._uPadding, 1, [padding]); this.set_uniform_float(this._uForOpening, 1, [forOpening]); - this.set_uniform_float(this._uDuration, 1, [duration]); + this.set_uniform_float(this._uDuration, 1, [duration * 0.001]); this.set_uniform_float(this._uSize, 2, [actor.width, actor.height]); this.emit('begin-animation', settings, forOpening, testMode, actor); @@ -103,11 +130,36 @@ var Shader = GObject.registerClass( // in vfunc_paint_target. We do not emit it here, as the pipeline which may be used // by handlers must not have been created yet. this._progress = progress; + + this.queue_repaint(); } - // This will just emit the end-animation signal. It can be used to clean up any - // resources required during the animation. + // This will stop any running animation and emit the end-animation signal. This can be + // used to clean up any resources required during the animation. endAnimation() { + // This will call endAnimation() again, so we can return for now. + if (this._timeline.is_playing()) { + this._timeline.stop(); + return; + } + + // Remove the effect. + const actor = this.get_actor(); + actor.remove_effect(this); + + // Once the animation is done or interrupted, we call the methods which should have + // been called by the original ease() methods. + // https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/main/js/ui/windowManager.js#L1487 + // https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/main/js/ui/windowManager.js#L1558. + if (this._forOpening) { + Main.wm._mapWindowDone(global.window_manager, actor); + } else { + Main.wm._destroyWindowDone(global.window_manager, actor); + } + + // Mark this shader of being re-usable. + this.returnToFactory(); + this.emit('end-animation'); }