import Shell from 'gi://Shell'; import Clutter from 'gi://Clutter'; import Meta from 'gi://Meta'; import * as Main from 'resource:///org/gnome/shell/ui/main.js'; import { PaintSignals } from '../effects/paint_signals.js'; import { ApplicationsService } from '../dbus/services.js'; export const ApplicationsBlur = class ApplicationsBlur { constructor(connections, settings, _) { this.connections = connections; this.settings = settings; this.paint_signals = new PaintSignals(connections); // stores every blurred window this.window_map = new Map(); // stores every blur actor this.blur_actor_map = new Map(); } enable() { this._log("blurring applications..."); // export dbus service for preferences this.service = new ApplicationsService; this.service.export(); // blur already existing windows this.update_all_windows(); // blur every new window this.connections.connect( global.display, 'window-created', (_meta_display, meta_window) => { this._log("window created"); if (meta_window) { let window_actor = meta_window.get_compositor_private(); this.track_new(window_actor, meta_window); } } ); this.connect_to_overview(); } /// Connect to the overview being opened/closed to force the blur being /// shown on every window of the workspaces viewer. connect_to_overview() { this.connections.disconnect_all_for(Main.overview); if (this.settings.applications.BLUR_ON_OVERVIEW) { // when the overview is opened, show every window actors (which // allows the blur to be shown too) this.connections.connect( Main.overview, 'showing', _ => this.window_map.forEach((meta_window, _pid) => { let window_actor = meta_window.get_compositor_private(); window_actor.show(); }) ); // when the overview is closed, hide every actor that is not on the // current workspace (to mimic the original behaviour) this.connections.connect( Main.overview, 'hidden', _ => { let active_workspace = global.workspace_manager.get_active_workspace(); this.window_map.forEach((meta_window, _pid) => { let window_actor = meta_window.get_compositor_private(); if ( meta_window.get_workspace() !== active_workspace ) window_actor.hide(); }); } ); } } /// Iterate through all existing windows and add blur as needed. update_all_windows() { // remove all previously blurred windows, in the case where the // whitelist was changed this.window_map.forEach(((_meta_window, pid) => { this.remove_blur(pid); })); for ( let i = 0; i < global.workspace_manager.get_n_workspaces(); ++i ) { let workspace = global.workspace_manager.get_workspace_by_index(i); let windows = workspace.list_windows(); windows.forEach(meta_window => { let window_actor = meta_window.get_compositor_private(); // disconnect previous signals this.connections.disconnect_all_for(window_actor); this.track_new(window_actor, meta_window); }); } } /// Adds the needed signals to every new tracked window, and adds blur if /// needed. track_new(window_actor, meta_window) { let pid = ("" + Math.random()).slice(2, 16); window_actor['blur_provider_pid'] = pid; meta_window['blur_provider_pid'] = pid; // remove the blur when the window is destroyed this.connections.connect(window_actor, 'destroy', window_actor => { let pid = window_actor.blur_provider_pid; if (this.blur_actor_map.has(pid)) { this.remove_blur(pid); } this.window_map.delete(pid); }); // update the blur when mutter-hint or wm-class is changed for (const prop of ['mutter-hints', 'wm-class']) { this.connections.connect( meta_window, `notify::${prop}`, _ => { let pid = meta_window.blur_provider_pid; this._log(`${prop} changed for pid ${pid}`); let window_actor = meta_window.get_compositor_private(); this.check_blur(pid, window_actor, meta_window); } ); } // update the position and size when the window size changes this.connections.connect(meta_window, 'size-changed', () => { if (this.blur_actor_map.has(pid)) { let allocation = this.compute_allocation(meta_window); let blur_actor = this.blur_actor_map.get(pid); blur_actor.x = allocation.x; blur_actor.y = allocation.y; blur_actor.width = allocation.width; blur_actor.height = allocation.height; } }); this.check_blur(pid, window_actor, meta_window); } /// Checks if the given actor needs to be blurred. /// /// In order to be blurred, a window either: /// - is whitelisted in the user preferences if not enable-all /// - is not blacklisted if enable-all /// - has a correct mutter hint, set to `blur-provider=sigma_value` check_blur(pid, window_actor, meta_window) { let mutter_hint = meta_window.get_mutter_hints(); let window_wm_class = meta_window.get_wm_class(); let enable_all = this.settings.applications.ENABLE_ALL; let whitelist = this.settings.applications.WHITELIST; let blacklist = this.settings.applications.BLACKLIST; this._log(`checking blur for ${pid}`); // either the window is included in whitelist if (window_wm_class !== "" && ((enable_all && !blacklist.includes(window_wm_class)) || (!enable_all && whitelist.includes(window_wm_class)) ) && [ Meta.FrameType.NORMAL, Meta.FrameType.DIALOG, Meta.FrameType.MODAL_DIALOG ].includes(meta_window.get_frame_type()) ) { this._log(`application ${pid} listed, blurring it`); // get blur effect parameters let brightness, sigma; if (this.settings.applications.CUSTOMIZE) { brightness = this.settings.applications.BRIGHTNESS; sigma = this.settings.applications.SIGMA; } else { brightness = this.settings.BRIGHTNESS; sigma = this.settings.SIGMA; } this.update_blur(pid, window_actor, meta_window, brightness, sigma); } // or blur is asked by window itself else if ( mutter_hint != null && mutter_hint.includes("blur-provider") ) { this._log(`application ${pid} has hint ${mutter_hint}, parsing`); // get blur effect parameters let [brightness, sigma] = this.parse_xprop(mutter_hint); this.update_blur(pid, window_actor, meta_window, brightness, sigma); } // remove blur if the mutter hint is no longer valid, and the window // is not explicitly whitelisted or un-blacklisted else if (this.blur_actor_map.has(pid)) { this.remove_blur(pid); } } /// When given the xprop property, returns the brightness and sigma values /// matching. If one of the two values is invalid, or missing, then it uses /// default values. /// /// An xprop property is valid if it is in one of the following formats: /// /// blur-provider=sigma:60,brightness:0.9 /// blur-provider=s:10,brightness:0.492 /// blur-provider=b:1.0,s:16 /// /// Brightness is a floating-point between 0.0 and 1.0 included. /// Sigma is an integer between 0 and 999 included. /// /// If sigma is set to 0, then the blur is removed. /// Setting "default" instead of the two values will make the /// extension use its default value. /// /// Note that no space can be inserted. /// parse_xprop(property) { // set brightness and sigma to default values let brightness, sigma; if (this.settings.applications.CUSTOMIZE) { brightness = this.settings.applications.BRIGHTNESS; sigma = this.settings.applications.SIGMA; } else { brightness = this.settings.BRIGHTNESS; sigma = this.settings.SIGMA; } // get the argument of the property let arg = property.match("blur-provider=(.*)"); this._log(`argument = ${arg}`); // if argument is valid, parse it if (arg != null) { // verify if there is only one value: in this case, this is sigma let maybe_sigma = parseInt(arg[1]); if ( !isNaN(maybe_sigma) && maybe_sigma >= 0 && maybe_sigma <= 999 ) { sigma = maybe_sigma; } else { // perform pattern matching let res_b = arg[1].match("(brightness|b):(default|0?1?\.[0-9]*)"); let res_s = arg[1].match("(sigma|s):(default|\\d{1,3})"); // if values are valid and not default, change them to the xprop one if ( res_b != null && res_b[2] !== 'default' ) { brightness = parseFloat(res_b[2]); } if ( res_s != null && res_s[2] !== 'default' ) { sigma = parseInt(res_s[2]); } } } this._log(`brightness = ${brightness}, sigma = ${sigma}`); return [brightness, sigma]; } /// Updates the blur on a window which needs to be blurred. update_blur(pid, window_actor, meta_window, brightness, sigma) { // the window is already blurred, update its blur effect if (this.blur_actor_map.has(pid)) { // window is already blurred, but sigma is null: remove the blur if (sigma === 0) { this.remove_blur(pid); } // window is already blurred and sigma is non-null: update it else { this.update_blur_effect( this.blur_actor_map.get(pid), brightness, sigma ); } } // the window is not blurred, and sigma is a non-null value: blur it else if (sigma !== 0) { // window is not blurred, blur it this.create_blur_effect( pid, window_actor, meta_window, brightness, sigma ); } } /// Add the blur effect to the window. create_blur_effect(pid, window_actor, meta_window, brightness, sigma) { let blur_effect = new Shell.BlurEffect({ sigma: sigma, brightness: brightness, mode: Shell.BlurMode.BACKGROUND }); let blur_actor = this.create_blur_actor( meta_window, window_actor, blur_effect ); // if hacks are selected, force to repaint the window if (this.settings.HACKS_LEVEL === 1 || this.settings.HACKS_LEVEL === 2) { this._log("applications hack level 1 or 2"); this.paint_signals.disconnect_all(); this.paint_signals.connect(blur_actor, blur_effect); } else { this.paint_signals.disconnect_all(); } // insert the blurred widget window_actor.insert_child_at_index(blur_actor, 0); // make sure window is blurred in overview if (this.settings.applications.BLUR_ON_OVERVIEW) this.enforce_window_visibility_on_overview_for(window_actor); // set the window actor's opacity this.set_window_opacity(window_actor, this.settings.applications.OPACITY); this.connections.connect( window_actor, 'notify::opacity', _ => this.set_window_opacity(window_actor, this.settings.applications.OPACITY) ); // register the blur actor/effect blur_actor['blur_provider_pid'] = pid; this.blur_actor_map.set(pid, blur_actor); this.window_map.set(pid, meta_window); // hide the blur if window is invisible if (!window_actor.visible) { blur_actor.hide(); } // hide the blur if window becomes invisible this.connections.connect( window_actor, 'notify::visible', window_actor => { let pid = window_actor.blur_provider_pid; if (window_actor.visible) { this.blur_actor_map.get(pid).show(); } else { this.blur_actor_map.get(pid).hide(); } } ); } /// Makes sure that, when the overview is visible, the window actor will /// stay visible no matter what. /// We can instead hide the last child of the window actor, which will /// improve performances without hiding the blur effect. enforce_window_visibility_on_overview_for(window_actor) { this.connections.connect(window_actor, 'notify::visible', _ => { if (this.settings.applications.BLUR_ON_OVERVIEW) { if ( !window_actor.visible && Main.overview.visible ) { window_actor.show(); window_actor.get_last_child().hide(); } else if ( window_actor.visible ) window_actor.get_last_child().show(); } } ); } /// Set the opacity of the window actor that sits on top of the blur effect. set_window_opacity(window_actor, opacity) { window_actor.get_children().forEach(child => { if (child.name !== "blur-actor" && child.opacity != opacity) child.opacity = opacity; }); } /// Compute the size and position for a blur actor. /// On wayland, it seems like we need to divide by the scale to get the /// correct result. compute_allocation(meta_window) { const is_wayland = Meta.is_wayland_compositor(); const monitor_index = meta_window.get_monitor(); // check if the window is using wayland, or xwayland/xorg for rendering const scale = is_wayland && meta_window.get_client_type() == 0 ? Main.layoutManager.monitors[monitor_index].geometry_scale : 1; let frame = meta_window.get_frame_rect(); let buffer = meta_window.get_buffer_rect(); return { x: (frame.x - buffer.x) / scale, y: (frame.y - buffer.y) / scale, width: frame.width / scale, height: frame.height / scale }; } /// Returns a new already blurred widget, configured to follow the size and /// position of its target window. create_blur_actor(meta_window, window_actor, blur_effect) { // compute the size and position let allocation = this.compute_allocation(meta_window); // create the actor let blur_actor = new Clutter.Actor({ x: allocation.x, y: allocation.y, width: allocation.width, height: allocation.height }); // add the effect blur_actor.add_effect_with_name('blur-effect', blur_effect); return blur_actor; } /// Updates the blur effect by overwriting its sigma and brightness values. update_blur_effect(blur_actor, brightness, sigma) { let effect = blur_actor.get_effect('blur-effect'); effect.sigma = sigma; effect.brightness = brightness; } /// Removes the blur actor from the shell and unregister it. remove_blur(pid) { this._log(`removing blur for pid ${pid}`); let meta_window = this.window_map.get(pid); // disconnect needed signals and untrack window if (meta_window) { this.window_map.delete(pid); let window_actor = meta_window.get_compositor_private(); let blur_actor = this.blur_actor_map.get(pid); if (blur_actor) { this.blur_actor_map.delete(pid); if (window_actor) { // reset the opacity this.set_window_opacity(window_actor, 255); // remove the blurred actor window_actor.remove_child(blur_actor); // disconnect the signals about overview animation etc this.connections.disconnect_all_for(window_actor); } } } } disable() { this._log("removing blur from applications..."); this.service?.unexport(); this.blur_actor_map.forEach(((_blur_actor, pid) => { this.remove_blur(pid); })); this.connections.disconnect_all(); this.paint_signals.disconnect_all(); } /// Update the opacity of all window actors. set_opacity() { let opacity = this.settings.applications.OPACITY; this.window_map.forEach(((meta_window, _pid) => { let window_actor = meta_window.get_compositor_private(); this.set_window_opacity(window_actor, opacity); })); } /// Updates each blur effect to use new sigma value // FIXME set_sigma and set_brightness are called when the extension is // loaded and when sigma is changed, and do not respect the per-app // xprop behaviour set_sigma(s) { this.blur_actor_map.forEach((actor, _) => { actor.get_effect('blur-effect').set_sigma(s); }); } /// Updates each blur effect to use new brightness value set_brightness(b) { this.blur_actor_map.forEach((actor, _) => { actor.get_effect('blur-effect').set_brightness(b); }); } // not implemented for dynamic blur set_color(c) { } set_noise_amount(n) { } set_noise_lightness(l) { } _log(str) { if (this.settings.DEBUG) console.log(`[Blur my Shell > applications] ${str}`); } };