This commit is contained in:
2024-07-08 22:46:35 +02:00
parent 02f44c49d2
commit 27254d817a
56249 changed files with 808097 additions and 1 deletions

View File

@ -0,0 +1,106 @@
import GObject from 'gi://GObject';
/// An object to easily manage signals.
export const Connections = class Connections {
constructor() {
this.buffer = [];
}
/// Adds a connection.
///
/// Takes as arguments:
/// - an actor, which fires the signal
/// - signal(s) (string or array of strings), which are watched for
/// - a callback, which is called when the signal is fired
connect(actor, signals, handler) {
if (signals instanceof Array) {
signals.forEach(signal => {
let id = actor.connect(signal, handler);
this.process_connection(actor, id);
});
} else {
let id = actor.connect(signals, handler);
this.process_connection(actor, id);
}
}
/// Process the given actor and id.
///
/// This makes sure that the signal is disconnected when the actor is
/// destroyed, and that the signal can be managed through other Connections
/// methods.
process_connection(actor, id) {
let infos = {
actor: actor,
id: id
};
// remove the signal when the actor is destroyed
if (
actor.connect &&
(
!(actor instanceof GObject.Object) ||
GObject.signal_lookup('destroy', actor)
)
) {
let destroy_id = actor.connect('destroy', () => {
actor.disconnect(id);
actor.disconnect(destroy_id);
let index = this.buffer.indexOf(infos);
if (index >= 0) {
this.buffer.splice(index, 1);
}
});
infos.destroy_id = destroy_id;
}
this.buffer.push(infos);
}
/// Disconnects every connection found for an actor.
disconnect_all_for(actor) {
// get every connection stored for the actor
let actor_connections = this.buffer.filter(
infos => infos.actor === actor
);
// remove each of them
actor_connections.forEach(connection => {
// disconnect
try {
connection.actor.disconnect(connection.id);
if ('destroy_id' in connection)
connection.actor.disconnect(connection.destroy_id);
} catch (e) {
this._warn(`error removing connection: ${e}; continuing`);
}
// remove from buffer
let index = this.buffer.indexOf(connection);
this.buffer.splice(index, 1);
});
}
/// Disconnect every connection for each actor.
disconnect_all() {
this.buffer.forEach(connection => {
// disconnect
try {
connection.actor.disconnect(connection.id);
if ('destroy_id' in connection)
connection.actor.disconnect(connection.destroy_id);
} catch (e) {
this._warn(`error removing connection: ${e}; continuing`);
}
});
// reset buffer
this.buffer = [];
}
_warn(str) {
console.warn(`[Blur my Shell > connections] ${str}`);
}
};

View File

@ -0,0 +1,96 @@
import St from 'gi://St';
import Clutter from 'gi://Clutter';
/// A dummy `Pipeline`, for dynamic blur only.
/// Instead of a pipeline id, we take the settings of the component we want to blur.
export const DummyPipeline = class DummyPipeline {
constructor(effects_manager, settings, actor = null) {
this.effects_manager = effects_manager;
this.settings = settings;
this.effect = null;
this.attach_effect_to_actor(actor);
}
create_background_with_effect(
background_group,
widget_name
) {
// create the new actor
this.actor = new St.Widget({ name: widget_name });
this.attach_effect_to_actor(this.actor);
// a dummy `BackgroundManager`, just to access the pipeline easily
let bg_manager = new Clutter.Actor;
bg_manager.backgroundActor = this.actor;
bg_manager._bms_pipeline = this;
background_group.insert_child_at_index(this.actor, 0);
return [this.actor, bg_manager];
};
attach_effect_to_actor(actor) {
// set the actor
this.actor = actor;
if (!actor)
return;
// build the new effect to be added
this.build_effect({
unscaled_radius: 2 * this.settings.SIGMA,
brightness: this.settings.BRIGHTNESS,
});
// add the effect to the actor
if (this.actor)
this.actor.add_effect(this.effect);
else
this._warn(`could not add effect to actor, actor does not exist anymore`);
}
build_effect(params) {
// create the effect
this.effect = this.effects_manager.new_native_dynamic_gaussian_blur_effect(params);
// connect to settings changes, using the true gsettings object
this._sigma_changed_id = this.settings.settings.connect(
'changed::sigma', () => this.effect.unscaled_radius = 2 * this.settings.SIGMA
);
this._brightness_changed_id = this.settings.settings.connect(
'changed::brightness', () => this.effect.brightness = this.settings.BRIGHTNESS
);
}
repaint_effect() {
this.effect?.queue_repaint();
}
/// Remove every effect from the actor it is attached to. Please note that they are not
/// destroyed, but rather stored (thanks to the `EffectManager` class) to be reused later.
remove_effect() {
this.effects_manager.remove(this.effect);
this.effect = null;
if (this._sigma_changed_id)
this.settings.settings.disconnect(this._sigma_changed_id);
if (this._brightness_changed_id)
this.settings.settings.disconnect(this._brightness_changed_id);
delete this._sigma_changed_id;
delete this._brightness_changed_id;
}
/// Do nothing for this dummy pipeline.
/// Note: exposed to public API.
change_pipeline_to() { return; }
/// Note: exposed to public API.
destroy() {
this.remove_effect();
this.actor = null;
}
_warn(str) {
console.warn(`[Blur my Shell > dummy pip] ${str}`);
}
};

