import Meta from 'gi://Meta'; import Gio from 'gi://Gio'; import * as Main from 'resource:///org/gnome/shell/ui/main.js'; import { ApplicationsService } from '../dbus/services.js'; import { PaintSignals } from '../conveniences/paint_signals.js'; import { DummyPipeline } from '../conveniences/dummy_pipeline.js'; export const ApplicationsBlur = class ApplicationsBlur { constructor(connections, settings, effects_manager) { this.connections = connections; this.settings = settings; this.effects_manager = effects_manager; this.paint_signals = new PaintSignals(connections); // stores every blurred meta window this.meta_window_map = new Map(); } enable() { this._log("blurring applications..."); // export dbus service for preferences this.service = new ApplicationsService; this.service.export(); this.mutter_gsettings = new Gio.Settings({ schema: 'org.gnome.mutter' }); // 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) this.track_new(meta_window); } ); // update window blur when focus is changed this.focused_window_pid = null; this.init_dynamic_opacity(); this.connections.connect( global.display, 'focus-window', (_meta_display, meta_window, _p0) => { if (meta_window && meta_window.bms_pid != this.focused_window_pid) this.set_focus_for_window(meta_window); else if (!meta_window) this.set_focus_for_window(null); } ); this.connect_to_overview(); } /// Initializes the dynamic opacity for windows, without touching to the connections. /// This is used both when enabling the component, and when changing the dynamic-opacity pref. init_dynamic_opacity() { if (this.settings.applications.DYNAMIC_OPACITY) { // make the currently focused window solid if (global.display.focus_window) this.set_focus_for_window(global.display.focus_window); } else { // remove old focused window if the pref was changed if (this.focused_window_pid) this.set_focus_for_window(null); } } /// 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.meta_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', _ => { this.meta_window_map.forEach((meta_window, _pid) => { let window_actor = meta_window.get_compositor_private(); if ( !meta_window.get_workspace().active ) 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.meta_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 => this.track_new(meta_window)); } } /// Adds the needed signals to every new tracked window, and adds blur if /// needed. /// Accepts only untracked meta windows (i.e no `bms_pid` set) track_new(meta_window) { // create a pid that will follow the window during its whole life const pid = ("" + Math.random()).slice(2, 16); meta_window.bms_pid = pid; this._log(`new window tracked, pid: ${pid}`); // register the blurred window this.meta_window_map.set(pid, meta_window); // update the blur when wm-class is changed this.connections.connect( meta_window, 'notify::wm-class', _ => this.check_blur(meta_window) ); // update the position and size when the window size changes this.connections.connect( meta_window, 'size-changed', _ => this.update_size(pid) ); // remove the blur when the window is unmanaged this.connections.connect( meta_window, 'unmanaging', _ => this.untrack_meta_window(pid) ); this.check_blur(meta_window); } /// Updates the size of the blur actor associated to a meta window from its pid. /// Accepts only tracked meta window (i.e `bms_pid` set), be it blurred or not. update_size(pid) { if (this.meta_window_map.has(pid)) { const meta_window = this.meta_window_map.get(pid); const blur_actor = meta_window.blur_actor; if (blur_actor) { const allocation = this.compute_allocation(meta_window); blur_actor.x = allocation.x; blur_actor.y = allocation.y; blur_actor.width = allocation.width; blur_actor.height = allocation.height; } } else // the pid was visibly not removed this.untrack_meta_window(pid); } /// Checks if the given actor needs to be blurred. /// Accepts only tracked meta window, be it blurred or not. /// /// In order to be blurred, a window either: /// - is whitelisted in the user preferences if not enable-all /// - is not blacklisted if enable-all check_blur(meta_window) { const window_wm_class = meta_window.get_wm_class(); const enable_all = this.settings.applications.ENABLE_ALL; const whitelist = this.settings.applications.WHITELIST; const blacklist = this.settings.applications.BLACKLIST; if (window_wm_class) this._log(`pid ${meta_window.bms_pid} associated to wm class name ${window_wm_class}`); // if we are in blacklist mode and the window is not blacklisted // or if we are in whitelist mode and the window is whitelisted 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()) ) { // only blur the window if it is not already done if (!meta_window.blur_actor) this.create_blur_effect(meta_window); } // remove blur it is not explicitly whitelisted or un-blacklisted else if (meta_window.blur_actor) this.remove_blur(meta_window.bms_pid); } /// Add the blur effect to the window. /// Accepts only tracked meta window that is NOT already blurred. create_blur_effect(meta_window) { const pid = meta_window.bms_pid; const window_actor = meta_window.get_compositor_private(); const pipeline = new DummyPipeline(this.effects_manager, this.settings.applications); let [blur_actor, bg_manager] = pipeline.create_background_with_effect( window_actor, 'bms-application-blurred-widget' ); meta_window.blur_actor = blur_actor; meta_window.bg_manager = bg_manager; // if hacks are selected, force to repaint the window if (this.settings.HACKS_LEVEL === 1) { this._log("hack level 1"); this.paint_signals.disconnect_all_for_actor(blur_actor); this.paint_signals.connect(blur_actor, pipeline.effect); } else { this.paint_signals.disconnect_all_for_actor(blur_actor); } // make sure window is blurred in overview if (this.settings.applications.BLUR_ON_OVERVIEW) this.enforce_window_visibility_on_overview_for(window_actor); // update the size this.update_size(pid); // set the window actor's opacity this.set_window_opacity(window_actor, this.settings.applications.OPACITY); // now set up the signals, for the window actor only: they are disconnected // in `remove_blur`, whereas the signals for the meta window are disconnected // only when the whole component is disabled // update the window opacity when it changes, else we don't control it fully this.connections.connect( window_actor, 'notify::opacity', _ => { if (this.focused_window_pid != pid) this.set_window_opacity(window_actor, this.settings.applications.OPACITY); } ); // hide the blur if window becomes invisible if (!window_actor.visible) blur_actor.hide(); this.connections.connect( window_actor, 'notify::visible', window_actor => { if (window_actor.visible) meta_window.blur_actor.show(); else meta_window.blur_actor.hide(); } ); } /// With `focus=true`, tells us we are focused on said window (which can be null if /// we are not focused anymore). It automatically removes the ancient focus. /// With `focus=false`, just remove the focus from said window (which can still be null). set_focus_for_window(meta_window, focus = true) { let blur_actor = null; let window_actor = null; let new_pid = null; if (meta_window) { blur_actor = meta_window.blur_actor; window_actor = meta_window.get_compositor_private(); new_pid = meta_window.bms_pid; } if (focus) { // remove old focused window if any if (this.focused_window_pid) { const old_focused_window = this.meta_window_map.get(this.focused_window_pid); if (old_focused_window) this.set_focus_for_window(old_focused_window, false); } // set new focused window pid this.focused_window_pid = new_pid; // if we have blur, hide it and make the window opaque if (this.settings.applications.DYNAMIC_OPACITY && blur_actor) { blur_actor.hide(); this.set_window_opacity(window_actor, 255); } } // if we remove the focus and have blur, show it and make the window transparent else if (blur_actor) { blur_actor.show(); this.set_window_opacity(window_actor, this.settings.applications.OPACITY); } } /// 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; }); } /// Update the opacity of all window actors. set_opacity() { let opacity = this.settings.applications.OPACITY; this.meta_window_map.forEach(((meta_window, pid) => { if (pid != this.focused_window_pid && meta_window.blur_actor) { let window_actor = meta_window.get_compositor_private(); this.set_window_opacity(window_actor, opacity); } })); } /// Compute the size and position for a blur actor. /// If `scale-monitor-framebuffer` experimental feature if on, we don't need to manage scaling. /// Else, on wayland, we need to divide by the scale to get the correct result. compute_allocation(meta_window) { const scale_monitor_framebuffer = this.mutter_gsettings.get_strv('experimental-features') .includes('scale-monitor-framebuffer'); 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 = !scale_monitor_framebuffer && 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 }; } /// Removes the blur actor to make a blurred window become normal again. /// It however does not untrack the meta window itself. /// Accepts a pid corresponding (or not) to a blurred (or not) meta window. remove_blur(pid) { this._log(`removing blur for pid ${pid}`); let meta_window = this.meta_window_map.get(pid); if (meta_window) { let window_actor = meta_window.get_compositor_private(); let blur_actor = meta_window.blur_actor; let bg_manager = meta_window.bg_manager; if (blur_actor && window_actor) { // reset the opacity this.set_window_opacity(window_actor, 255); // remove the blurred actor window_actor.remove_child(blur_actor); bg_manager._bms_pipeline.destroy(); bg_manager.destroy(); blur_actor.destroy(); // kinda untrack the blurred actor, as its presence is how we know // whether we are blurred or not delete meta_window.blur_actor; delete meta_window.bg_manager; // disconnect the signals of the window actor this.paint_signals.disconnect_all_for_actor(blur_actor); this.connections.disconnect_all_for(window_actor); } } } /// Kinda the same as `remove_blur`, but better: it also untracks the window. /// This needs to be called when the component is being disabled, else it /// would cause havoc by having untracked windows during normal operations, /// which is not the point at all! /// Accepts a pid corresponding (or not) to a blurred (or not) meta window. untrack_meta_window(pid) { this.remove_blur(pid); let meta_window = this.meta_window_map.get(pid); if (meta_window) { this.connections.disconnect_all_for(meta_window); this.meta_window_map.delete(pid); } } disable() { this._log("removing blur from applications..."); this.service?.unexport(); delete this.mutter_gsettings; this.meta_window_map.forEach((_meta_window, pid) => { this.untrack_meta_window(pid); }); this.connections.disconnect_all(); this.paint_signals.disconnect_all(); } _log(str) { if (this.settings.DEBUG) console.log(`[Blur my Shell > applications] ${str}`); } };