import Meta from 'gi://Meta'; import * as Main from 'resource:///org/gnome/shell/ui/main.js'; import * as Signals from 'resource:///org/gnome/shell/misc/signals.js'; import { PaintSignals } from '../conveniences/paint_signals.js'; import * as utils from '../conveniences/utils.js'; import { Pipeline } from '../conveniences/pipeline.js'; import { DummyPipeline } from '../conveniences/dummy_pipeline.js'; const DASH_STYLES = [ "transparent-dash", "light-dash", "dark-dash" ]; /// This type of object is created for every dash found, and talks to the main /// DashBlur thanks to signals. /// /// This allows to dynamically track the created dashes for each screen. class DashInfos { constructor( dash_blur, dash, dash_container, dash_background, background, background_group, bg_manager, paint_signals ) { // the parent DashBlur object, to communicate this.dash_blur = dash_blur; this.dash = dash; this.dash_container = dash_container; this.dash_background = dash_background; this.background = background; this.background_group = background_group; this.bg_manager = bg_manager; this.paint_signals = paint_signals; this.settings = dash_blur.settings; this.old_style = this.dash._background?.style; this.updateId = 0; this._first_boot = true; this.bg_allocation_id = this.background_group?.connect('notify::allocation', () => { this.schedule_update(); }); let monitor = Main.layoutManager.findMonitorForActor(this.dash_container); this.current_monitor_index = monitor ? monitor.index : null; this.dash_destroy_id = dash.connect('destroy', () => this.remove_dash_blur()); this.dash_blur_connections_ids = []; this.dash_blur_connections_ids.push( this.dash_blur.connect('remove-dashes', () => this.remove_dash_blur()), this.dash_blur.connect('override-style', () => this.override_style()), this.dash_blur.connect('remove-style', () => this.remove_style()), this.dash_blur.connect('show', () => this.background_group?.show()), this.dash_blur.connect('hide', () => this.background_group?.hide()), this.dash_blur.connect('update-size', () => this.schedule_update()), this.dash_blur.connect('change-blur-type', () => this.change_blur_type()), this.dash_blur.connect('update-pipeline', () => this.update_pipeline()) ); } schedule_update() { this.clear_pending_idles(); this.updateId = global.compositor.get_laters().add(Meta.LaterType.IDLE, () => { this.updateId = 0; this.update_size(); return false; }); } clear_pending_idles() { if (this.updateId) { global.compositor.get_laters().remove(this.updateId); this.updateId = 0; } } // IMPORTANT: do never call this in a mutable `this.dash_blur.forEach` remove_dash_blur() { // remove the style and destroy the effects this.remove_style(); this.destroy_dash(); // remove the dash infos from their list const dash_infos_index = this.dash_blur.dashes.indexOf(this); if (dash_infos_index >= 0) this.dash_blur.dashes.splice(dash_infos_index, 1); // disconnect everything if (this.bg_allocation_id && this.background_group) { this.background_group.disconnect(this.bg_allocation_id); this.bg_allocation_id = null; } this.dash_blur_connections_ids.forEach(id => { if (id) this.dash_blur.disconnect(id); }); this.dash_blur_connections_ids = []; if (this.dash_destroy_id) this.dash.disconnect(this.dash_destroy_id); this.dash_destroy_id = null; } override_style() { this.remove_style(); const style = DASH_STYLES[ this.settings.dash_to_dock.STYLE_DASH_TO_DOCK ]; // Dash to Dock owns its style class list. Replacing it removes the // extension's layout and border-radius rules. Add Blur My Shell's // visual modifier alongside the native classes instead. if (style && !this.dash.has_style_class_name(style)) this.dash.add_style_class_name(style); } remove_style() { if (this.dash?._background) this.dash._background.style = this.old_style; DASH_STYLES.forEach( style => { this.dash?.remove_style_class_name(style); this.dash_background?.remove_style_class_name(style); } ); } destroy_dash() { this.clear_pending_idles(); if (this.paint_signals) this.paint_signals.disconnect_all(); if (this.background_group && this.dash) { const parent = this.dash.get_parent(); if (parent) parent.remove_child(this.background_group); } if (this.bg_manager) { if (this.bg_manager._bms_pipeline) { this.bg_manager._bms_pipeline.destroy(); this.bg_manager._bms_pipeline = null; } this.bg_manager.backgroundActor = null; this.bg_manager.destroy(); this.bg_manager = null; } if (this.bg_allocation_id && this.background_group) { this.background_group.disconnect(this.bg_allocation_id); this.bg_allocation_id = null; } this._first_boot = null; if (this.background_group) { this.background_group.destroy(); this.background_group = null; } } change_blur_type() { this.destroy_dash(); let blur_result = this.dash_blur.add_blur(this.dash, this.dash_container); if (!blur_result) return; let [ background, background_group, bg_manager, paint_signals ] = blur_result; this.background = background; this.background_group = background_group; this.bg_manager = bg_manager; this.paint_signals = paint_signals; this.dash.get_parent().insert_child_at_index(this.background_group, 0); // Apply the layout instantly before drawing the first frame this.bg_allocation_id = this.background_group.connect('notify::allocation', () => { this.schedule_update(); }); this.schedule_update(); } update_pipeline() { if (this.bg_manager?._bms_pipeline) { this.bg_manager._bms_pipeline.change_pipeline_to( this.settings.dash_to_dock.PIPELINE ); } } update_size() { if (!this.dash_blur._has_valid_allocation(this.dash_container) || !this.dash_blur._has_valid_allocation(this.dash) || !this.dash_blur._has_valid_allocation(this.dash_background)) return; if (this.dash_blur.is_static) { let monitor = Main.layoutManager.findMonitorForActor(this.dash_container); if (!monitor) return; if (this.current_monitor_index !== monitor.index) { this.current_monitor_index = monitor.index; this.change_blur_type(); return; } let dash_box = this.get_dash_position(this.dash_container); if (!dash_box) return; const inset = utils.static_blur_clip_inset(); const clip_x = Math.floor(dash_box.clip_x) - inset; const clip_y = Math.floor(dash_box.clip_y) - inset; const clip_w = Math.ceil(this.dash_background.width) + inset * 2; const clip_h = Math.ceil(this.dash_background.height) + inset * 2; this.background.x = dash_box.background_x + inset; this.background.y = dash_box.background_y + inset; this.background.set_clip(clip_x, clip_y, clip_w, clip_h); } else { this.background.width = this.dash_background.width; this.background.height = this.dash_background.height; this.background.x = this.dash_background.x; this.background.y = this.dash_background.y + this.dash.y; } if (this._first_boot) { if (this.settings.dash_to_dock.UNBLUR_IN_OVERVIEW && Main.overview.visible) { this.background_group?.hide(); } this._first_boot = false; } } get_dash_position(dash_container) { let monitor = Main.layoutManager.findMonitorForActor(this.dash_container); if (!monitor) return; let dash_box = dash_container._slider.get_child(); if (!dash_box) return null; let parent = this.background_group?.get_parent(); if (!parent) return; let [parent_stage_x, parent_stage_y] = parent.get_transformed_position(); let [bg_stage_x, bg_stage_y] = this.dash_background.get_transformed_position(); let background_x = monitor.x - parent_stage_x; let background_y = monitor.y - parent_stage_y; let clip_x = bg_stage_x - monitor.x; let clip_y = bg_stage_y - monitor.y; return {background_x, background_y, clip_x, clip_y}; } _log(str) { if (this.settings.DEBUG) console.log(`[Blur my Shell > dash] ${str}`); } _warn(str) { console.warn(`[Blur my Shell > dash] ${str}`); } } export const DashBlur = class DashBlur extends Signals.EventEmitter { constructor(connections, settings, _) { super(); this.dashes = []; this.connections = connections; this.settings = settings; this.paint_signals = new PaintSignals(connections); this.is_static = this.settings.dash_to_dock.STATIC_BLUR; this.enabled = false; } enable() { this.connections.connect(Main.uiGroup, 'child-added', (_, actor) => { if ( (actor.get_name() === "dashtodockContainer") && (actor.constructor.name === 'DashToDock') ) this.try_blur(actor); }); this.blur_existing_dashes(); this.connect_to_overview(); this.update_size(); this.enabled = true; } // Finds all existing dashes on every monitor, and call `try_blur` on them // We cannot only blur `Main.overview.dash`, as there could be several blur_existing_dashes() { this._log("searching for dash"); // blur every dash found, filtered by name Main.uiGroup.get_children().filter((child) => { return (child.get_name() === "dashtodockContainer") && (child.constructor.name === 'DashToDock'); }).forEach(dash_container => this.try_blur(dash_container)); } _has_valid_allocation(actor) { return actor && (!actor.has_allocation || actor.has_allocation()) && actor.width > 0 && actor.height > 0; } _defer_blur_until_allocated(dash_container, dash) { if (dash_container._bms_pending_blur_setup) return; dash_container._bms_pending_blur_setup = true; const connections = []; const cleanup = () => { delete dash_container._bms_pending_blur_setup; delete dash_container._bms_defer_cleanup; connections.forEach(({ actor, id }) => { if (actor && id) actor.disconnect(id); }); }; const retry = () => { if (!dash_container._bms_pending_blur_setup) return; let current_dash_box = dash_container._slider?.get_child(); let current_dash = dash || current_dash_box?.get_children().find(child => child.get_name() === 'dash'); if (!this._has_valid_allocation(dash_container) || !this._has_valid_allocation(current_dash_box) || !this._has_valid_allocation(current_dash)) return; cleanup(); this.try_blur(dash_container); }; const dash_cont_id = dash_container.connect('notify::allocation', retry); connections.push({ actor: dash_container, id: dash_cont_id }); if (dash) { const dash_id = dash.connect('notify::allocation', retry); connections.push({ actor: dash, id: dash_id }); } const destroy_id = dash_container.connect('destroy', cleanup); connections.push({ actor: dash_container, id: destroy_id }); dash_container._bms_defer_cleanup = cleanup; } // Tries to blur the dash contained in the given actor try_blur(dash_container) { let dash_box = dash_container._slider?.get_child(); if (!dash_box){ this._defer_blur_until_allocated(dash_container); return; } let dash = dash_box.get_children().find(child => { return child.get_name() === 'dash'; }); if (!dash || !this._has_valid_allocation(dash_container) || !this._has_valid_allocation(dash_box) || !this._has_valid_allocation(dash)) { this._defer_blur_until_allocated(dash_container, dash); return; } let dash_exist = this.dashes.find(info => info.dash === dash); if (dash_exist) { dash_exist.remove_dash_blur(); } let existing_bg = dash_box.get_children().find(child => child.get_name() === "bms-dash-backgroundgroup" ); if (existing_bg) { if (dash_box.contains(existing_bg)) dash_box.remove_child(existing_bg); existing_bg.destroy(); } // verify that we did not already blur that dash this._log("dash to dock found, blurring it"); let infos = this.blur_dash_from(dash, dash_container); if (infos) { this.dashes.push(infos); } } // Blurs the dash and returns a `DashInfos` containing its information blur_dash_from(dash, dash_container) { let blur_result = this.add_blur(dash, dash_container); if (!blur_result) return null; let [background, background_group, bg_manager, paint_signals] = blur_result; // insert the background group to the right element dash.get_parent().insert_child_at_index(background_group, 0); // updates size and position on change this.connections.connect( dash, ['notify::width', 'notify::height'], _ => this.update_size() ); this.connections.connect( dash_container, [ 'notify::width', 'notify::height', 'notify::y', 'notify::x', 'notify::style-class-name', 'notify::allocation' ], _ => this.update_size() ); const slider = dash_container._slider; if (slider) { this.connections.connect( slider, ['notify::slide-x', 'notify::allocation'], _ => this.update_size() ); } const dash_background = dash._background || dash.get_children().find(child => child.has_style_class_name('dash-background')) || dash; if (!dash_background) return null; // create infos let infos = new DashInfos( this, dash, dash_container, dash_background, background, background_group, bg_manager, paint_signals ); this.update_size(); global.compositor.get_laters().add(Meta.LaterType.BEFORE_REDRAW, () => { this.update_size(); return false; }); this.update_background(); // returns infos return infos; } add_blur(dash, dash_container = null) { const target_actor = dash_container || dash; const monitor = Main.layoutManager.findMonitorForActor(target_actor) || Main.layoutManager.primaryMonitor; if (!monitor) return null; const background_group = new Meta.BackgroundGroup({ name: 'bms-dash-backgroundgroup', width: 0, height: 0 }); let background, bg_manager, paint_signals; let static_blur = this.settings.dash_to_dock.STATIC_BLUR; if (static_blur) { let bg_manager_list = []; const pipeline = new Pipeline( global.blur_my_shell._effects_manager, global.blur_my_shell._pipelines_manager, this.settings.dash_to_dock.PIPELINE ); background = pipeline.create_background_with_effects( monitor.index, bg_manager_list, background_group, 'bms-dash-blurred-widget' ); bg_manager = bg_manager_list[0]; } else { const pipeline = new DummyPipeline( global.blur_my_shell._effects_manager, this.settings.dash_to_dock ); [background, bg_manager] = pipeline.create_background_with_effect( background_group, 'bms-dash-blurred-widget' ); paint_signals = new PaintSignals(this.connections); // HACK // //`Shell.BlurEffect` does not repaint when shadows are under it. [1] // // This does not entirely fix this bug (shadows caused by windows // still cause artifacts), but it prevents the shadows of the dash // buttons to cause artifacts on the dash itself // // [1]: https://gitlab.gnome.org/GNOME/gnome-shell/-/issues/2857 if (this.settings.HACKS_LEVEL === 1) { this._log("hack level 1"); paint_signals.disconnect_all(); paint_signals.connect(background, pipeline.effect); } else { paint_signals.disconnect_all(); } } return [background, background_group, bg_manager, paint_signals]; } change_blur_type() { this.is_static = this.settings.dash_to_dock.STATIC_BLUR; this.emit('change-blur-type'); this.update_background(); } /// Connect when overview if opened/closed to hide/show the blur accordingly connect_to_overview() { this.connections.disconnect_all_for(Main.overview); if (this.settings.dash_to_dock.UNBLUR_IN_OVERVIEW) { this.connections.connect( Main.overview, 'showing', _ => this.hide() ); this.connections.connect( Main.overview, 'hidden', _ => this.show() ); } }; /// Updates the background to either remove it or not, according to the /// user preferences. update_background() { this._log("updating background"); if (this.settings.dash_to_dock.OVERRIDE_BACKGROUND) this.emit('override-style'); else this.emit('remove-style'); } update_pipeline() { this.emit('update-pipeline'); } update_size() { this.emit('update-size'); } show() { this.emit('show'); } hide() { this.emit('hide'); } disable() { this._log("removing blur from dashes"); this.emit('remove-dashes'); for (let child of Main.uiGroup.get_children()) { if (child._bms_defer_cleanup) child._bms_defer_cleanup(); delete child._bms_pending_blur_setup; } this.dashes = []; this.connections.disconnect_all(); this.enabled = false; } _log(str) { if (this.settings.DEBUG) console.log(`[Blur my Shell > dash manager] ${str}`); } _warn(str) { console.warn(`[Blur my Shell > dash manager] ${str}`); } };