View File

@ -0,0 +1,90 @@
import { get_supported_effects } from '../effects/effects.js';
/// An object to manage effects (by not destroying them all the time)
export const EffectsManager = class EffectsManager {
constructor(connections) {
this.connections = connections;
this.used = [];
this.SUPPORTED_EFFECTS = get_supported_effects();
Object.keys(this.SUPPORTED_EFFECTS).forEach(effect_name => {
// init the arrays containing each unused effect
this[effect_name + '_effects'] = [];
// init the functions for each effect
this['new_' + effect_name + '_effect'] = function (params) {
let effect;
if (this[effect_name + '_effects'].length > 0) {
effect = this[effect_name + '_effects'].splice(0, 1)[0];
effect.set({
...this.SUPPORTED_EFFECTS[effect_name].class.default_params, ...params
});
} else
effect = new this.SUPPORTED_EFFECTS[effect_name].class({
...this.SUPPORTED_EFFECTS[effect_name].class.default_params, ...params
});
this.used.push(effect);
this.connect_to_destroy(effect);
return effect;
};
});
}
connect_to_destroy(effect) {
effect.old_actor = effect.get_actor();
if (effect.old_actor)
effect.old_actor_id = effect.old_actor.connect('destroy', _ => {
this.remove(effect, true);
});
this.connections.connect(effect, 'notify::actor', _ => {
let actor = effect.get_actor();
if (effect.old_actor && actor != effect.old_actor)
effect.old_actor.disconnect(effect.old_actor_id);
if (actor && actor != effect.old_actor) {
effect.old_actor_id = actor.connect('destroy', _ => {
this.remove(effect, true);
});
}
});
}
// IMPORTANT: do never call this in a mutable `this.used.forEach`
remove(effect, actor_already_destroyed = false) {
if (!actor_already_destroyed)
try {
effect.get_actor()?.remove_effect(effect);
} catch (e) {
this._warn(`could not remove the effect, continuing: ${e}`);
}
if (effect.old_actor)
effect.old_actor.disconnect(effect.old_actor_id);
delete effect.old_actor;
delete effect.old_actor_id;
let index = this.used.indexOf(effect);
if (index >= 0) {
this.used.splice(index, 1);
Object.keys(this.SUPPORTED_EFFECTS).forEach(effect_name => {
if (effect instanceof this.SUPPORTED_EFFECTS[effect_name].class)
this[effect_name + '_effects'].push(effect);
});
}
}
destroy_all() {
const immutable_used_list = [...this.used];
immutable_used_list.forEach(effect => this.remove(effect));
Object.keys(this.SUPPORTED_EFFECTS).forEach(effect_name => {
this[effect_name + '_effects'].splice(0, this[effect_name + '_effects'].length);
});
}
_warn(str) {
console.warn(`[Blur my Shell > effects mng] ${str}`);
}
};

View File

@ -0,0 +1,182 @@
import { Type } from './settings.js';
// This lists the preferences keys
export const KEYS = [
{
component: "general", schemas: [
{ type: Type.PIPELINES, name: "pipelines" },
{ type: Type.I, name: "hacks-level" },
{ type: Type.B, name: "debug" },
]
},
{
component: "overview", schemas: [
{ type: Type.B, name: "blur" },
{ type: Type.S, name: "pipeline" },
{ type: Type.I, name: "style-components" },
]
},
{
component: "appfolder", schemas: [
{ type: Type.B, name: "blur" },
{ type: Type.I, name: "sigma" },
{ type: Type.D, name: "brightness" },
{ type: Type.I, name: "style-dialogs" },
]
},
{
component: "panel", schemas: [
{ type: Type.B, name: "blur" },
{ type: Type.B, name: "static-blur" },
{ type: Type.S, name: "pipeline" },
{ type: Type.I, name: "sigma" },
{ type: Type.D, name: "brightness" },
{ type: Type.B, name: "unblur-in-overview" },
{ type: Type.B, name: "force-light-text" },
{ type: Type.B, name: "override-background" },
{ type: Type.I, name: "style-panel" },
{ type: Type.B, name: "override-background-dynamically" },
]
},
{
component: "dash-to-dock", schemas: [
{ type: Type.B, name: "blur" },
{ type: Type.B, name: "static-blur" },
{ type: Type.S, name: "pipeline" },
{ type: Type.I, name: "sigma" },
{ type: Type.D, name: "brightness" },
{ type: Type.B, name: "unblur-in-overview" },
{ type: Type.B, name: "override-background" },
{ type: Type.I, name: "style-dash-to-dock" },
]
},
{
component: "applications", schemas: [
{ type: Type.B, name: "blur" },
{ type: Type.I, name: "sigma" },
{ type: Type.D, name: "brightness" },
{ type: Type.I, name: "opacity" },
{ type: Type.B, name: "dynamic-opacity" },
{ type: Type.B, name: "blur-on-overview" },
{ type: Type.B, name: "enable-all" },
{ type: Type.AS, name: "whitelist" },
{ type: Type.AS, name: "blacklist" },
]
},
{
component: "lockscreen", schemas: [
{ type: Type.B, name: "blur" },
{ type: Type.S, name: "pipeline" },
]
},
{
component: "window-list", schemas: [
{ type: Type.B, name: "blur" },
{ type: Type.S, name: "pipeline" },
{ type: Type.I, name: "sigma" },
{ type: Type.D, name: "brightness" },
]
},
{
component: "screenshot", schemas: [
{ type: Type.B, name: "blur" },
{ type: Type.S, name: "pipeline" },
]
},
{
component: "hidetopbar", schemas: [
{ type: Type.B, name: "compatibility" },
]
},
{
component: "dash-to-panel", schemas: [
{ type: Type.B, name: "blur-original-panel" },
]
},
];
// This lists the deprecated preferences keys
export const DEPRECATED_KEYS = [
{
component: "general", schemas: [
{ type: Type.I, name: "sigma" },
{ type: Type.D, name: "brightness" },
{ type: Type.C, name: "color" },
{ type: Type.D, name: "noise-amount" },
{ type: Type.D, name: "noise-lightness" },
{ type: Type.B, name: "color-and-noise" },
]
},
{
component: "overview", schemas: [
{ type: Type.B, name: "customize" },
{ type: Type.I, name: "sigma" },
{ type: Type.D, name: "brightness" },
{ type: Type.C, name: "color" },
{ type: Type.D, name: "noise-amount" },
{ type: Type.D, name: "noise-lightness" },
]
},
{
component: "appfolder", schemas: [
{ type: Type.B, name: "customize" },
{ type: Type.C, name: "color" },
{ type: Type.D, name: "noise-amount" },
{ type: Type.D, name: "noise-lightness" },
]
},
{
component: "panel", schemas: [
{ type: Type.B, name: "customize" },
{ type: Type.C, name: "color" },
{ type: Type.D, name: "noise-amount" },
{ type: Type.D, name: "noise-lightness" },
]
},
{
component: "dash-to-dock", schemas: [
{ type: Type.B, name: "customize" },
{ type: Type.C, name: "color" },
{ type: Type.D, name: "noise-amount" },
{ type: Type.D, name: "noise-lightness" },
{ type: Type.I, name: "corner-radius" },
]
},
{
component: "applications", schemas: [
{ type: Type.B, name: "customize" },
{ type: Type.C, name: "color" },
{ type: Type.D, name: "noise-amount" },
{ type: Type.D, name: "noise-lightness" },
]
},
{
component: "lockscreen", schemas: [
{ type: Type.B, name: "customize" },
{ type: Type.I, name: "sigma" },
{ type: Type.D, name: "brightness" },
{ type: Type.C, name: "color" },
{ type: Type.D, name: "noise-amount" },
{ type: Type.D, name: "noise-lightness" },
]
},
{
component: "window-list", schemas: [
{ type: Type.B, name: "customize" },
{ type: Type.C, name: "color" },
{ type: Type.D, name: "noise-amount" },
{ type: Type.D, name: "noise-lightness" },
]
},
{
component: "screenshot", schemas: [
{ type: Type.B, name: "customize" },
{ type: Type.I, name: "sigma" },
{ type: Type.D, name: "brightness" },
{ type: Type.C, name: "color" },
{ type: Type.D, name: "noise-amount" },
{ type: Type.D, name: "noise-lightness" },
]
},
];

View File

@ -0,0 +1,91 @@
import GObject from 'gi://GObject';
import Clutter from 'gi://Clutter';
export const PaintSignals = class PaintSignals {
constructor(connections) {
this.buffer = [];
this.connections = connections;
}
connect(actor, blur_effect) {
let paint_effect = new EmitPaintSignal();
let infos = {
actor: actor,
paint_effect: paint_effect
};
let counter = 0;
actor.add_effect(paint_effect);
this.connections.connect(paint_effect, 'update-blur', () => {
try {
// checking if blur_effect.queue_repaint() has been recently called
if (counter === 0) {
counter = 2;
blur_effect.queue_repaint();
}
else counter--;
} catch (e) { }
});
// remove the actor from buffer when it is destroyed
if (
actor.connect &&
(
!(actor instanceof GObject.Object) ||
GObject.signal_lookup('destroy', actor)
)
)
this.connections.connect(actor, 'destroy', () => {
const immutable_buffer = [...this.buffer];
immutable_buffer.forEach(infos => {
if (infos.actor === actor) {
// remove from buffer
let index = this.buffer.indexOf(infos);
this.buffer.splice(index, 1);
}
});
});
this.buffer.push(infos);
}
disconnect_all_for_actor(actor) {
const immutable_buffer = [...this.buffer];
immutable_buffer.forEach(infos => {
if (infos.actor === actor) {
this.connections.disconnect_all_for(infos.paint_effect);
infos.actor.remove_effect(infos.paint_effect);
// remove from buffer
let index = this.buffer.indexOf(infos);
this.buffer.splice(index, 1);
}
});
}
disconnect_all() {
this.buffer.forEach(infos => {
this.connections.disconnect_all_for(infos.paint_effect);
infos.actor.remove_effect(infos.paint_effect);
});
this.buffer = [];
}
};
export const EmitPaintSignal = GObject.registerClass({
GTypeName: 'EmitPaintSignal',
Signals: {
'update-blur': {
param_types: []
},
}
},
class EmitPaintSignal extends Clutter.Effect {
vfunc_paint(node, paint_context, paint_flags) {
this.emit("update-blur");
super.vfunc_paint(node, paint_context, paint_flags);
}
}
);

View File

@ -0,0 +1,204 @@
import St from 'gi://St';
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
import * as Background from 'resource:///org/gnome/shell/ui/background.js';
/// A `Pipeline` object is a handy way to manage the effects attached to an actor. It only manages
/// one actor at a time (so blurring multiple widgets will need multiple `Pipeline`), and is
/// linked to a `pipeline_id` that has been (hopefully) defined in the settings.
///
/// It communicates with the settings through the `PipelinesManager` object, and receives different
/// signals (with `pipeline_id` being an unique string):
/// - `'pipeline_id'::pipeline-updated`, handing a new pipeline descriptor object, when the pipeline
/// has been changed enough that it needs to rebuild the effects configuration
/// - `'pipeline_id'::pipeline-destroyed`, when the pipeline has been destroyed; thus making the
/// `Pipeline` change its id to `pipeline_default`
///
/// And each effect, with an unique `id`, is connected to the `PipelinesManager` for the signals:
/// - `'pipeline_id'::effect-'id'-key-removed`, handing the key that was removed
/// - `'pipeline_id'::effect-'id'-key-updated`, handing the key that was changed and its new value
/// - `'pipeline_id'::effect-'id'-key-added`, handing the key that was added and its value
export const Pipeline = class Pipeline {
constructor(effects_manager, pipelines_manager, pipeline_id, actor = null) {
this.effects_manager = effects_manager;
this.pipelines_manager = pipelines_manager;
this.effects = [];
this.set_pipeline_id(pipeline_id);
this.attach_pipeline_to_actor(actor);
}
/// Create a background linked to the monitor with index `monitor_index`, with a
/// `BackgroundManager` that is appended to the list `background_managers`. The background actor
/// will be given the name `widget_name` and inserted into the given `background_group`.
/// Note: exposed to public API.
create_background_with_effects(
monitor_index,
background_managers,
background_group,
widget_name
) {
let monitor = Main.layoutManager.monitors[monitor_index];
// create the new actor
this.actor = new St.Widget({
name: widget_name,
x: monitor.x,
y: monitor.y,
width: monitor.width,
height: monitor.height
});
// remove the effects, wether or not we attach the pipeline to the actor: if they are fired
// while the actor has changed, this could go bad
this.remove_all_effects();
if (this.pipeline_id)
this.attach_pipeline_to_actor(this.actor);
let bg_manager = new Background.BackgroundManager({
container: this.actor,
monitorIndex: monitor_index,
controlPosition: false,
});
bg_manager._bms_pipeline = this;
background_managers.push(bg_manager);
background_group.insert_child_at_index(this.actor, 0);
return this.actor;
};
/// Set the pipeline id, correctly connecting the `Pipeline` object to listen the pipelines
/// manager for pipeline-wide changes. This does not update the effects in consequence, call
/// `change_pipeline_to` instead if you want to reconstruct the effects too.
set_pipeline_id(pipeline_id) {
// disconnect ancient signals
this.remove_connections();
// change the id
this.pipeline_id = pipeline_id;
// connect to settings changes
this._pipeline_changed_id = this.pipelines_manager.connect(
this.pipeline_id + '::pipeline-updated',
(_, new_pipeline) => this.update_effects_from_pipeline(new_pipeline)
);
this._pipeline_destroyed_id = this.pipelines_manager.connect(
this.pipeline_id + '::pipeline-destroyed',
_ => this.change_pipeline_to("pipeline_default")
);
}
/// Disconnect the signals for the pipeline changes. Please note that the signals related to the
/// effects are stored with them and removed with `remove_all_effects`.
remove_connections() {
if (this._pipeline_changed_id)
this.pipelines_manager.disconnect(this._pipeline_changed_id);
if (this._pipeline_destroyed_id)
this.pipelines_manager.disconnect(this._pipeline_destroyed_id);
this._pipeline_changed_id = null;
this._pipeline_destroyed_id = null;
}
/// Attach a Pipeline object with `pipeline_id` already set to an actor.
attach_pipeline_to_actor(actor) {
// set the actor
this.actor = actor;
if (!actor)
return;
// attach the pipeline
let pipeline = this.pipelines_manager.pipelines[this.pipeline_id];
if (!pipeline) {
this._warn(`could not attach pipeline to actor, pipeline "${this.pipeline_id}" not found`);
// do not recurse...
if ("pipeline_default" in this.pipelines_manager.pipelines) {
this.set_pipeline_id("pipeline_default");
pipeline = this.pipelines_manager.pipelines["pipeline_default"];
} else
return;
}
// update the effects
this.update_effects_from_pipeline(pipeline);
}
/// Update the effects from the given pipeline object, the hard way.
update_effects_from_pipeline(pipeline) {
// remove all effects
this.remove_all_effects();
// build the new effects to be added
pipeline.effects.forEach(effect => {
if ('new_' + effect.type + '_effect' in this.effects_manager)
this.build_effect(effect);
else
this._warn(`could not add effect to actor, effect "${effect.type}" not found`);
});
this.effects.reverse();
// add the effects to the actor
if (this.actor)
this.effects.forEach(effect => this.actor.add_effect(effect));
else
this._warn(`could not add effect to actor, actor does not exist anymore`);
}
/// Given an `effect_infos` object containing the effect type, id and params, build an effect
/// and append it to the effects list
build_effect(effect_infos) {
let effect = this.effects_manager['new_' + effect_infos.type + '_effect'](effect_infos.params);
this.effects.push(effect);
// connect to settings changes
effect._effect_key_removed_id = this.pipelines_manager.connect(
this.pipeline_id + '::effect-' + effect_infos.id + '-key-removed',
(_, key) => effect[key] = effect.constructor.default_params[key]
);
effect._effect_key_updated_id = this.pipelines_manager.connect(
this.pipeline_id + '::effect-' + effect_infos.id + '-key-updated',
(_, key, value) => effect[key] = value
);
effect._effect_key_added_id = this.pipelines_manager.connect(
this.pipeline_id + '::effect-' + effect_infos.id + '-key-added',
(_, key, value) => effect[key] = value
);
}
/// Remove every effect from the actor it is attached to. Please note that they are not
/// destroyed, but rather stored (thanks to the `EffectManager` class) to be reused later.
remove_all_effects() {
this.effects.forEach(effect => {
this.effects_manager.remove(effect);
[
effect._effect_key_removed_id,
effect._effect_key_updated_id,
effect._effect_key_added_id
].forEach(
id => { if (id) this.pipelines_manager.disconnect(id); }
);
delete effect._effect_key_removed_id;
delete effect._effect_key_updated_id;
delete effect._effect_key_added_id;
});
this.effects = [];
}
/// Change the pipeline id, and update the effects according to this change.
/// Note: exposed to public API.
change_pipeline_to(pipeline_id) {
this.set_pipeline_id(pipeline_id);
this.attach_pipeline_to_actor(this.actor);
}
/// Resets the `Pipeline` object to a sane state, removing every effect and signal.
/// Note: exposed to public API.
destroy() {
this.remove_all_effects();
this.remove_connections();
this.actor = null;
this.pipeline_id = null;
}
_warn(str) {
console.warn(`[Blur my Shell > pipeline] ${str}`);
}
};

View File

@ -0,0 +1,168 @@
const Signals = imports.signals;
/// The `PipelinesManager` object permits to store the list of pipelines and their effects in
/// memory. It is meant to *always* be in sync with the `org.gnome.shell.extensions.blur-my-shell`'s
/// `pipelines` gschema. However, we do not want to re-create every effect each time this schema is
/// changed, so the pipelines manager handles it, and dispatches the updates with targeted signals.
///
/// It is only connected to ONE signal (the pipelines schema being changed), and emits numerous
/// which are connected to by both the different `Pipeline` objects in the extension, and by the
/// different pages of the extension preferences.
/// It emits three different types of signals:
///
/// - general changes to the pipelines list, connected to by the extension preferences:
/// - `pipeline-list-changed`, when the list of pipelines has changed (by creation or deletion)
/// - `pipeline-names-changed`, when the name of a pipeline is changed
///
/// - signals that are targeted towards a given pipeline, with `pipeline_id` being its unique id:
/// - `'pipeline_id'::pipeline-updated`, handing a new pipeline descriptor object, when the
/// pipeline has been changed quite a bit (added/destroyed/reordered the effects)
/// - `'pipeline_id'::pipeline-destroyed`, when the pipeline has been destroyed
/// - `'pipeline_id'::pipeline-renamed`, handing the new name, when the pipeline has been
/// renamed, which is only important for the preferences
///
/// - signals that are targeted towards a given effect, with `effect_id` being its unique id, and
/// `pipeline_id` the unique id of the pipeline it is attached to:
/// - `'pipeline_id'::effect-'effect_id'-key-removed`, handing the key that was removed
/// - `'pipeline_id'::effect-'effect_id'-key-updated`, handing the key that was changed and its
/// new value
/// - `'pipeline_id'::effect-'effect_id'-key-added`, handing the key that was added and its
/// value
export class PipelinesManager {
constructor(settings) {
this.settings = settings;
this.pipelines = this.settings.PIPELINES;
this.settings.PIPELINES_changed(_ => this.on_pipeline_update());
}
create_pipeline(name, effects = []) {
// select a random id for the pipeline
let id = "pipeline_" + ("" + Math.random()).slice(2, 16);
// add a random ID for each effect, to help tracking them
effects.forEach(effect => effect.id = "effect_" + ("" + Math.random()).slice(2, 16));
this.pipelines[id] = { name, effects };
this.settings.PIPELINES = this.pipelines;
this._emit('pipeline-created', id, this.pipelines[id]);
this._emit('pipeline-list-changed');
return id;
}
duplicate_pipeline(id) {
if (!(id in this.pipelines)) {
this._warn(`could not duplicate pipeline, id ${id} does not exist`);
return;
}
const pipeline = this.pipelines[id];
this.create_pipeline(pipeline.name + " - duplicate", [...pipeline.effects]);
this.settings.PIPELINES = this.pipelines;
}
delete_pipeline(id) {
if (!(id in this.pipelines)) {
this._warn(`could not delete pipeline, id ${id} does not exist`);
return;
}
if (id == "pipeline_default") {
this._warn(`could not delete pipeline "pipeline_default" as it is immutable`);
return;
}
delete this.pipelines[id];
this.settings.PIPELINES = this.pipelines;
this._emit(id + '::pipeline-destroyed');
this._emit('pipeline-list-changed');
}
update_pipeline_effects(id, effects, emit_update_signal = true) {
if (!(id in this.pipelines)) {
this._warn(`could not update pipeline effects, id ${id} does not exist`);
return;
}
this.pipelines[id].effects = [...effects];
this.settings.PIPELINES = this.pipelines;
if (emit_update_signal)
this._emit(id + '::pipeline-updated');
}
rename_pipeline(id, name) {
if (!(id in this.pipelines)) {
this._warn(`could not rename pipeline, id ${id} does not exist`);
return;
}
this.pipelines[id].name = name.slice();
this.settings.PIPELINES = this.pipelines;
this._emit(id + '::pipeline-renamed', name);
this._emit('pipeline-names-changed');
}
on_pipeline_update() {
const old_pipelines = this.pipelines;
this.pipelines = this.settings.PIPELINES;
for (var pipeline_id in old_pipelines) {
// if we find a pipeline that does not exist anymore, signal it
if (!(pipeline_id in this.pipelines)) {
this._emit(pipeline_id + '::pipeline-destroyed');
continue;
}
const old_pipeline = old_pipelines[pipeline_id];
const new_pipeline = this.pipelines[pipeline_id];
// verify if both pipelines have effects in the same order
// if they have, then check for their parameters
if (
old_pipeline.effects.length == new_pipeline.effects.length &&
old_pipeline.effects.every((effect, i) => effect.id === new_pipeline.effects[i].id)
) {
for (let i = 0; i < old_pipeline.effects.length; i++) {
const old_effect = old_pipeline.effects[i];
const new_effect = new_pipeline.effects[i];
const id = old_effect.id;
for (let key in old_effect.params) {
// if a key was removed, we emit to tell the effect to use the default value
if (!(key in new_effect.params))
this._emit(
pipeline_id + '::effect-' + id + '-key-removed', key
);
// if a key was updated, we emit to tell the effect to change its value
else if (old_effect.params[key] != new_effect.params[key])
this._emit(
pipeline_id + '::effect-' + id + '-key-updated', key, new_effect.params[key]
);
}
for (let key in new_effect.params) {
// if a key was added, we emit to tell the effect the key and its value
if (!(key in old_effect.params))
this._emit(
pipeline_id + '::effect-' + id + '-key-added', key, new_effect.params[key]
);
}
}
}
// if either the order has changed, or there are new effects, then rebuild it
else
this._emit(pipeline_id + '::pipeline-updated', new_pipeline);
}
}
destroy() {
this.settings.PIPELINES_disconnect();
}
_emit(signal, ...args) {
this.emit(signal, ...args);
this._log(`signal: '${signal}', arguments: ${args}`);
}
_log(str) {
if (this.settings.DEBUG)
console.log(`[Blur my Shell > pipelines] ${str}`);
}
_warn(str) {
console.warn(`[Blur my Shell > pipelines] ${str}`);
}
}
Signals.addSignalMethods(PipelinesManager.prototype);

View File

@ -0,0 +1,363 @@
import GLib from 'gi://GLib';
const Signals = imports.signals;
/// An enum non-extensively describing the type of gsettings key.
export const Type = {
B: 'Boolean',
I: 'Integer',
D: 'Double',
S: 'String',
C: 'Color',
AS: 'StringArray',
PIPELINES: 'Pipelines'
};
/// An object to get and manage the gsettings preferences.
///
/// Should be initialized with an array of keys, for example:
///
/// let settings = new Settings([
/// { type: Type.I, name: "panel-corner-radius" },
/// { type: Type.B, name: "debug" }
/// ]);
///
/// Each {type, name} object represents a gsettings key, which must be created
/// in the gschemas.xml file of the extension.
export const Settings = class Settings {
constructor(keys, settings) {
this.settings = settings;
this.keys = keys;
this.keys.forEach(bundle => {
let component = this;
let component_settings = settings;
if (bundle.component !== "general") {
let bundle_component = bundle.component.replaceAll('-', '_');
this[bundle_component] = {
settings: this.settings.get_child(bundle.component)
};
component = this[bundle_component];
component_settings = settings.get_child(bundle.component);
}
bundle.schemas.forEach(key => {
let property_name = this.get_property_name(key.name);
switch (key.type) {
case Type.B:
Object.defineProperty(component, property_name, {
get() {
return component_settings.get_boolean(key.name);
},
set(v) {
component_settings.set_boolean(key.name, v);
}
});
break;
case Type.I:
Object.defineProperty(component, property_name, {
get() {
return component_settings.get_int(key.name);
},
set(v) {
component_settings.set_int(key.name, v);
}
});
break;
case Type.D:
Object.defineProperty(component, property_name, {
get() {
return component_settings.get_double(key.name);
},
set(v) {
component_settings.set_double(key.name, v);
}
});
break;
case Type.S:
Object.defineProperty(component, property_name, {
get() {
return component_settings.get_string(key.name);
},
set(v) {
component_settings.set_string(key.name, v);
}
});
break;
case Type.C:
Object.defineProperty(component, property_name, {
// returns the array [red, blue, green, alpha] with
// values between 0 and 1
get() {
let val = component_settings.get_value(key.name);
return val.deep_unpack();
},
// takes an array [red, blue, green, alpha] with
// values between 0 and 1
set(v) {
let val = new GLib.Variant("(dddd)", v);
component_settings.set_value(key.name, val);
}
});
break;
case Type.AS:
Object.defineProperty(component, property_name, {
get() {
let val = component_settings.get_value(key.name);
return val.deep_unpack();
},
set(v) {
let val = new GLib.Variant("as", v);
component_settings.set_value(key.name, val);
}
});
break;
case Type.PIPELINES:
Object.defineProperty(component, property_name, {
get() {
let pips = component_settings.get_value(key.name).deep_unpack();
Object.keys(pips).forEach(pipeline_id => {
let pipeline = pips[pipeline_id];
if (!('name' in pipeline)) {
this._warn('impossible to get pipelines, pipeline has not name, resetting');
component[property_name + '_reset']();
return component[property_name];
}
let name = pipeline.name.deep_unpack();
if (typeof name !== 'string') {
this._warn('impossible to get pipelines, pipeline name is not a string, resetting');
component[property_name + '_reset']();
return component[property_name];
}
if (!('effects' in pipeline)) {
this._warn('impossible to get pipelines, pipeline has not effects, resetting');
component[property_name + '_reset']();
return component[property_name];
}
let effects = pipeline.effects.deep_unpack();
if (!Array.isArray(effects)) {
this._warn('impossible to get pipelines, pipeline effects is not an array, resetting');
component[property_name + '_reset']();
return component[property_name];
}
effects = effects.map(effect => effect.deep_unpack());
effects.forEach(effect => {
if (!('type' in effect)) {
this._warn('impossible to get pipelines, effect has not type, resetting');
component[property_name + '_reset']();
return component[property_name];
}
let type = effect.type.deep_unpack();
if (typeof type !== 'string') {
this._warn('impossible to get pipelines, effect type is not a string, resetting');
component[property_name + '_reset']();
return component[property_name];
}
if (!('id' in effect)) {
this._warn('impossible to get pipelines, effect has not id, resetting');
component[property_name + '_reset']();
return component[property_name];
}
let id = effect.id.deep_unpack();
if (typeof id !== 'string') {
this._warn('impossible to get pipelines, effect id is not a string, resetting');
component[property_name + '_reset']();
return component[property_name];
}
let params = {};
if ('params' in effect)
params = effect.params.deep_unpack();
if (!(params && typeof params === 'object' && params.constructor === Object)) {
this._warn('impossible to get pipelines, effect params is not an object, resetting');
component[property_name + '_reset']();
return component[property_name];
}
Object.keys(params).forEach(param_key => {
params[param_key] = params[param_key].deep_unpack();
});
effect.type = type;
effect.id = id;
effect.params = params;
});
pipeline.name = name;
pipeline.effects = effects;
});
return pips;
},
set(pips) {
let pipelines = {};
Object.keys(pips).forEach(pipeline_id => {
let pipeline = pips[pipeline_id];
if (!(pipeline && typeof pipeline === 'object' && pipeline.constructor === Object)) {
this._warn('impossible to set pipelines, pipeline is not an object');
return;
}
if (!('name' in pipeline)) {
this._warn('impossible to set pipelines, pipeline has no name');
return;
}
if (typeof pipeline.name !== 'string') {
this._warn('impossible to set pipelines, pipeline name is not a string');
return;
}
if (!('effects' in pipeline)) {
this._warn('impossible to set pipelines, pipeline has no effect');
return;
}
if (!Array.isArray(pipeline.effects)) {
this._warn('impossible to set pipelines, effects is not an array');
return;
}
let gvariant_effects = [];
pipeline.effects.forEach(effect => {
if (!(effect instanceof Object)) {
this._warn('impossible to set pipelines, effect is not an object');
return;
}
if (!('type' in effect)) {
this._warn('impossible to set pipelines, effect has not type');
return;
}
if (typeof effect.type !== 'string') {
this._warn('impossible to set pipelines, effect type is not a string');
return;
}
if (!('id' in effect)) {
this._warn('impossible to set pipelines, effect has not id');
return;
}
if (typeof effect.id !== 'string') {
this._warn('impossible to set pipelines, effect id is not a string');
return;
}
let params = {};
if ('params' in effect) {
params = effect.params;
}
let gvariant_params = {};
Object.keys(params).forEach(param_key => {
let param = params[param_key];
if (typeof param === 'boolean')
gvariant_params[param_key] = GLib.Variant.new_boolean(param);
else if (typeof param === 'number') {
if (Number.isInteger(param))
gvariant_params[param_key] = GLib.Variant.new_int32(param);
else
gvariant_params[param_key] = GLib.Variant.new_double(param);
} else if (typeof param === 'string')
gvariant_params[param_key] = GLib.Variant.new_string(param);
else if (Array.isArray(param) && param.length == 4)
gvariant_params[param_key] = new GLib.Variant("(dddd)", param);
else
this._warn('impossible to set pipeline, effect parameter type is unknown');
});
gvariant_effects.push(
new GLib.Variant("a{sv}", {
type: GLib.Variant.new_string(effect.type),
id: GLib.Variant.new_string(effect.id),
params: new GLib.Variant("a{sv}", gvariant_params)
})
);
});
pipelines[pipeline_id] = {
name: GLib.Variant.new_string(pipeline.name),
effects: new GLib.Variant("av", gvariant_effects)
};
});
let val = new GLib.Variant("a{sa{sv}}", pipelines);
component_settings.set_value(key.name, val);
}
});
break;
}
component[property_name + '_reset'] = function () {
return component_settings.reset(key.name);
};
component[property_name + '_signal_ids'] = [];
component[property_name + '_changed'] = function (cb) {
component[property_name + '_signal_ids'].push(
component_settings.connect('changed::' + key.name, cb)
);
};
component[property_name + '_disconnect'] = function () {
component[property_name + '_signal_ids'].forEach(
id => component_settings.disconnect(id)
);
component[property_name + '_signal_ids'] = [];
};
});
});
};
/// Reset the preferences.
reset() {
this.keys.forEach(bundle => {
let component = this;
if (bundle.component !== "general") {
let bundle_component = bundle.component.replaceAll('-', '_');
component = this[bundle_component];
}
bundle.schemas.forEach(key => {
let property_name = this.get_property_name(key.name);
component[property_name + '_reset']();
});
});
this.emit('reset', true);
}
/// From the gschema name, returns the name of the associated property on
/// the Settings object.
get_property_name(name) {
return name.replaceAll('-', '_').toUpperCase();
}
/// Remove all connections managed by the Settings object, i.e. created with
/// `settings.PROPERTY_changed(callback)`.
disconnect_all_settings() {
this.keys.forEach(bundle => {
let component = this;
if (bundle.component !== "general") {
let bundle_component = bundle.component.replaceAll('-', '_');
component = this[bundle_component];
}
bundle.schemas.forEach(key => {
let property_name = this.get_property_name(key.name);
component[property_name + '_disconnect']();
});
});
}
_warn(str) {
console.warn(`[Blur my Shell > settings] ${str}`);
}
};
Signals.addSignalMethods(Settings.prototype);

View File

@ -0,0 +1,40 @@
import { Settings } from './settings.js';
import { KEYS, DEPRECATED_KEYS } from './keys.js';
const CURRENT_SETTINGS_VERSION = 2;
export function update_from_old_settings(gsettings) {
const preferences = new Settings(KEYS, gsettings);
const deprecated_preferences = new Settings(DEPRECATED_KEYS, gsettings);
const old_version = preferences.settings.get_int('settings-version');
if (old_version < CURRENT_SETTINGS_VERSION) {
// set artifacts hacks to be 1 at most, as it should be suitable now that most big bugs have
// been resolved (and especially because hack levels to 2 now means disabling clipped
// redraws entirely, which is very much not what we want for users that update)
if (preferences.HACKS_LEVEL > 1)
preferences.HACKS_LEVEL = 1;
// enable dash-to-dock blurring, as most disabled it due to the lack of rounded corners; set
// it to static blur by default too and with transparent background
preferences.dash_to_dock.BLUR = true;
preferences.dash_to_dock.STATIC_BLUR = true;
preferences.dash_to_dock.STYLE_DASH_TO_DOCK = 0;
// 'customize' has been removed: we merge the current used settings
['appfolder', 'panel', 'dash_to_dock', 'applications', 'window_list'].forEach(
component_name => {
const deprecated_component = deprecated_preferences[component_name];
const new_component = preferences[component_name];
if (!deprecated_component.CUSTOMIZE) {
new_component.SIGMA = deprecated_preferences.SIGMA;
new_component.BRIGHTNESS = deprecated_preferences.BRIGHTNESS;
}
});
// remove old preferences in order not to clutter the gsettings
deprecated_preferences.reset();
}
preferences.settings.set_int('settings-version', CURRENT_SETTINGS_VERSION);
}

View File

@ -0,0 +1,27 @@
import GLib from 'gi://GLib';
export const IS_IN_PREFERENCES = typeof global === 'undefined';
// Taken from https://github.com/Schneegans/Burn-My-Windows/blob/main/src/utils.js
// This method can be used to import a module in the GNOME Shell process only. This
// is useful if you want to use a module in extension.js, but not in the preferences
// process. This method returns null if it is called in the preferences process.
export async function import_in_shell_only(module) {
if (IS_IN_PREFERENCES)
return null;
return (await import(module)).default;
}
export const get_shader_source = (Shell, shader_filename, self_uri) => {
if (!Shell)
return;
const shader_path = GLib.filename_from_uri(
GLib.uri_resolve_relative(self_uri, shader_filename, GLib.UriFlags.NONE)
)[0];
try {
return Shell.get_file_contents_utf8_sync(shader_path);
} catch (e) {
console.warn(`[Blur my Shell > effect] error loading shader from ${shader_path}: ${e}`);
return null;
}
};