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,366 @@
/*
This file is part of CoverflowAltTab.
CoverflowAltTab is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
CoverflowAltTab is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with CoverflowAltTab. If not, see <http://www.gnu.org/licenses/>.
*/
/* CoverflowAltTab::CoverflowSwitcher:
*
* Extends CoverflowAltTab::Switcher, switching tabs using a cover flow.
*/
import Graphene from 'gi://Graphene';
import {Switcher} from './switcher.js';
const BaseSwitcher = Switcher;
import {Preview, Placement, Direction, findUpperLeftFromCenter} from './preview.js'
const SIDE_ANGLE = 90;
const BLEND_OUT_ANGLE = 30;
const ALPHA = 1;
function appendParams(base, extra) {
for (let key in extra) {
base[key] = extra[key];
}
}
export class CoverflowSwitcher extends BaseSwitcher {
constructor(...args) {
super(...args);
}
_createPreviews() {
// TODO: Shouldn't monitor be set once per coverflow state?
let monitor = this._updateActiveMonitor();
let currentWorkspace = this._manager.workspace_manager.get_active_workspace();
this._previewsCenterPosition = {
x: this.actor.width / 2,
y: this.actor.height / 2 + this._settings.offset
};
let ratio = this._settings.preview_to_monitor_ratio;
this._xOffsetLeft = this.actor.width * (0.5 * (1 - ratio) - 0.1 * ratio)
this._xOffsetRight = this.actor.width - this._xOffsetLeft;
for (let windowActor of global.get_window_actors()) {
let metaWin = windowActor.get_meta_window();
let compositor = metaWin.get_compositor_private();
if (compositor) {
let texture = compositor.get_texture();
let width, height;
if (texture.get_size) {
[width, height] = texture.get_size();
} else {
// TODO: Check this OK!
let preferred_size_ok;
[preferred_size_ok, width, height] = texture.get_preferred_size();
}
let scale = 1.0;
let previewScale = this._settings.preview_to_monitor_ratio;
let previewWidth = this.actor.width * previewScale;
let previewHeight = this.actor.height * previewScale;
if (width > previewWidth || height > previewHeight)
scale = Math.min(previewWidth / width, previewHeight / height);
let preview = new Preview(metaWin, this, {
name: metaWin.title,
opacity: ALPHA * (!metaWin.minimized && metaWin.get_workspace() == currentWorkspace || metaWin.is_on_all_workspaces()) ? 255 : 0,
source: texture.get_size ? texture : compositor,
reactive: true,
x: metaWin.minimized ? 0 :
compositor.x - monitor.x,
y: metaWin.minimized ? 0 :
compositor.y - monitor.y,
translation_x: 0,
width: width,
height: height,
scale_x: metaWin.minimized ? 0 : 1,
scale_y: metaWin.minimized ? 0 : 1,
scale_z: metaWin.minimized ? 0 : 1,
rotation_angle_y: 0,
});
preview.scale = scale;
preview.set_pivot_point_placement(Placement.CENTER);
preview.center_position = {
x: findUpperLeftFromCenter(width,
this._previewsCenterPosition.x),
y: findUpperLeftFromCenter(height,
this._previewsCenterPosition.y)
};
if (this._windows.includes(metaWin)) {
this._previews[this._windows.indexOf(metaWin)] = preview;
}
this._allPreviews.push(preview);
this.previewActor.add_child(preview);
}
}
}
_usingCarousel() {
return (this._parent === null && this._settings.switcher_looping_method == "Carousel");
}
_previewNext() {
if (this._currentIndex == this._windows.length - 1) {
this._setCurrentIndex(0);
if (this._usingCarousel()) {
this._updatePreviews(false)
} else {
this._flipStack(Direction.TO_LEFT);
}
} else {
this._setCurrentIndex(this._currentIndex + 1);
this._updatePreviews(false);
}
}
_previewPrevious() {
if (this._currentIndex == 0) {
this._setCurrentIndex(this._windows.length-1);
if (this._usingCarousel()) {
this._updatePreviews(false)
} else {
this._flipStack(Direction.TO_RIGHT);
}
} else {
this._setCurrentIndex(this._currentIndex - 1);
this._updatePreviews(false);
}
}
_flipStack(direction) {
//this._looping = true;
let xOffset, angle;
this._updateActiveMonitor();
if (direction === Direction.TO_LEFT) {
xOffset = -this._xOffsetLeft;
angle = BLEND_OUT_ANGLE;
} else {
xOffset = this._activeMonitor.width + this._xOffsetLeft;
angle = -BLEND_OUT_ANGLE;
}
let animation_time = this._settings.animation_time * 2/3;
for (let [i, preview] of this._previews.entries()) {
this._onFlipIn(preview, i, direction);
}
}
_onFlipIn(preview, index, direction) {
let xOffsetStart, xOffsetEnd, angleStart, angleEnd;
let zeroIndexPreview = null;
this._updateActiveMonitor();
if (direction === Direction.TO_LEFT) {
xOffsetStart = this.actor.width + this._xOffsetLeft;
xOffsetEnd = this._xOffsetRight;
angleStart = -BLEND_OUT_ANGLE;
angleEnd = -SIDE_ANGLE + this._getPerspectiveCorrectionAngle(1);
} else {
xOffsetStart = -this._xOffsetLeft;
xOffsetEnd = this._xOffsetLeft;
angleStart = BLEND_OUT_ANGLE;
angleEnd = SIDE_ANGLE + this._getPerspectiveCorrectionAngle(0);
}
//let animation_time = this._settings.animation_time * 2;
let animation_time = this._settings.animation_time * 2 * (direction === Direction.TO_RIGHT ? ((index + 1) / this._previews.length) : (1 - index / this._previews.length));
this._updatePreview(index, zeroIndexPreview, preview, index, false, animation_time);
let translation_x;
if (direction === Direction.TO_RIGHT) {
translation_x = xOffsetStart - (this._previewsCenterPosition.x
- preview.width / 2) + 50 * (index - this._currentIndex);
} else {
translation_x = xOffsetStart - (this._previewsCenterPosition.x
+ preview.width / 2) + 50 * (index - this._currentIndex);
}
let lastExtraParams = {
transition: 'userChoice',
onCompleteParams: [direction],
onComplete: this._onFlipComplete,
onCompleteScope: this
};
this._manager.platform.tween(preview, {
transition: 'easeInOutQuint',
opacity: ALPHA * 255,
time: animation_time,
});
this._raiseIcons();
return;
}
_onFlipComplete(direction) {
this._looping = false;
this._updatePreviews(false);
}
// TODO: Remove unused direction variable
_animatePreviewToMid(preview, animation_time, extraParams = []) {
let pivot_point = preview.get_pivot_point_placement(Placement.CENTER);
let tweenParams = {
x: findUpperLeftFromCenter(preview.width, this._previewsCenterPosition.x),
y: findUpperLeftFromCenter(preview.height, this._previewsCenterPosition.y),
scale_x: preview.scale,
scale_y: preview.scale,
scale_z: preview.scale,
pivot_point: pivot_point,
translation_x: 0,
rotation_angle_y: 0,
time: animation_time,
transition: 'userChoice',
};
appendParams(tweenParams, extraParams);
this._manager.platform.tween(preview, tweenParams);
}
_animatePreviewToSide(preview, index, xOffset, extraParams, toChangePivotPoint = true) {
let [x, y] = preview.get_pivot_point();
let pivot_point = new Graphene.Point({ x: x, y: y });
let half_length = Math.floor(this._previews.length / 2);
let pivot_index = (this._usingCarousel()) ?
half_length : this._currentIndex;
if (toChangePivotPoint) {
if (index < pivot_index) {
let progress = pivot_index - index < 1 ? pivot_index - index : 1;
pivot_point = new Graphene.Point({ x: 0.5 - 0.5 * progress, y: 0.5});
} else {
let progress = index - pivot_index < 1 ? index - pivot_index : 1;
pivot_point = new Graphene.Point({ x: 0.5 + 0.5 * progress, y: 0.5});
}
}
let scale = Math.pow(this._settings.preview_scaling_factor, Math.abs(index - pivot_index));
scale = scale * preview.scale;
let tweenParams = {
x: findUpperLeftFromCenter(preview.width, this._previewsCenterPosition.x),
y: findUpperLeftFromCenter(preview.height, this._previewsCenterPosition.y),
scale_x: scale,
scale_y: scale,
scale_z: scale,
pivot_point: pivot_point,
};
if (index < pivot_index) {
tweenParams.translation_x = xOffset - (this._previewsCenterPosition.x
- preview.width / 2) + 50 * (index - pivot_index);
} else {
tweenParams.translation_x = xOffset - (this._previewsCenterPosition.x
+ preview.width / 2) + 50 * (index - pivot_index);
}
appendParams(tweenParams, extraParams);
this._manager.platform.tween(preview, tweenParams);
}
_getPerspectiveCorrectionAngle(side) {
if (this._settings.perspective_correction_method != "Adjust Angles") return 0;
if (this.num_monitors == 1) {
return 0;
} else if (this.num_monitors == 2) {
if (this.monitor_number == this.monitors_ltr[0].index) {
if (side == 0) return 508/1000 * 90;
else return 508/1000 *90;
} else {
if (side == 0) return -508/1000 * 90;
else return -508/1000 * 90;
}
} else if (this.num_monitors == 3) {
if (this.monitor_number == this.monitors_ltr[0].index) {
if (side == 0) return (666)/1000 * 90;
else return 750/1000 * 90;
} else if (this.monitor_number == this.monitors_ltr[1].index) {
return 0;
} else {
if (side == 0) return (-750)/1000 * 90;
else return -666/1000 * 90;
}
}
}
_updatePreviews(reorder_only=false) {
if (this._previews == null) return;
let half_length = Math.floor(this._previews.length / 2);
let previews = [];
for (let [i, preview] of this._previews.entries()) {
let idx = (this._usingCarousel()) ?
(i - this._currentIndex + half_length + this._previews.length) % this._previews.length :
i;
previews.push([i, idx, preview]);
}
previews.sort((a, b) => a[1] - b[1]);
let zeroIndexPreview = null;
for (let item of previews) {
let preview = item[2];
let i = item[0];
let idx = item[1];
let animation_time = this._settings.animation_time * (this._settings.randomize_animation_times ? this._getRandomArbitrary(0.0001, 1) : 1);
zeroIndexPreview = this._updatePreview(idx, zeroIndexPreview, preview, i, reorder_only, animation_time);
this._manager.platform.tween(preview, {
opacity: ALPHA * 255,
time: this._settings.animation_time,
transition: 'easeInOutQuint',
onComplete: () => {
preview.set_reactive(true);
}
});
}
if (zeroIndexPreview != null) zeroIndexPreview.make_bottom_layer(this.previewActor);
this._raiseIcons();
}
_updatePreview(idx, zeroIndexPreview, preview, i, reorder_only, animation_time) {
let half_length = Math.floor(this._previews.length / 2);
let pivot_index = (this._usingCarousel()) ?
half_length : this._currentIndex;
if (this._usingCarousel() && idx == 0) {
zeroIndexPreview = preview;
}
if (i == this._currentIndex) {
preview.make_top_layer(this.previewActor);
if (!reorder_only) {
this._animatePreviewToMid(preview, this._settings.animation_time);
}
} else if (idx < pivot_index) {
preview.make_top_layer(this.previewActor);
if (!reorder_only) {
let final_angle = SIDE_ANGLE + this._getPerspectiveCorrectionAngle(0);
let progress = pivot_index - idx < 1 ? pivot_index - idx : 1;
let center_offset = (this._xOffsetLeft + this._xOffsetRight) / 2;
this._animatePreviewToSide(preview, idx, center_offset - preview.width / 2 - progress * (center_offset - preview.width / 2 - this._xOffsetLeft), {
rotation_angle_y: progress * final_angle,
time: this.gestureInProgress ? 0 : animation_time,
transition: 'userChoice',
});
}
} else /* i > this._currentIndex */ {
preview.make_bottom_layer(this.previewActor);
if (!reorder_only) {
let final_angle = -SIDE_ANGLE + this._getPerspectiveCorrectionAngle(1);
let progress = idx - pivot_index < 1 ? idx - pivot_index : 1;
let center_offset = (this._xOffsetLeft + this._xOffsetRight) / 2;
this._animatePreviewToSide(preview, idx, center_offset + preview.width / 2 + progress * (this._xOffsetRight - center_offset - preview.width / 2), {
rotation_angle_y: progress * final_angle,
time: this.gestureInProgress ? 0 : animation_time,
transition: 'userChoice',
});
}
}
return zeroIndexPreview;
}
};

View File

@ -0,0 +1,11 @@
uniform sampler2D tex;
uniform float red;
uniform float green;
uniform float blue;
uniform float blend;
void main() {
vec4 s = texture2D(tex, cogl_tex_coord_in[0].st);
vec4 dst = vec4(red, green, blue, blend);
cogl_color_out = vec4(mix(s.rgb, dst.rgb * s.a, blend), s.a);
}

View File

@ -0,0 +1,176 @@
// This code istaken from the blur-my-shell extension
//'use strict';
import Clutter from 'gi://Clutter';
import Shell from 'gi://Shell';
import GObject from 'gi://GObject';
import GLib from 'gi://GLib';
const SHADER_PATH = GLib.filename_from_uri(GLib.uri_resolve_relative(import.meta.url, 'color_effect.glsl', GLib.UriFlags.NONE))[0];
const get_shader_source = _ => {
try {
return Shell.get_file_contents_utf8_sync(SHADER_PATH);
} catch (e) {
log(`[Coverflow Alt-Tab] error loading shader from ${SHADER_PATH}: ${e}`);
return null;
}
};
/// New Clutter Shader Effect that simply mixes a color in, the class applies
/// the GLSL shader programmed into vfunc_get_static_shader_source and applies
/// it to an Actor.
///
/// Clutter Shader Source Code:
/// https://github.com/GNOME/clutter/blob/master/clutter/clutter-shader-effect.c
///
/// GJS Doc:
/// https://gjs-docs.gnome.org/clutter10~10_api/clutter.shadereffect
export const ColorEffect = new GObject.registerClass({
GTypeName: "CoverflowAltTabColorEffect",
Properties: {
'red': GObject.ParamSpec.double(
`red`,
`Red`,
`Red value in shader`,
GObject.ParamFlags.READWRITE,
0.0, 1.0,
0.4,
),
'green': GObject.ParamSpec.double(
`green`,
`Green`,
`Green value in shader`,
GObject.ParamFlags.READWRITE,
0.0, 1.0,
0.4,
),
'blue': GObject.ParamSpec.double(
`blue`,
`Blue`,
`Blue value in shader`,
GObject.ParamFlags.READWRITE,
0.0, 1.0,
0.4,
),
'blend': GObject.ParamSpec.double(
`blend`,
`Blend`,
`Amount of blending between the colors`,
GObject.ParamFlags.READWRITE,
0.0, 1.0,
0.4,
),
}
}, class CoverflowAltTabColorShader extends Clutter.ShaderEffect {
_init(params) {
this._red = null;
this._green = null;
this._blue = null;
this._blend = null;
// initialize without color as a parameter
let _color = params.color;
delete params.color;
super._init(params);
// set shader source
this._source = get_shader_source();
if (this._source)
this.set_shader_source(this._source);
// set shader color
if (_color)
this.color = _color;
this.update_enabled();
}
get red() {
return this._red;
}
set red(value) {
if (this._red !== value) {
this._red = value;
this.set_uniform_value('red', parseFloat(this._red - 1e-6));
}
}
get green() {
return this._green;
}
set green(value) {
if (this._green !== value) {
this._green = value;
this.set_uniform_value('green', parseFloat(this._green - 1e-6));
}
}
get blue() {
return this._blue;
}
set blue(value) {
if (this._blue !== value) {
this._blue = value;
this.set_uniform_value('blue', parseFloat(this._blue - 1e-6));
}
}
get blend() {
return this._blend;
}
set blend(value) {
if (this._blend !== value) {
this._blend = value;
this.set_uniform_value('blend', parseFloat(this._blend - 1e-6));
}
this.update_enabled();
}
set color(rgba) {
let [r, g, b, a] = rgba;
this.red = r;
this.green = g;
this.blue = b;
this.blend = a;
}
get color() {
return [this.red, this.green, this.blue, this.blend];
}
/// False set function, only cares about the color. Too hard to change.
set(params) {
this.color = params.color;
}
update_enabled() {
this.set_enabled(true);
}
vfunc_paint_target(paint_node = null, paint_context = null) {
this.set_uniform_value("tex", 0);
if (paint_node && paint_context)
super.vfunc_paint_target(paint_node, paint_context);
else if (paint_node)
super.vfunc_paint_target(paint_node);
else
super.vfunc_paint_target();
}
});

View File

@ -0,0 +1,109 @@
//
// Description : Array and textureless GLSL 2D simplex noise function.
// Author : Ian McEwan, Ashima Arts.
// Maintainer : stegu
// Lastmod : 20110822 (ijm)
// License : Copyright (C) 2011 Ashima Arts. All rights reserved.
// Distributed under the MIT License. See LICENSE file.
// https://github.com/ashima/webgl-noise
// https://github.com/stegu/webgl-noise
//
uniform sampler2D tex;
uniform float time;
vec3 mod289(vec3 x) {
return x - floor(x * (1.0 / 289.0)) * 289.0;
}
vec2 mod289(vec2 x) {
return x - floor(x * (1.0 / 289.0)) * 289.0;
}
vec3 permute(vec3 x) {
return mod289(((x*34.0)+1.0)*x);
}
float snoise(vec2 v) {
const vec4 C = vec4(0.211324865405187, // (3.0-sqrt(3.0))/6.0
0.366025403784439, // 0.5*(sqrt(3.0)-1.0)
-0.577350269189626, // -1.0 + 2.0 * C.x
0.024390243902439); // 1.0 / 41.0
// First corner
vec2 i = floor(v + dot(v, C.yy) );
vec2 x0 = v - i + dot(i, C.xx);
// Other corners
vec2 i1;
//i1.x = step( x0.y, x0.x ); // x0.x > x0.y ? 1.0 : 0.0
//i1.y = 1.0 - i1.x;
i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0);
// x0 = x0 - 0.0 + 0.0 * C.xx ;
// x1 = x0 - i1 + 1.0 * C.xx ;
// x2 = x0 - 1.0 + 2.0 * C.xx ;
vec4 x12 = x0.xyxy + C.xxzz;
x12.xy -= i1;
// Permutations
i = mod289(i); // Avoid truncation effects in permutation
vec3 p = permute( permute( i.y + vec3(0.0, i1.y, 1.0 ))
+ i.x + vec3(0.0, i1.x, 1.0 ));
vec3 m = max(0.5 - vec3(dot(x0,x0), dot(x12.xy,x12.xy), dot(x12.zw,x12.zw)), 0.0);
m = m*m ;
m = m*m ;
// Gradients: 41 points uniformly over a line, mapped onto a diamond.
// The ring size 17*17 = 289 is close to a multiple of 41 (41*7 = 287)
vec3 x = 2.0 * fract(p * C.www) - 1.0;
vec3 h = abs(x) - 0.5;
vec3 ox = floor(x + 0.5);
vec3 a0 = x - ox;
// Normalise gradients implicitly by scaling m
// Approximation of: m *= inversesqrt( a0*a0 + h*h );
m *= 1.79284291400159 - 0.85373472095314 * ( a0*a0 + h*h );
// Compute final noise value at P
vec3 g;
g.x = a0.x * x0.x + h.x * x0.y;
g.yz = a0.yz * x12.xz + h.yz * x12.yw;
return 130.0 * dot(m, g);
}
float rand(vec2 co) {
return fract(sin(dot(co.xy,vec2(12.9898,78.233))) * 43758.5453);
}
void main() {
vec2 uv = cogl_tex_coord_in[0].st;
float t = time * 2.0;
// Create large, incidental noise waves
float noise = max(0.0, snoise(vec2(t, uv.y * 0.3)) - 0.3) * (1.0 / 0.7);
// Offset by smaller, constant noise waves
noise = noise + (snoise(vec2(t*10.0, uv.y * 2.4)) - 0.5) * 0.15;
// Apply the noise as x displacement for every line
float xpos = uv.x - noise * noise * 0.25;
if (xpos < 0.0)
xpos = -xpos;
if (xpos > 1.0)
xpos = 1.0 - xpos;
cogl_color_out = texture2D(tex, vec2(xpos, uv.y));
// Mix in some random interference for lines
cogl_color_out.rgb = mix(cogl_color_out.rgb, vec3(rand(vec2(uv.y * t))), noise * 0.3).rgb;
// Apply a line pattern every 4 pixels
if (floor(mod(gl_FragCoord.y * 0.25, 2.0)) == 0.0)
{
cogl_color_out.rgb *= 1.0 - (0.15 * noise);
}
// Shift green/blue channels (using the red channel)
cogl_color_out.g = mix(cogl_color_out.r, texture2D(tex, vec2(xpos + noise * 0.05, uv.y)).g, 0.25);
cogl_color_out.b = mix(cogl_color_out.r, texture2D(tex, vec2(xpos - noise * 0.05, uv.y)).b, 0.25);
}

View File

@ -0,0 +1,59 @@
// This code istaken from the blur-my-shell extension
//'use strict';
import Clutter from 'gi://Clutter';
import Shell from 'gi://Shell';
import GObject from 'gi://GObject';
import GLib from 'gi://GLib';
const SHADER_PATH = GLib.filename_from_uri(GLib.uri_resolve_relative(import.meta.url, 'glitch_effect.glsl', GLib.UriFlags.NONE))[0];
const get_shader_source = _ => {
try {
return Shell.get_file_contents_utf8_sync(SHADER_PATH);
} catch (e) {
log(`[Coverflow Alt-Tab] error loading shader from ${SHADER_PATH}: ${e}`);
return null;
}
};
/// New Clutter Shader Effect that simply mixes a color in, the class applies
/// the GLSL shader programmed into vfunc_get_static_shader_source and applies
/// it to an Actor.
///
/// Clutter Shader Source Code:
/// https://github.com/GNOME/clutter/blob/master/clutter/clutter-shader-effect.c
///
/// GJS Doc:
/// https://gjs-docs.gnome.org/clutter10~10_api/clutter.shadereffect
export const GlitchEffect = new GObject.registerClass({
GTypeName: "CoverflowAltTabGlitchEffect",
}, class CoverflowAltTabGlitchShader extends Clutter.ShaderEffect {
_init(params) {
super._init(params);
this._timeOffset = Math.random() * 1000000;
// set shader source
this._source = get_shader_source();
if (this._source)
this.set_shader_source(this._source);
this.set_enabled(true);
}
vfunc_paint_target(paint_node = null, paint_context = null) {
const time = this._timeOffset + GLib.get_monotonic_time() / GLib.USEC_PER_SEC;
this.set_uniform_value("time", time);
this.set_uniform_value("tex", 0);
if (paint_node && paint_context)
super.vfunc_paint_target(paint_node, paint_context);
else if (paint_node)
super.vfunc_paint_target(paint_node);
else
super.vfunc_paint_target();
this.queue_repaint();
}
});

View File

@ -0,0 +1,67 @@
/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */
/*
This file is part of CoverflowAltTab.
CoverflowAltTab is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
CoverflowAltTab is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with CoverflowAltTab. If not, see <http://www.gnu.org/licenses/>.
*/
/*
* Gnome Shell extension specific routines.
*
* Create the correct manager and enable/disable it.
*/
import * as Manager from './manager.js';
import * as Platform from './platform.js';
import * as Keybinder from './keybinder.js';
import {Extension} from 'resource:///org/gnome/shell/extensions/extension.js';
let manager = null;
export default class CoverflowAltTabExtension extends Extension {
constructor(metadata) {
super(metadata);
}
enable() {
if (!manager) {
/*
* As there are restricted Gnome versions the current extension support (that
* are specified in metadata.json file), only the API related to those supported
* versions must be used, not anything else. As a result, performing checks for
* keeping backward-compatiblity with old unsupported versions is a wrong
* decision.
*
* To support older versions of Gnome, first, add the version to the metadata
* file, then, if needed, include backward-compatible API here for each
* version.
*/
manager = new Manager.Manager(
new Platform.PlatformGnomeShell(this.getSettings()),
new Keybinder.Keybinder330Api()
);
}
manager.enable();
}
disable() {
if (manager) {
manager.disable();
manager = null;
}
}
}

View File

@ -0,0 +1,84 @@
/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */
/*
This file is part of CoverflowAltTab.
CoverflowAltTab is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
CoverflowAltTab is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with CoverflowAltTab. If not, see <http://www.gnu.org/licenses/>.
*/
/* CoverflowAltTab::Keybinder
*
* Originally, created to be helper classes to handle the different keybinding APIs.
*/
import Shell from 'gi://Shell';
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
import {__ABSTRACT_METHOD__} from './lib.js'
class AbstractKeybinder {
enable() { __ABSTRACT_METHOD__(this, this.enable) }
disable() { __ABSTRACT_METHOD__(this, this.disable) }
}
export const Keybinder330Api = class Keybinder330Api extends AbstractKeybinder {
constructor(...args) {
super(...args);
this._startAppSwitcherBind = null;
}
enable(startAppSwitcherBind, platform) {
let mode = Shell.ActionMode ? Shell.ActionMode : Shell.KeyBindingMode;
this._startAppSwitcherBind = startAppSwitcherBind;
platform.addSettingsChangedCallback(this._onSettingsChanged.bind(this));
Main.wm.setCustomKeybindingHandler('switch-group', mode.NORMAL, startAppSwitcherBind);
Main.wm.setCustomKeybindingHandler('switch-group-backward', mode.NORMAL, startAppSwitcherBind);
}
disable() {
let mode = Shell.ActionMode ? Shell.ActionMode : Shell.KeyBindingMode;
Main.wm.setCustomKeybindingHandler('switch-applications', mode.NORMAL, Main.wm._startSwitcher.bind(Main.wm));
Main.wm.setCustomKeybindingHandler('switch-windows', mode.NORMAL, Main.wm._startSwitcher.bind(Main.wm));
Main.wm.setCustomKeybindingHandler('switch-group', mode.NORMAL, Main.wm._startSwitcher.bind(Main.wm));
Main.wm.setCustomKeybindingHandler('switch-applications-backward', mode.NORMAL, Main.wm._startSwitcher.bind(Main.wm));
Main.wm.setCustomKeybindingHandler('switch-windows-backward', mode.NORMAL, Main.wm._startSwitcher.bind(Main.wm));
Main.wm.setCustomKeybindingHandler('switch-group-backward', mode.NORMAL, Main.wm._startSwitcher.bind(Main.wm));
}
_onSettingsChanged(settings, key=null) {
let mode = Shell.ActionMode ? Shell.ActionMode : Shell.KeyBindingMode;
if (key == null || key == 'bind-to-switch-applications') {
if (settings.get_boolean('bind-to-switch-applications')) {
Main.wm.setCustomKeybindingHandler('switch-applications', mode.NORMAL, this._startAppSwitcherBind);
Main.wm.setCustomKeybindingHandler('switch-applications-backward', mode.NORMAL, this._startAppSwitcherBind);
} else {
Main.wm.setCustomKeybindingHandler('switch-applications', mode.NORMAL, Main.wm._startSwitcher.bind(Main.wm));
Main.wm.setCustomKeybindingHandler('switch-applications-backward', mode.NORMAL, Main.wm._startSwitcher.bind(Main.wm));
}
}
if (key == null || key == 'bind-to-switch-windows') {
if (settings.get_boolean('bind-to-switch-windows')) {
Main.wm.setCustomKeybindingHandler('switch-windows', mode.NORMAL, this._startAppSwitcherBind);
Main.wm.setCustomKeybindingHandler('switch-windows-backward', mode.NORMAL, this._startAppSwitcherBind);
} else {
Main.wm.setCustomKeybindingHandler('switch-windows', mode.NORMAL, Main.wm._startSwitcher.bind(Main.wm));
Main.wm.setCustomKeybindingHandler('switch-windows-backward', mode.NORMAL, Main.wm._startSwitcher.bind(Main.wm));
}
}
}
}

View File

@ -0,0 +1,62 @@
/* -*- mode: js; js-basic-offset: 4; indent-tabs-mode: nil -*- */
/*
Copyright (c) 2011-2012, Giovanni Campagna <scampa.giovanni@gmail.com>
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of the GNOME nor the
names of its contributors may be used to endorse or promote products
derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
// This method can be used to write a message to GNOME Shell's log. This is enhances
// the standard log() functionality by prepending the extension's name and the location
// where the message was logged. As the extensions name is part of the location, you
// can more effectively watch the log output of GNOME Shell:
// journalctl -f -o cat | grep -E 'desktop-cube|'
// This method is based on a similar script from the Fly-Pie GNOME Shell extension which
// os published under the MIT License (https://github.com/Schneegans/Fly-Pie).
export function debug(message) {
const stack = new Error().stack.split('\n');
// Remove debug() function call from stack.
stack.shift();
// Find the index of the extension directory (e.g. desktopcube@schneegans.github.com)
// in the stack entry. We do not want to print the entire absolute file path.
const extensionRoot = stack[0].indexOf('CoverflowAltTab@palatis.blogspot.com');
log('[' + stack[0].slice(extensionRoot) + '] ' + message);
log(new Error().stack);
}
/**
* Make a method psuedo-abstract.
*
* @param {Object} object The current class instance, i.e. this.
* @param {Object} method The method object, e.g. this.enable.
* @return {void}
*/
export function __ABSTRACT_METHOD__(object, method) {
throw new Error(
"Abstract method " +
object.constructor.name + "." + method.name + "()" +
" not implemented"
);
}

View File

@ -0,0 +1,148 @@
/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */
/*
This file is part of CoverflowAltTab.
CoverflowAltTab is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
CoverflowAltTab is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with CoverflowAltTab. If not, see <http://www.gnu.org/licenses/>.
*/
/* CoverflowAltTab::Manager
*
* This class is a helper class to start the actual switcher.
*/
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
function sortWindowsByUserTime(win1, win2) {
let t1 = win1.get_user_time();
let t2 = win2.get_user_time();
return (t2 > t1) ? 1 : -1;
}
function matchSkipTaskbar(win) {
return !win.is_skip_taskbar();
}
function matchWmClass(win) {
return win.get_wm_class() == this && !win.is_skip_taskbar();
}
function matchWorkspace(win) {
return win.get_workspace() == this && !win.is_skip_taskbar();
}
function matchOtherWorkspace(win) {
return win.get_workspace() != this && !win.is_skip_taskbar();
}
export const Manager = class Manager {
constructor(platform, keybinder) {
this.platform = platform;
this.keybinder = keybinder;
this.switcher = null;
if (global.workspace_manager && global.workspace_manager.get_active_workspace)
this.workspace_manager = global.workspace_manager;
else
this.workspace_manager = global.screen;
if (global.display && global.display.get_n_monitors)
this.display = global.display;
else
this.display = global.screen;
}
enable() {
this.platform.enable();
this.keybinder.enable(this._startWindowSwitcher.bind(this), this.platform);
}
disable() {
if (this.switcher != null)
this.switcher.destroy();
this.platform.disable();
this.keybinder.disable();
}
activateSelectedWindow(win) {
Main.activateWindow(win, global.get_current_time());
}
removeSelectedWindow(win) {
win.delete(global.get_current_time());
}
_startWindowSwitcher(display, window, binding) {
let windows = [];
let currentWorkspace = this.workspace_manager.get_active_workspace();
let isApplicationSwitcher = false;
// Construct a list with all windows
let windowActors = global.get_window_actors();
for (let windowActor of windowActors) {
if (typeof windowActor.get_meta_window === "function") {
windows.push(windowActor.get_meta_window());
}
}
windowActors = null;
switch (binding.get_name()) {
case 'switch-group':
// Switch between windows of same application from all workspaces
let focused = display.focus_window ? display.focus_window : windows[0];
windows = windows.filter(matchWmClass, focused.get_wm_class());
windows.sort(sortWindowsByUserTime);
break;
case 'switch-applications':
case 'switch-applications-backward':
isApplicationSwitcher = !this.platform.getSettings().switch_application_behaves_like_switch_windows
default:
let currentOnly = this.platform.getSettings().current_workspace_only;
if (currentOnly === 'all-currentfirst') {
// Switch between windows of all workspaces, prefer
// those from current workspace
let wins1 = windows.filter(matchWorkspace, currentWorkspace);
let wins2 = windows.filter(matchOtherWorkspace, currentWorkspace);
// Sort by user time
wins1.sort(sortWindowsByUserTime);
wins2.sort(sortWindowsByUserTime);
windows = wins1.concat(wins2);
wins1 = [];
wins2 = [];
} else {
let filter = currentOnly === 'current' ? matchWorkspace :
matchSkipTaskbar;
// Switch between windows of current workspace
windows = windows.filter(filter, currentWorkspace);
windows.sort(sortWindowsByUserTime);
}
break;
}
// filter by windows existing on the active monitor
if(this.platform.getSettings().switch_per_monitor)
{
windows = windows.filter ( (win) =>
win.get_monitor() == Main.layoutManager.currentMonitor.index );
}
if (windows.length) {
let mask = binding.get_mask();
let currentIndex = windows.indexOf(display.focus_window);
let switcher_class = this.platform.getSettings().switcher_class;
this.switcher = new switcher_class(windows, mask, currentIndex, this, null, isApplicationSwitcher, null);
}
}
}

View File

@ -0,0 +1,18 @@
{
"_generated": "Generated by SweetTooth, do not edit",
"description": "Replacement of Alt-Tab, iterates through windows in a cover-flow manner.",
"donations": {
"github": "dsheeler",
"liberapay": "dsheeler",
"paypal": "DanielSheeler"
},
"gettext-domain": "CoverflowAltTab@palatis.blogspot.com",
"name": "Coverflow Alt-Tab",
"settings-schema": "org.gnome.shell.extensions.coverflowalttab",
"shell-version": [
"46"
],
"url": "https://github.com/dmo60/CoverflowAltTab",
"uuid": "CoverflowAltTab@palatis.blogspot.com",
"version": 72
}

View File

@ -0,0 +1,685 @@
/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */
/*
This file is part of CoverflowAltTab.
CoverflowAltTab is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
CoverflowAltTab is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with CoverflowAltTab. If not, see <http://www.gnu.org/licenses/>.
*/
/*
* CoverflowAltTab::Platform
*
* Originally, created to be helper classes to handle Gnome Shell and Cinnamon differences.
*/
import St from 'gi://St';
import GObject from 'gi://GObject';
import Gio from 'gi://Gio';
import Meta from 'gi://Meta';
import Clutter from 'gi://Clutter';
import Shell from 'gi://Shell';
import GLib from 'gi://GLib';
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
import * as Background from 'resource:///org/gnome/shell/ui/background.js';
import {__ABSTRACT_METHOD__} from './lib.js'
import {Switcher} from './switcher.js';
import {CoverflowSwitcher} from './coverflowSwitcher.js';
import {TimelineSwitcher} from './timelineSwitcher.js';
const POSITION_TOP = 1;
const POSITION_BOTTOM = 7;
const DESKTOP_INTERFACE_SCHEMA = 'org.gnome.desktop.interface';
const KEY_TEXT_SCALING_FACTOR = 'text-scaling-factor';
const TRANSITION_TYPE = 'easeOutQuad';
const modes = [
Clutter.AnimationMode.EASE_IN_BOUNCE,
Clutter.AnimationMode.EASE_OUT_BOUNCE,
Clutter.AnimationMode.EASE_IN_OUT_BOUNCE,
Clutter.AnimationMode.EASE_IN_BACK,
Clutter.AnimationMode.EASE_OUT_BACK,
Clutter.AnimationMode.EASE_IN_OUT_BACK,
Clutter.AnimationMode.EASE_IN_ELASTIC,
Clutter.AnimationMode.EASE_OUT_ELASTIC,
Clutter.AnimationMode.EASE_IN_OUT_ELASTIC,
Clutter.AnimationMode.EASE_IN_QUAD,
Clutter.AnimationMode.EASE_OUT_QUAD,
Clutter.AnimationMode.EASE_IN_OUT_QUAD,
Clutter.AnimationMode.EASE_IN_CUBIC,
Clutter.AnimationMode.EASE_OUT_CUBIC,
Clutter.AnimationMode.EASE_IN_OUT_CUBIC,
Clutter.AnimationMode.EASE_IN_QUART,
Clutter.AnimationMode.EASE_OUT_QUART,
Clutter.AnimationMode.EASE_IN_OUT_QUART,
Clutter.AnimationMode.EASE_IN_QUINT,
Clutter.AnimationMode.EASE_OUT_QUINT,
Clutter.AnimationMode.EASE_IN_OUT_QUINT,
Clutter.AnimationMode.EASE_IN_SINE,
Clutter.AnimationMode.EASE_OUT_SINE,
Clutter.AnimationMode.EASE_IN_OUT_SINE,
Clutter.AnimationMode.EASE_IN_EXPO,
Clutter.AnimationMode.EASE_OUT_EXPO,
Clutter.AnimationMode.EASE_IN_OUT_EXPO,
Clutter.AnimationMode.EASE_IN_CIRC,
Clutter.AnimationMode.EASE_OUT_CIRC,
Clutter.AnimationMode.EASE_IN_OUT_CIRC,
Clutter.AnimationMode.LINEAR
];
function clamp(value, min, max) {
return Math.max(min, Math.min(max, value));
}
class AbstractPlatform {
enable() { __ABSTRACT_METHOD__(this, this.enable) }
disable() { __ABSTRACT_METHOD__(this, this.disable) }
getWidgetClass() { __ABSTRACT_METHOD__(this, this.getWidgetClass) }
getWindowTracker() { __ABSTRACT_METHOD__(this, this.getWindowTracker) }
getPrimaryModifier(mask) { __ABSTRACT_METHOD__(this, this.getPrimaryModifier) }
getSettings() { __ABSTRACT_METHOD__(this, this.getSettings) }
tween(actor, params) { __ABSTRACT_METHOD__(this, this.tween) }
removeTweens(actor) { __ABSTRACT_METHOD__(this, this.removeTweens) }
getDefaultSettings() {
return {
animation_time: 0.2,
randomize_animation_times: false,
dim_factor: 0,
title_position: POSITION_BOTTOM,
icon_style: 'Classic',
icon_has_shadow: false,
overlay_icon_opacity: 1,
text_scaling_factor: 1,
offset: 0,
hide_panel: true,
enforce_primary_monitor: true,
switcher_class: Switcher,
easing_function: 'ease-out-cubic',
current_workspace_only: '1',
switch_per_monitor: false,
preview_to_monitor_ratio: 0.5,
preview_scaling_factor: 0.75,
bind_to_switch_applications: true,
bind_to_switch_windows: true,
perspective_correction_method: "Move Camera",
highlight_mouse_over: false,
raise_mouse_over: true,
switcher_looping_method: 'Flip Stack',
switch_application_behaves_like_switch_windows: false,
blur_radius: 0,
desaturate_factor: 0.0,
tint_color: (0., 0., 0.),
switcher_background_color: (0., 0., 0.),
tint_blend: 0.0,
use_glitch_effect: false,
use_tint: false,
invert_swipes: false,
overlay_icon_size: 128,
};
}
initBackground() {
this._background = Meta.BackgroundActor.new_for_screen(global.screen);
this._background.hide();
global.overlay_group.add_child(this._background);
}
dimBackground() {
this._background.show();
this.tween(this._background, {
dim_factor: this._settings.dim_factor,
time: this._settings.animation_time,
transition: TRANSITION_TYPE
});
}
removeBackground() {
global.overlay_group.remove_child(this._background);
}
}
export class PlatformGnomeShell extends AbstractPlatform {
constructor(settings, ...args) {
super(...args);
this._settings = null;
this._connections = null;
this._extensionSettings = settings;
this._desktopSettings = null;
this._backgroundColor = null;
this._settings_changed_callbacks = null;
this._themeContext = null;
}
_getSwitcherBackgroundColor() {
if (this._backgroundColor === null) {
let widgetClass = this.getWidgetClass();
let parent = new widgetClass({ visible: false, reactive: false, style_class: 'switcher-list'});
let actor = new widgetClass({ visible: false, reactive: false, style_class: 'item-box' });
parent.add_child(actor);
actor.add_style_pseudo_class('selected');
Main.uiGroup.add_child(parent);
this._backgroundColor = actor.get_theme_node().get_background_color();
Main.uiGroup.remove_child(parent);
parent = null;
let color = new GLib.Variant("(ddd)", [this._backgroundColor.red/255, this._backgroundColor.green/255, this._backgroundColor.blue/255]);
this._extensionSettings.set_value("switcher-background-color", color);
}
return this._backgroundColor;
}
enable() {
this._themeContext = St.ThemeContext.get_for_stage(global.stage);
this._themeContextChangedID = this._themeContext.connect("changed", (themeContext) => {
this._backgroundColor = null;
this._getSwitcherBackgroundColor();
});
this._settings_changed_callbacks = [];
if (this._desktopSettings == null)
this._desktopSettings = new Gio.Settings({ schema_id: DESKTOP_INTERFACE_SCHEMA });
let keys = [
"animation-time",
"randomize-animation-times",
"dim-factor",
"position",
"icon-style",
"icon-has-shadow",
"overlay-icon-size",
"overlay-icon-opacity",
"offset",
"hide-panel",
"enforce-primary-monitor",
"easing-function",
"current-workspace-only",
"switch-per-monitor",
"switcher-style",
"preview-to-monitor-ratio",
"preview-scaling-factor",
"bind-to-switch-applications",
"bind-to-switch-windows",
"perspective-correction-method",
"highlight-mouse-over",
"raise-mouse-over",
"desaturate-factor",
"blur-radius",
"switcher-looping-method",
"switch-application-behaves-like-switch-windows",
"use-tint",
"tint-color",
"tint-blend",
"switcher-background-color",
"use-glitch-effect",
"invert-swipes",
];
let dkeys = [
KEY_TEXT_SCALING_FACTOR,
];
this._connections = [];
for (let key of keys) {
let bind = this._onSettingsChanged.bind(this, key);
this._connections.push(this._extensionSettings.connect('changed::' + key, bind));
}
this._dconnections = [];
for (let dkey of dkeys) {
let bind = this._onSettingsChanged.bind(this, dkey);
this._dconnections.push(this._desktopSettings.connect('changed::' + dkey, bind));
}
this._settings = this._loadSettings();
}
disable() {
this.showPanels(0);
if (this._connections) {
for (let connection of this._connections) {
this._extensionSettings.disconnect(connection);
}
this._connections = null;
}
if (this._dconnections) {
for (let dconnection of this._dconnections) {
this._desktopSettings.disconnect(dconnection);
}
}
this._themeContext.disconnect(this._themeContextChangedID);
this._themeContext = null;
this._settings = null;
}
getWidgetClass() {
return St.Widget;
}
getWindowTracker() {
return Shell.WindowTracker.get_default();
}
getPrimaryModifier(mask) {
if (mask === 0)
return 0;
let primary = 1;
while (mask > 1) {
mask >>= 1;
primary <<= 1;
}
return primary;
}
getSettings() {
if (!this._settings) {
this._settings = this._loadSettings();
}
return this._settings;
}
addSettingsChangedCallback(cb) {
cb(this._extensionSettings);
this._settings_changed_callbacks.push(cb);
}
_onSettingsChanged(key) {
this._settings = null;
for (let cb of this._settings_changed_callbacks) {
cb(this._extensionSettings, key);
}
}
_loadSettings() {
try {
let settings = this._extensionSettings;
let dsettings = this._desktopSettings;
return {
animation_time: settings.get_double("animation-time"),
randomize_animation_times: settings.get_boolean("randomize-animation-times"),
dim_factor: clamp(settings.get_double("dim-factor"), 0, 1),
title_position: (settings.get_string("position") == 'Top' ? POSITION_TOP : POSITION_BOTTOM),
icon_style: (settings.get_string("icon-style")),
icon_has_shadow: settings.get_boolean("icon-has-shadow"),
overlay_icon_size: clamp(settings.get_double("overlay-icon-size"), 16, 1024),
overlay_icon_opacity: clamp(settings.get_double("overlay-icon-opacity"), 0, 1),
text_scaling_factor: dsettings.get_double(KEY_TEXT_SCALING_FACTOR),
offset: settings.get_int("offset"),
hide_panel: settings.get_boolean("hide-panel"),
enforce_primary_monitor: settings.get_boolean("enforce-primary-monitor"),
easing_function: settings.get_string("easing-function"),
switcher_class: settings.get_string("switcher-style") === 'Timeline'
? TimelineSwitcher : CoverflowSwitcher,
current_workspace_only: settings.get_string("current-workspace-only"),
switch_per_monitor: settings.get_boolean("switch-per-monitor"),
preview_to_monitor_ratio: clamp(settings.get_double("preview-to-monitor-ratio"), 0, 1),
preview_scaling_factor: clamp(settings.get_double("preview-scaling-factor"), 0, 1),
bind_to_switch_applications: settings.get_boolean("bind-to-switch-applications"),
bind_to_switch_windows: settings.get_boolean("bind-to-switch-windows"),
perspective_correction_method: settings.get_string("perspective-correction-method"),
highlight_mouse_over: settings.get_boolean("highlight-mouse-over"),
raise_mouse_over: settings.get_boolean("raise-mouse-over"),
desaturate_factor: settings.get_double("desaturate-factor") === 1.0 ? 0.999 : settings.get_double("desaturate-factor"),
blur_radius: settings.get_int("blur-radius"),
switcher_looping_method: settings.get_string("switcher-looping-method"),
switch_application_behaves_like_switch_windows: settings.get_boolean("switch-application-behaves-like-switch-windows"),
tint_color: settings.get_value("tint-color").deep_unpack(),
tint_blend: settings.get_double("tint-blend"),
switcher_background_color: settings.get_value("switcher-background-color").deep_unpack(),
use_glitch_effect: settings.get_boolean("use-glitch-effect"),
use_tint: settings.get_boolean("use-tint"),
invert_swipes: settings.get_boolean("invert-swipes"),
};
} catch (e) {
global.log(e);
}
return this.getDefaultSettings();
}
tween(actor, params) {
params.duration = params.time * 1000;
if (params.transition == 'userChoice' && this.getSettings().easing_function == 'random' ||
params.transition == 'Random') {
params.mode = modes[Math.floor(Math.random()*modes.length)];
} else if (params.transition == 'userChoice' && this.getSettings().easing_function == "ease-in-bounce" ||
params.transition == 'easeInBounce') {
params.mode = Clutter.AnimationMode.EASE_IN_BOUNCE;
} else if (params.transition == 'userChoice' && this.getSettings().easing_function == "ease-out-bounce" ||
params.transition == 'easeOutBounce') {
params.mode = Clutter.AnimationMode.EASE_OUT_BOUNCE;
} else if (params.transition == 'userChoice' && this.getSettings().easing_function == "ease-in-out-bounce" ||
params.transition == 'easeInOutBounce') {
params.mode = Clutter.AnimationMode.EASE_IN_OUT_BOUNCE;
} else if (params.transition == 'userChoice' && this.getSettings().easing_function == "ease-in-back" ||
params.transition == 'easeInBack') {
params.mode = Clutter.AnimationMode.EASE_IN_BACK;
} else if (params.transition == 'userChoice' && this.getSettings().easing_function == "ease-out-back" ||
params.transition == 'easeOutBack') {
params.mode = Clutter.AnimationMode.EASE_OUT_BACK;
} else if (params.transition == 'userChoice' && this.getSettings().easing_function == "ease-in-out-back" ||
params.transition == 'easeInOutBack') {
params.mode = Clutter.AnimationMode.EASE_IN_OUT_BACK;
} else if (params.transition == 'userChoice' && this.getSettings().easing_function == "ease-in-elastic" ||
params.transition == 'easeInElastic') {
params.mode = Clutter.AnimationMode.EASE_IN_ELASTIC;
} else if (params.transition == 'userChoice' && this.getSettings().easing_function == "ease-out-elastic" ||
params.transition == 'easeOutElastic') {
params.mode = Clutter.AnimationMode.EASE_OUT_ELASTIC;
} else if (params.transition == 'userChoice' && this.getSettings().easing_function == "ease-in-out-elastic" ||
params.transition == 'easeInOutElastic') {
params.mode = Clutter.AnimationMode.EASE_IN_OUT_ELASTIC;
} else if (params.transition == 'userChoice' && this.getSettings().easing_function == "ease-in-quad" ||
params.transition == 'easeInQuad') {
params.mode = Clutter.AnimationMode.EASE_IN_QUAD;
} else if (params.transition == 'userChoice' && this.getSettings().easing_function == "ease-out-quad" ||
params.transition == 'easeOutQuad') {
params.mode = Clutter.AnimationMode.EASE_OUT_QUAD;
} else if (params.transition == 'userChoice' && this.getSettings().easing_function == "ease-in-out-quad" ||
params.transition == 'easeInOutQuad') {
params.mode = Clutter.AnimationMode.EASE_IN_OUT_QUAD;
} else if (params.transition == 'userChoice' && this.getSettings().easing_function == "ease-in-cubic" ||
params.transition == 'easeInCubic') {
params.mode = Clutter.AnimationMode.EASE_IN_CUBIC;
} else if (params.transition == 'userChoice' && this.getSettings().easing_function == "ease-out-cubic" ||
params.transition == 'easeOutCubic') {
params.mode = Clutter.AnimationMode.EASE_OUT_CUBIC;
} else if (params.transition == 'userChoice' && this.getSettings().easing_function == "ease-in-out-cubic" ||
params.transition == 'easeInOutCubic') {
params.mode = Clutter.AnimationMode.EASE_IN_OUT_CUBIC;
} else if (params.transition == 'userChoice' && this.getSettings().easing_function == "ease-in-quart" ||
params.transition == 'easeInQuart') {
params.mode = Clutter.AnimationMode.EASE_IN_QUART;
} else if (params.transition == 'userChoice' && this.getSettings().easing_function == "ease-out-quart" ||
params.transition == 'easeOutQuart') {
params.mode = Clutter.AnimationMode.EASE_OUT_QUART;
} else if (params.transition == 'userChoice' && this.getSettings().easing_function == "ease-in-out-quart" ||
params.transition == 'easeInOutQuart') {
params.mode = Clutter.AnimationMode.EASE_IN_OUT_QUART;
} else if (params.transition == 'userChoice' && this.getSettings().easing_function == "ease-in-quint" ||
params.transition == 'easeInQuint') {
params.mode = Clutter.AnimationMode.EASE_IN_QUINT;
} else if (params.transition == 'userChoice' && this.getSettings().easing_function == "ease-out-quint" ||
params.transition == 'easeOutQuint') {
params.mode = Clutter.AnimationMode.EASE_OUT_QUINT;
} else if (params.transition == 'userChoice' && this.getSettings().easing_function == "ease-in-out-quint" ||
params.transition == 'easeInOutQuint') {
params.mode = Clutter.AnimationMode.EASE_IN_OUT_QUINT;
} else if (params.transition == 'userChoice' && this.getSettings().easing_function == "ease-in-sine" ||
params.transition == 'easeInSine') {
params.mode = Clutter.AnimationMode.EASE_IN_SINE;
} else if (params.transition == 'userChoice' && this.getSettings().easing_function == "ease-out-sine" ||
params.transition == 'easeOutSine') {
params.mode = Clutter.AnimationMode.EASE_OUT_SINE;
} else if (params.transition == 'userChoice' && this.getSettings().easing_function == "ease-in-out-sine" ||
params.transition == 'easeInOutSine') {
params.mode = Clutter.AnimationMode.EASE_IN_OUT_SINE;
} else if (params.transition == 'userChoice' && this.getSettings().easing_function == "ease-in-expo" ||
params.transition == 'easeInExpo') {
params.mode = Clutter.AnimationMode.EASE_IN_EXPO;
} else if (params.transition == 'userChoice' && this.getSettings().easing_function == "ease-out-expo" ||
params.transition == 'easeOutExpo') {
params.mode = Clutter.AnimationMode.EASE_OUT_EXPO;
} else if (params.transition == 'userChoice' && this.getSettings().easing_function == "ease-in-out-expo" ||
params.transition == 'easeInOutExpo') {
params.mode = Clutter.AnimationMode.EASE_IN_OUT_EXPO;
} else if (params.transition == 'userChoice' && this.getSettings().easing_function == "ease-in-circ" ||
params.transition == 'easeInCirc') {
params.mode = Clutter.AnimationMode.EASE_IN_CIRC;
} else if (params.transition == 'userChoice' && this.getSettings().easing_function == "ease-out-circ" ||
params.transition == 'easeOutCirc') {
params.mode = Clutter.AnimationMode.EASE_OUT_CIRC;
} else if (params.transition == 'userChoice' && this.getSettings().easing_function == "ease-in-out-circ" ||
params.transition == 'easeInOutCirc') {
params.mode = Clutter.AnimationMode.EASE_IN_OUT_CIRC;
} else if (params.transition == 'userChoice' && this.getSettings().easing_function == "ease-linear" ||
params.transition == 'easeLinear') {
params.mode = Clutter.AnimationMode.LINEAR;
} else {
log("Could not find Clutter AnimationMode", params.transition, this.getSettings().easing_function);
}
if (params.onComplete) {
if (params.onCompleteParams && params.onCompleteScope) {
params.onComplete = params.onComplete.bind(params.onCompleteScope, ...params.onCompleteParams);
} else if (params.onCompleteParams) {
params.onComplete = params.onComplete.bind(null, params.onCompleteParams);
} else if (params.onCompleteScope) {
params.onComplete = params.onComplete.bind(params.onCompleteScope);
}
}
actor.ease(params);
}
removeTweens(actor) {
actor.remove_all_transitions();
}
initBackground() {
this._backgroundGroup = new Meta.BackgroundGroup();
Main.layoutManager.uiGroup.add_child(this._backgroundGroup);
if (this._backgroundGroup.lower_bottom) {
this._backgroundGroup.lower_bottom();
} else {
Main.uiGroup.set_child_below_sibling(this._backgroundGroup, null);
}
this._backgroundShade = new Clutter.Actor({
opacity: 0,
reactive: false
});
let constraint = Clutter.BindConstraint.new(this._backgroundGroup,
Clutter.BindCoordinate.ALL, 0);
this._backgroundShade.add_constraint(constraint);
let shade = new MyRadialShaderEffect({name: 'shade'});
shade.brightness = 1;
shade.sharpness = this._settings.dim_factor;
this._backgroundShade.add_effect(shade);
this._backgroundGroup.add_child(this._backgroundShade);
this._backgroundGroup.set_child_above_sibling(this._backgroundShade, null);
this._backgroundGroup.hide();
for (let i = 0; i < Main.layoutManager.monitors.length; i++) {
new Background.BackgroundManager({
container: this._backgroundGroup,
monitorIndex: i,
vignette: false,
});
}
}
hidePanels() {
let panels = this.getPanels();
for (let panel of panels) {
try {
let panelActor = (panel instanceof Clutter.Actor) ? panel : panel.actor;
panelActor.set_reactive(false);
this.tween(panelActor, {
opacity: 0,
time: this._settings.animation_time,
transition: 'easeInOutQuint'
});
} catch (e) {
log(e);
// ignore fake panels
}
}
}
dimBackground() {
if (this._settings.hide_panel) {
this.hidePanels();
}
// hide gnome-shell legacy tray
try {
if (Main.legacyTray) {
Main.legacyTray.actor.hide();
}
} catch (e) {
// ignore missing legacy tray
}
this._backgroundGroup.show();
this.tween(this._backgroundShade, {
opacity: 255,
time: this._settings.animation_time,
transition: 'easeInOutQuint',
});
}
showPanels(time) {
// panels
let panels = this.getPanels();
for (let panel of panels){
try {
let panelActor = (panel instanceof Clutter.Actor) ? panel : panel.actor;
panelActor.set_reactive(true);
if (this._settings.hide_panel) {
this.removeTweens(panelActor);
this.tween(panelActor, {
opacity: 255,
time: time,
transition: 'easeInOutQuint'
});
}
} catch (e) {
//ignore fake panels
}
}
}
lightenBackground() {
if (this._settings.hide_panel) {
this.showPanels(this._settings.animation_time);
}
// show gnome-shell legacy trayconn
try {
if (Main.legacyTray) {
Main.legacyTray.actor.show();
}
} catch (e) {
//ignore missing legacy tray
}
this.tween(this._backgroundShade, {
time: this._settings.animation_time * 0.95,
transition: 'easeInOutQuint',
opacity: 0,
});
}
removeBackground() {
this._backgroundGroup.destroy();
}
getPanels() {
let panels = [Main.panel];
if (Main.panel2)
panels.push(Main.panel2);
// gnome-shell dash
if (Main.overview._dash)
panels.push(Main.overview._dash);
return panels;
}
}
const VIGNETTE_DECLARATIONS = ' \
uniform float brightness; \n\
uniform float vignette_sharpness; \n\
float rand(vec2 p) { \n\
return fract(sin(dot(p, vec2(12.9898, 78.233))) * 43758.5453123); \n\
} \n';
const VIGNETTE_CODE = ' \
cogl_color_out.a = cogl_color_in.a; \n\
cogl_color_out.rgb = vec3(0.0, 0.0, 0.0); \n\
vec2 position = cogl_tex_coord_in[0].xy - 0.5; \n\
float t = clamp(length(1.41421 * position), 0.0, 1.0); \n\
float pixel_brightness = mix(1.0, 1.0 - vignette_sharpness, t); \n\
cogl_color_out.a *= 1.0 - pixel_brightness * brightness; \n\
cogl_color_out.a += (rand(position) - 0.5) / 100.0; \n';
const MyRadialShaderEffect = GObject.registerClass({
Properties: {
'brightness': GObject.ParamSpec.float(
'brightness', 'brightness', 'brightness',
GObject.ParamFlags.READWRITE,
0, 1, 1),
'sharpness': GObject.ParamSpec.float(
'sharpness', 'sharpness', 'sharpness',
GObject.ParamFlags.READWRITE,
0, 1, 0),
},
}, class MyRadialShaderEffect extends Shell.GLSLEffect {
_init(params) {
this._brightness = undefined;
this._sharpness = undefined;
super._init(params);
this._brightnessLocation = this.get_uniform_location('brightness');
this._sharpnessLocation = this.get_uniform_location('vignette_sharpness');
this.brightness = 1.0;
this.sharpness = 0.0;
}
vfunc_build_pipeline() {
this.add_glsl_snippet(Shell.SnippetHook.FRAGMENT,
VIGNETTE_DECLARATIONS, VIGNETTE_CODE, true);
}
get brightness() {
return this._brightness;
}
set brightness(v) {
if (this._brightness === v)
return;
this._brightness = v;
this.set_uniform_float(this._brightnessLocation,
1, [this._brightness]);
this.notify('brightness');
}
get sharpness() {
return this._sharpness;
}
set sharpness(v) {
if (this._sharpness === v)
return;
this._sharpness = v;
this.set_uniform_float(this._sharpnessLocation,
1, [this._sharpness]);
this.notify('sharpness');
}
});

View File

@ -0,0 +1,641 @@
/*
This file is part of CoverflowAltTab.
CoverflowAltTab is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
CoverflowAltTab is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with CoverflowAltTab. If not, see <http://www.gnu.org/licenses/>.
*/
/*
* CoverflowAltTab
*
* Preferences dialog for "gnome-extensions prefs" tool
*
* Based on preferences in the following extensions: JustPerfection, dash2doc-lite, night theme switcher, and desktop cube
*
*/
import Adw from 'gi://Adw';
import Gdk from 'gi://Gdk';
import GLib from 'gi://GLib';
import Gtk from 'gi://Gtk';
import Gio from 'gi://Gio';
import {ExtensionPreferences, gettext as _} from 'resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js'
const easing_options = [
{
id: 'ease-linear', name: 'easeLinear'
}, {
id: 'ease-in-quad', name: "easeInQuad"
}, {
id: 'ease-out-quad', name: "easeOutQuad"
}, {
id: 'ease-in-out-quad', name: "easeInOutQuad"
}, {
id: 'ease-in-cubic', name: "easeInCubic"
}, {
id: 'ease-out-cubic', name: "easeOutCubic"
}, {
id: 'ease-in-out-cubic', name: "easeInOutCubic"
}, {
id: 'ease-in-quart', name: "easeInQuart"
}, {
id: 'ease-out-quart', name: "easeOutQuart"
}, {
id: 'ease-in-out-quart', name: "easeInOutQuart"
}, {
id: 'ease-in-quint', name: "easeInQuint"
}, {
id: 'ease-out-quint', name: "easeOutQuint"
}, {
id: 'ease-in-out-quint', name: "easeInOutQuint"
}, {
id: 'ease-in-sine', name: "easeInSine"
}, {
id: 'ease-out-sine', name: "easeOutSine"
}, {
id: 'ease-in-out-sine', name: "easeInOutSine"
}, {
id: 'ease-in-expo', name: "easeInExpo"
}, {
id: 'ease-out-expo', name: "easeOutExpo"
}, {
id: 'ease-in-out-expo', name: "easeInOutExpo"
}, {
id: 'ease-in-circ', name: "easeInCirc"
}, {
id: 'ease-out-circ', name: "easeOutCirc"
}, {
id: 'ease-in-out-circ', name: "easeInOutCirc"
}, {
id: 'ease-in-back', name: "easeInBack"
}, {
id: 'ease-out-back', name: "easeOutBack"
}, {
id: 'ease-in-out-back', name: "easeInOutBack"
}, {
id: 'ease-in-elastic', name: "easeInElastic"
}, {
id: 'ease-out-elastic', name: "easeOutElastic"
}, {
id: 'ease-in-out-elastic', name: "easeInOutElastic"
}, {
id: 'ease-in-bounce', name: "easeInBounce"
}, {
id: 'ease-out-bounce', name: "easeOutBounce"
}, {
id: 'ease-in-out-bounce', name: "easeInOutBounce"
}, {
id: 'random', name: "Random"
}];
function getBaseString(translatedString) {
switch (translatedString) {
case _("Coverflow"): return "Coverflow";
case _("Timeline"): return "Timeline";
case _("Bottom"): return "Bottom";
case _("Top"): return "Top";
case _("Classic"): return "Classic";
case _("Overlay"): return "Overlay";
default: return translatedString;
}
}
function makeResetButton() {
return new Gtk.Button({
icon_name: "edit-clear-symbolic",
tooltip_text: _("Reset to default value"),
valign: Gtk.Align.CENTER,
});
}
export default class CoverflowAltTabPreferences extends ExtensionPreferences {
constructor(metadata) {
super(metadata);
let IconsPath = GLib.build_filenamev([this.path, 'ui', 'icons']);
let iconTheme = Gtk.IconTheme.get_for_display(Gdk.Display.get_default());
iconTheme.add_search_path(IconsPath);
}
getVersionString(_page) {
return _('Version %d').format(this.metadata.version);
}
fillPreferencesWindow(window) {
let settings = this.getSettings();
let general_page = new Adw.PreferencesPage({
title: _('General'),
icon_name: 'general-symbolic',
});
let switcher_pref_group = new Adw.PreferencesGroup({
title: _('Switcher'),
});
let switcher_looping_method_buttons = new Map([ [_("Flip Stack"), [[],[]]], [_("Carousel"), [[],[]]]]);
let switcher_looping_method_row = buildRadioAdw(settings, "switcher-looping-method", switcher_looping_method_buttons, _("Looping Method"), _("How to cycle through windows."));
switcher_pref_group.add(buildRadioAdw(settings, "switcher-style", new Map([ [_("Coverflow"), [[switcher_looping_method_row], []]], [_("Timeline"), [[],[switcher_looping_method_row]] ]]), _("Style"), _("Pick the type of switcher.")))
switcher_pref_group.add(buildSpinAdw(settings, "offset", [-500, 500, 1, 10], _("Vertical Offset"), _("Positive value moves everything down, negative up.")));
switcher_pref_group.add(buildRadioAdw(settings, "position", new Map([ [_("Bottom"), [[], []]], [_("Top"), [[],[]]]]), _("Window Title Position"), _("Place window title above or below the switcher.")));
switcher_pref_group.add(buildSwitcherAdw(settings, "enforce-primary-monitor", [], [], _("Enforce Primary Monitor"), _("Always show on the primary monitor, otherwise, show on the active monitor.")));
switcher_pref_group.add(switcher_looping_method_row);
switcher_pref_group.add(buildSwitcherAdw(settings, "hide-panel", [], [], _("Hide Panel"), _("Hide panel when switching windows.")));
switcher_pref_group.add(buildSwitcherAdw(settings, "invert-swipes", [], [], _("Invert Swipes"), _("Swipe content instead of view.")));
let animation_pref_group = new Adw.PreferencesGroup({
title: _('Animation'),
});
animation_pref_group.add(buildDropDownAdw(settings, "easing-function", easing_options, "Easing Function", "Determine how windows move."));
animation_pref_group.add(buildRangeAdw(settings, "animation-time", [0.01, 20, 0.001, [0.5, 1, 1.5]], _("Duration [s]"), "", true));
animation_pref_group.add(buildSwitcherAdw(settings, "randomize-animation-times", [], [], _("Randomize Durations"), _("Each animation duration assigned randomly between 0 and configured duration.")));
let windows_pref_group = new Adw.PreferencesGroup({
title: _('Switcher Windows'),
});
let options = [{
id: 'current', name: _("Current workspace only")
}, {
id: 'all', name: _("All workspaces")
}, {
id: 'all-currentfirst', name: _("All workspaces, current first")
}];
windows_pref_group.add(buildDropDownAdw(settings, "current-workspace-only", options, _("Workspaces"), _("Switch between windows on current or on all workspaces.")));
windows_pref_group.add(buildSwitcherAdw(settings, "switch-per-monitor", [], [], _("Current Monitor"), _("Switch between windows on current monitor.")));
let icon_pref_group = new Adw.PreferencesGroup({
title: _("Icons"),
});
let size_row = buildRangeAdw(settings, "overlay-icon-size", [16, 1024, 1, [32, 64, 128, 256, 512]], _("Overlay Icon Size"), _("Set the overlay icon size in pixels."), true);
let opacity_row = buildRangeAdw(settings, "overlay-icon-opacity", [0, 1, 0.001, [0.25, 0.5, 0.75]], _("Overlay Icon Opacity"), _("Set the overlay icon opacity."), true);
let buttons = new Map([[_("Classic"), [[],[size_row, opacity_row]]], [_("Overlay"), [[size_row, opacity_row], []]], [_("Attached"), [[size_row, opacity_row], []]]]);
let style_row = buildRadioAdw(settings, "icon-style", buttons, _("Application Icon Style"));
icon_pref_group.add(style_row);
icon_pref_group.add(size_row);
icon_pref_group.add(opacity_row);
icon_pref_group.add(buildSwitcherAdw(settings, "icon-has-shadow", [], [], _("Icon Shadow")));
let window_size_pref_group = new Adw.PreferencesGroup({
title: _("Window Properties")
});
window_size_pref_group.add(buildRangeAdw(settings, "preview-to-monitor-ratio", [0, 1, 0.001, [0.250, 0.500, 0.750]], _("Window Preview Size to Monitor Size Ratio"), _("Maximum ratio of window preview size to monitor size."), true));
window_size_pref_group.add(buildRangeAdw(settings, "preview-scaling-factor", [0, 1, 0.001, [0.250, 0.500, 0.800]], _("Off-center Size Factor"), _("Factor by which to successively shrink previews off to the side."), true));
let background_application_switcher_pref_group = new Adw.PreferencesGroup({
title: _('Application Switcher'),
});
background_application_switcher_pref_group.add(buildSwitcherAdw(settings, "switch-application-behaves-like-switch-windows", [], [], _("Make the Application Switcher Behave Like the Window Switcher"), _("Don't group windows of the same application in a subswitcher.")));
background_application_switcher_pref_group.add(buildRangeAdw(settings, "desaturate-factor", [0, 1, 0.001, [0.25, 0.5, 0.75]], _("Desaturate"), _("Larger means more desaturation."), true));
background_application_switcher_pref_group.add(buildSpinAdw(settings, "blur-radius", [0, 20, 1, 1], _("Blur"), _("Larger means blurrier.")));
let color_row = new Adw.ExpanderRow({
title: _("Tint"),
});
background_application_switcher_pref_group.add(color_row);
let use_tint_switch = new Gtk.Switch({
valign: Gtk.Align.CENTER,
active: settings.get_boolean("use-tint"),
});
settings.bind("use-tint", use_tint_switch, "active", Gio.SettingsBindFlags.DEFAULT);
color_row.add_suffix(use_tint_switch);
let tint_chooser_row = new Adw.ActionRow({
title: _("Color")
});
let choose_tint_box = new Gtk.Box({
orientation: Gtk.Orientation.HORIZONTAL,
spacing: 10,
valign: Gtk.Align.CENTER,
});
tint_chooser_row.add_suffix(choose_tint_box);
color_row.add_row(tint_chooser_row);
let color_dialog = new Gtk.ColorDialog({
with_alpha: false,
});
let color_button = new Gtk.ColorDialogButton({
valign: Gtk.Align.CENTER,
dialog: color_dialog,
});
let use_theme_color_button = new Gtk.Button({
label: _("Set to Theme Color"),
valign: Gtk.Align.CENTER,
});
use_theme_color_button.connect('clicked', () => {
let c = settings.get_value("switcher-background-color").deep_unpack();
let rgba = color_button.rgba;
rgba.red = c[0];
rgba.green = c[1];
rgba.blue = c[2];
rgba.alpha = 1
color_button.set_rgba(rgba);
});
choose_tint_box.append(use_theme_color_button);
choose_tint_box.append(color_button);
let c = settings.get_value("tint-color").deep_unpack();
let rgba = color_button.rgba;
rgba.red = c[0];
rgba.green = c[1];
rgba.blue = c[2];
rgba.alpha = 1
color_button.set_rgba(rgba);
color_button.connect('notify::rgba', _ => {
let c = color_button.rgba;
let val = new GLib.Variant("(ddd)", [c.red, c.green, c.blue]);
settings.set_value("tint-color", val);
});
use_tint_switch.connect('notify::active', function(widget) {
color_row.set_expanded(widget.get_active());
});
let reset_button = makeResetButton();
reset_button.connect("clicked", function (widget) {
settings.reset("use-tint");
});
color_row.add_suffix(reset_button);
color_row.add_row(buildRangeAdw(settings, "tint-blend", [0, 1, 0.001, [0.25, 0.5, 0.75]], _("Blend"), _("How much to blend the tint color; bigger means more tint color."), true));
background_application_switcher_pref_group.add(buildSwitcherAdw(settings, "use-glitch-effect", [], [], _("Glitch")));
let background_pref_group = new Adw.PreferencesGroup({
title: _('Background'),
});
background_pref_group.add(buildRangeAdw(settings, "dim-factor", [0, 1, 0.001, [0.25, 0.5, 0.75]], _("Dim-factor"), _("Bigger means darker."), true));
let keybinding_pref_group = new Adw.PreferencesGroup({
title: _("Keybindings"),
});
keybinding_pref_group.add(buildSwitcherAdw(settings, "bind-to-switch-windows", [], [], _("Bind to 'switch-windows'")));
keybinding_pref_group.add(buildSwitcherAdw(settings, "bind-to-switch-applications", [background_application_switcher_pref_group], [], _("Bind to 'switch-applications'")));
let pcorrection_pref_group = new Adw.PreferencesGroup({
title: _("Perspective Correction")
})
pcorrection_pref_group.add(buildDropDownAdw(settings, "perspective-correction-method", [
{ id: "None", name: _("None") },
{ id: "Move Camera", name: _("Move Camera") },
{ id: "Adjust Angles", name: _("Adjust Angles") }],
_("Perspective Correction"), ("Method to make off-center switcher look centered.")));
let highlight_mouse_over_pref_group = new Adw.PreferencesGroup({
title: _("Highlight Window Under Mouse"),
});
window_size_pref_group.add(buildSwitcherAdw(settings, "highlight-mouse-over", [], [], _("Highlight Window Under Mouse"), _("Draw embelishment on window under the mouse to know the effects of clicking.")));
window_size_pref_group.add(buildSwitcherAdw(settings, "raise-mouse-over", [], [], _("Raise Window Under Mouse"), _("Raise the window under the mouse above all others.")));
/*let tweaks_page = new Adw.PreferencesPage({
title: _('Tweaks'),
icon_name: 'applications-symbolic',
});
tweaks_page.add(pcorrection_pref_group);
tweaks_page.add(highlight_mouse_over_pref_group);*/
general_page.add(switcher_pref_group);
general_page.add(animation_pref_group);
general_page.add(icon_pref_group);
general_page.add(windows_pref_group);
general_page.add(window_size_pref_group);
general_page.add(background_pref_group);
general_page.add(background_application_switcher_pref_group);
general_page.add(pcorrection_pref_group);
general_page.add(keybinding_pref_group);
let contribution_page = new Adw.PreferencesPage({
title: _("Contribute"),
icon_name: 'contribute-symbolic',
});
let contribute_icon_pref_group = new Adw.PreferencesGroup();
let icon_box = new Gtk.Box({
orientation: Gtk.Orientation.VERTICAL,
margin_top: 24,
margin_bottom: 24,
spacing: 18,
});
let icon_image = new Gtk.Image({
icon_name: "coverflow-symbolic",
pixel_size: 128,
});
let label_box = new Gtk.Box({
orientation: Gtk.Orientation.VERTICAL,
spacing: 6,
});
let label = new Gtk.Label({
label: "Coverflow Alt-Tab",
wrap: true,
});
let context = label.get_style_context();
context.add_class("title-1");
let another_label = new Gtk.Label({
label: this.getVersionString(),
});
let links_pref_group = new Adw.PreferencesGroup();
let code_row = new Adw.ActionRow({
icon_name: "code-symbolic",
title: _("Code (create pull requests, report issues, etc.)")
});
let github_link = new Gtk.LinkButton({
label: "Github",
uri: "https://github.com/dmo60/CoverflowAltTab",
});
let donate_row = new Adw.ActionRow({
title: _("Donate"),
icon_name: "support-symbolic",
})
let donate_link = new Gtk.LinkButton({
label: "Liberapay",
uri: "https://liberapay.com/dsheeler/donate",
});
let donate_link_paypal = new Gtk.LinkButton({
label: "PayPal",
uri: "https://paypal.me/DanielSheeler?country.x=US&locale.x=en_US",
});
let donate_link_github = new Gtk.LinkButton({
label: "Github",
uri: "https://github.com/sponsors/dsheeler",
});
let translate_row = new Adw.ActionRow({
title: _("Translate"),
icon_name: "translate-symbolic",
})
let translate_link = new Gtk.LinkButton({
label: "Weblate",
uri: "https://hosted.weblate.org/engage/coverflow-alt-tab/",
});
code_row.add_suffix(github_link);
code_row.set_activatable_widget(github_link);
translate_row.add_suffix(translate_link);
translate_row.set_activatable_widget(translate_link);
donate_row.add_suffix(donate_link);
donate_row.add_suffix(donate_link_paypal);
donate_row.add_suffix(donate_link_github);
links_pref_group.add(code_row);
links_pref_group.add(translate_row);
links_pref_group.add(donate_row);
label_box.append(label);
label_box.append(another_label);
icon_box.append(icon_image);
icon_box.append(label_box);
contribute_icon_pref_group.add(icon_box);
contribution_page.add(contribute_icon_pref_group);
contribution_page.add(links_pref_group)
window.add(general_page);
// window.add(appearance_page);
window.add(contribution_page);
window.set_search_enabled(true);
}
}
function buildSwitcherAdw(settings, key, dependant_widgets, inverse_dependant_widgets, title, subtitle=null) {
let pref = new Adw.ActionRow({
title: title,
});
if (subtitle != null) {
pref.set_subtitle(subtitle);
}
let switcher = new Gtk.Switch({
valign: Gtk.Align.CENTER,
active: settings.get_boolean(key)
});
switcher.expand = false;
switcher.connect('notify::active', function(widget) {
settings.set_boolean(key, widget.active);
});
pref.set_activatable_widget(switcher);
pref.add_suffix(switcher);
switcher.connect('notify::active', function(widget) {
for (let dep of dependant_widgets) {
dep.set_sensitive(widget.get_active());
}
});
for (let widget of dependant_widgets) {
widget.set_sensitive(switcher.get_active());
}
switcher.connect('notify::active', function(widget) {
for (let inv_dep of inverse_dependant_widgets) {
inv_dep.set_sensitive(!widget.get_active());
}
});
for (let widget of inverse_dependant_widgets) {
widget.set_sensitive(!switcher.get_active());
}
let reset_button = makeResetButton();
reset_button.connect("clicked", function(widget) {
settings.reset(key);
switcher.set_active(settings.get_boolean(key));
})
pref.add_suffix(reset_button);
return pref;
}
function buildRangeAdw(settings, key, values, title, subtitle="", draw_value=false) {
let [min, max, step, defvs] = values;
let pref = new Adw.ActionRow({
title: title, });
if (subtitle !== null && subtitle !== "") {
pref.set_subtitle(subtitle);
}
let range = Gtk.Scale.new_with_range(Gtk.Orientation.HORIZONTAL, min, max, step);
range.set_value(settings.get_double(key));
if (draw_value) {
range.set_draw_value(true);
range.set_value_pos(Gtk.PositionType.RIGHT)
}
for (let defv of defvs) {
range.add_mark(defv, Gtk.PositionType.BOTTOM, null);
}
range.set_size_request(200, -1);
range.connect('value-changed', function(slider) {
settings.set_double(key, slider.get_value());
});
pref.set_activatable_widget(range);
pref.add_suffix(range)
let reset_button = makeResetButton();
reset_button.connect("clicked", function(widget) {
settings.reset(key);
range.set_value(settings.get_double(key));
});
pref.add_suffix(reset_button);
return pref;
}
function buildRadioAdw(settings, key, buttons, title, subtitle=null) {
let pref = new Adw.ActionRow({
title: title,
});
if (subtitle != null) {
pref.set_subtitle(subtitle);
}
let hbox = new Gtk.Box({
orientation: Gtk.Orientation.HORIZONTAL,
spacing: 10,
valign: Gtk.Align.CENTER,
});
let radio = new Gtk.ToggleButton();
let radio_for_button = {};
for (let button_name of buttons.keys()) {
radio = new Gtk.ToggleButton({group: radio, label: button_name});
radio_for_button[button_name] = radio;
if (getBaseString(button_name) == settings.get_string(key)) {
radio.set_active(true);
for (let pref_row of buttons.get(button_name)[0]) {
pref_row.set_sensitive(radio_for_button[button_name].get_active());
}
for (let pref_row of buttons.get(button_name)[1]) {
pref_row.set_sensitive(!radio_for_button[button_name].get_active());
}
}
radio.connect('toggled', function(widget) {
if (widget.get_active()) {
settings.set_string(key, getBaseString(widget.get_label()));
}
for (let pref_row of buttons.get(button_name)[0]) {
pref_row.set_sensitive(widget.get_active());
}
for (let pref_row of buttons.get(button_name)[1]) {
pref_row.set_sensitive(!widget.get_active());
}
});
hbox.append(radio);
};
let reset_button = makeResetButton();
reset_button.connect("clicked", function(widget) {
settings.reset(key);
for (let button of buttons.keys()) {
if (getBaseString(button) == settings.get_string(key)) {
radio_for_button[button].set_active(true);
}
}
});
pref.set_activatable_widget(hbox);
pref.add_suffix(hbox);
pref.add_suffix(reset_button);
return pref;
};
function buildSpinAdw(settings, key, values, title, subtitle=null) {
let [min, max, step, page] = values;
let pref = new Adw.ActionRow({
title: title,
});
if (subtitle != null) {
pref.set_subtitle(subtitle);
}
let spin = new Gtk.SpinButton({ valign: Gtk.Align.CENTER });
spin.set_range(min, max);
spin.set_increments(step, page);
spin.set_value(settings.get_int(key));
spin.connect('value-changed', function(widget) {
settings.set_int(key, widget.get_value());
});
pref.set_activatable_widget(spin);
pref.add_suffix(spin);
let reset_button = makeResetButton();
reset_button.connect("clicked", function(widget) {
settings.reset(key);
spin.set_value(settings.get_int(key));
});
pref.add_suffix(reset_button);
return pref;
}
function buildDropDownAdw(settings, key, values, title, subtitle=null) {
let pref = new Adw.ActionRow({
title: title,
});
if (subtitle != null) {
pref.set_subtitle(subtitle);
}
let model = new Gtk.StringList();
let chosen_idx = 0;
for (let i = 0; i < values.length; i++) {
let item = values[i];
model.append(item.name);
if (item.id == settings.get_string(key)) {
chosen_idx = i;
}
}
let chooser = new Gtk.DropDown({
valign: Gtk.Align.CENTER,
model: model,
selected: chosen_idx,
});
chooser.connect('notify::selected-item', function(c) {
let idx = c.get_selected();
settings.set_string(key, values[idx].id);
});
pref.set_activatable_widget(chooser);
pref.add_suffix(chooser);
let reset_button = makeResetButton();
reset_button.connect("clicked", function(widget) {
settings.reset(key);
for (let i = 0; i < values.length; i++) {
let item = values[i];
if (item.id == settings.get_string(key)) {
chooser.set_selected(i);
break;
}
}
});
pref.add_suffix(reset_button);
return pref;
}

View File

@ -0,0 +1,429 @@
/*
This file is part of CoverflowAltTab.
CoverflowAltTab is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
CoverflowAltTab is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with CoverflowAltTab. If not, see <http://www.gnu.org/licenses/>.
*/
import Clutter from 'gi://Clutter';
import GObject from 'gi://GObject';
import Graphene from 'gi://Graphene';
import St from 'gi://St';
/**
* Direction and Placement properties values are set to be compatible with deprecated
* Clutter.Gravity.
*/
export class Direction {}
Direction.TO_RIGHT = 3;
Direction.TO_LEFT = 7;
export class Placement {}
Placement.TOP = 1;
Placement.TOP_RIGHT = 2;
Placement.RIGHT = 3;
Placement.BOTTOM_RIGHT = 4;
Placement.BOTTOM = 5;
Placement.BOTTOM_LEFT = 6;
Placement.LEFT = 7;
Placement.TOP_LEFT = 8;
Placement.CENTER = 9;
export const Preview = GObject.registerClass({
GTypeName: "Preview",
Properties: {
'remove_icon_opacity': GObject.ParamSpec.double(
`remove_icon_opacity`,
`Revmove Icon Opacity`,
`Icon opacity when `,
GObject.ParamFlags.READWRITE,
0.0, 1.0,
1.0,
)
}
}, class Preview extends Clutter.Clone {
_init(window, switcher, ...args) {
super._init(...args);
this.metaWin = window;
this.switcher = switcher;
this._icon = null;
this._highlight = null;
this._flash = null;
this._entered = false;
this._effectNames = ['blur', 'glitch', 'desaturate', 'tint']
this._effectCounts = {};
for (let effect_name of this._effectNames) {
this._effectCounts[effect_name] = 0;
}
}
/**
* Make the preview above all other children layers in the given parent.
*
* @param {Object} parent The preview parent. If is not its real parent,then the
* behaviour is undefined.
* @return {void}
*/
make_top_layer(parent) {
if (this.raise_top) {
this.raise_top()
if (this._icon) this._icon.raise_top();
} else if (parent.set_child_above_sibling) {
parent.set_child_above_sibling(this, null);
if (this._icon) parent.set_child_above_sibling(this._icon, this);
} else {
// Don't throw anything here, it may cause unstabilities
logError("No method found for making preview the top layer");
}
}
/**
* Make the preview below all other children layers in the given parent.
*
* @param {Object} parent The preview parent. If is not its real parent,then the
* behaviour is undefined.
* @return {void}
*/
make_bottom_layer(parent) {
if (this.lower_bottom) {
if (this._icon) this._icon.lower_bottom();
this.lower_bottom()
} else if (parent.set_child_below_sibling) {
parent.set_child_below_sibling(this, null);
if (this._icon) parent.set_child_above_sibling(this._icon, this);
} else {
// Don't throw anything here, it may cause unstabilities
logError("No method found for making preview the bottom layer");
}
}
addEffect(effect_class, constructor_argument, name, parameter_name, from_param_value, param_value, duration) {
duration = 0.99 * 1000.0 * duration;
let effect_name = name + "-effect";
let add_transition_name = effect_name + "-add";
let remove_transition_name = effect_name + "-remove";
let property_transition_name = `@effects.${effect_name}.${parameter_name}`;
if (this.get_transition(remove_transition_name) !== null) {
this.remove_transition(remove_transition_name);
let transition = Clutter.PropertyTransition.new(property_transition_name);
transition.progress_mode = Clutter.AnimationMode.LINEAR;
transition.duration = duration;
transition.remove_on_complete = true;
transition.set_from(this.get_effect(effect_name)[parameter_name]);
transition.set_to(param_value);
this.get_effect(effect_name)[parameter_name] = 1.0;
this.add_transition(add_transition_name, transition);
transition.connect('new-frame', (timeline, msecs) => {
this.queue_redraw();
});
} else if (this._effectCounts[name] == 0) {
if (this.get_transition(add_transition_name) === null) {
let transition = Clutter.PropertyTransition.new(property_transition_name);
transition.progress_mode = Clutter.AnimationMode.LINEAR;
transition.duration = duration;
transition.remove_on_complete = true;
transition.set_to(param_value);
transition.set_from(from_param_value);
this._newFrameCount = 0;
this.add_effect_with_name(effect_name, new effect_class(constructor_argument));
this.add_transition(add_transition_name, transition);
this._effectCounts[name] = 1;
transition.connect('new-frame', (timeline, msecs) => {
this.queue_redraw();
});
}
} else {
this._effectCounts[name] += 1;
}
}
removeEffect(name, parameter_name, value, duration) {
duration = 0.99 * 1000.0 * duration;
let effect_name = name + "-effect";
let add_transition_name = effect_name + "-add";
let remove_transition_name = effect_name + "-remove";
let property_transition_name = `@effects.${effect_name}.${parameter_name}`;
if (this._effectCounts[name] > 0) {
if (this._effectCounts[name] == 1) {
this.remove_transition(add_transition_name);
if (this.get_transition(remove_transition_name) === null) {
let transition = Clutter.PropertyTransition.new(property_transition_name);
transition.progress_mode = Clutter.AnimationMode.LINEAR;
transition.duration = duration;
transition.remove_on_complete = true;
transition.set_from(this.get_effect(effect_name)[parameter_name]);
transition.set_to(value);
this.get_effect(effect_name)[parameter_name] = 1.0;
this.add_transition(remove_transition_name, transition);
transition.connect("completed", (trans) => {
this.remove_effect_by_name(effect_name);
this._effectCounts[name] = 0;
});
}
} else {
this._effectCounts[name] -= 1;
}
}
}
_pulse_highlight() {
if (this._highlight == null) return;
this._highlight.ease({
opacity: 255,
duration: 2000,
mode: Clutter.AnimationMode.EASE_IN_OUT_QUINT,
onComplete: () => {
this._highlight.ease({
opacity: 80,
duration: 1400,
mode: Clutter.AnimationMode.EASE_IN_OUT_QUINT,
onComplete: () => {
this._pulse_highlight();
},
});
},
});
}
remove_highlight() {
if (this._highlight != null) {
this._highlight.ease({
opacity: 0,
duration: 300,
mode: Clutter.AnimationMode.EASE_IN_OUT_QUINT,
onComplete: () => {
if (this._highlight != null) {
this._highlight.destroy()
this._highlight = null;
}
},
});
}
if (this._flash != null) {
this._flash.destroy();
this._flash = null;
}
}
_getHighlightStyle(alpha) {
let bgcolor = this.switcher._getSwitcherBackgroundColor();
let style =`background-color: rgba(${bgcolor.red}, ${bgcolor.green}, ${bgcolor.blue}, ${alpha})`;
return style;
}
vfunc_enter_event(crossingEvent) {
if (this.switcher._animatingClosed || this._entered == true) {
return Clutter.EVENT_PROPAGATE;
}
this._entered = true;
if (this.switcher._settings.raise_mouse_over) {
this.make_top_layer(this.switcher.previewActor);
this.switcher._raiseIcons();
}
if (this.switcher._settings.highlight_mouse_over) {
let window_actor = this.metaWin.get_compositor_private();
if (this._highlight == null) {
this._highlight = new St.Bin({
opacity: 0,
width: this.width,
height: this.height,
x: 0,
y: 0,
reactive: false,
});
this._highlight.set_style(this._getHighlightStyle(0.3));
let constraint = Clutter.BindConstraint.new(window_actor, Clutter.BindCoordinate.SIZE, 0);
this._highlight.add_constraint(constraint);
window_actor.add_child(this._highlight);
}
if (this._flash == null) {
this._flash = new St.Bin({
width: 1,
height: 1,
opacity: 255,
reactive: false,
x: 0,
y: 0,
});
this._flash.set_style(this._getHighlightStyle(1));
let constraint = Clutter.BindConstraint.new(window_actor, Clutter.BindCoordinate.SIZE, 0);
this._flash.add_constraint(constraint);
window_actor.add_child(this._flash);
this._flash.ease({
opacity: 0,
duration: 500,
mode: Clutter.AnimationMode.EASE_OUT_QUINT,
onComplete: () => {
this._pulse_highlight();
}
});
}
}
return Clutter.EVENT_PROPAGATE;
}
addIcon() {
let app = this.switcher._tracker.get_window_app(this.metaWin);
let icon_size = this.switcher._settings.overlay_icon_size;
this._icon = app ? app.create_icon_texture(Math.min(icon_size, this.width, this.height) / this.scale) : null;
if (this._icon == null) {
this._icon = new St.Icon({
icon_name: 'applications-other',
});
}
let constraint = Clutter.BindConstraint.new(this, Clutter.BindCoordinate.ALL, 0);
this._icon.add_constraint(constraint);
this.bind_property_full('opacity',
this._icon, 'opacity',
GObject.BindingFlags.SYNC_CREATE,
(bind, source) => {
/* So that the icon fades out 1) when the preview fades
out, such as in the timeline switcher, and
2) when the icon is being removed,
but also ensure the icon only goes as high as the setting
opacity, we take the minimum of those three as our opacity.
Seems there might be a better way, but I'm not sure.
*/
return [true, Math.min(source, 255 * this.remove_icon_opacity, 255 * this.switcher._settings.overlay_icon_opacity)];
},
null);
this.bind_property('rotation_angle_y', this._icon, 'rotation_angle_y',
GObject.BindingFlags.SYNC_CREATE);
this.bind_property('pivot_point', this._icon, 'pivot_point',
GObject.BindingFlags.SYNC_CREATE);
this.bind_property('translation_x', this._icon, 'translation_x',
GObject.BindingFlags.SYNC_CREATE);
this.bind_property('scale_x', this._icon, 'scale_x',
GObject.BindingFlags.SYNC_CREATE);
this.bind_property('scale_y', this._icon, 'scale_y',
GObject.BindingFlags.SYNC_CREATE);
this.bind_property('scale_z', this._icon, 'scale_z',
GObject.BindingFlags.SYNC_CREATE);
this.switcher.previewActor.add_child(this._icon);
if (this.switcher._settings.icon_has_shadow) {
this._icon.add_style_class_name("icon-dropshadow");
}
}
removeIcon(animation_time) {
if (this._icon != null) {
let transition = Clutter.PropertyTransition.new('remove_icon_opacity');
transition.duration = 1000.0 * animation_time;
this._icon.remove_icon_opacity_start = this._icon.opacity / 255.;
transition.set_from(this._icon.remove_icon_opacity_start);
transition.set_to(0);
transition.remove_on_complete = true;
transition.connect('new-frame', (timeline, msecs) => {
this._icon.opacity = 255 * this._icon.remove_icon_opacity_start * (1 -
timeline.get_progress());//(1 - msecs / transition.duration);
this._icon.queue_redraw();
})
transition.connect('completed', (timeline) => {
if (this._icon != null) {
this._icon.destroy()
this._icon = null;
}
});
this.add_transition('remove_icon_opacity_transition', transition);
}
}
vfunc_leave_event(crossingEvent) {
this.remove_highlight();
this._entered = false;
if (this.switcher._settings.raise_mouse_over && !this.switcher._animatingClosed) this.switcher._updatePreviews(true, 0);
return Clutter.EVENT_PROPAGATE;
}
/**
* Gets the pivot point relative to the preview.
*
* @param {Placement} placement
* @return {Graphene.Point}
*/
get_pivot_point_placement(placement) {
let xFraction = 0,
yFraction = 0;
// Set xFraction
switch (placement) {
case Placement.TOP_LEFT:
case Placement.LEFT:
case Placement.BOTTOM_LEFT:
xFraction = 0;
break;
case Placement.TOP:
case Placement.CENTER:
case Placement.BOTTOM:
xFraction = 0.5;
break;
case Placement.TOP_RIGHT:
case Placement.RIGHT:
case Placement.BOTTOM_RIGHT:
xFraction = 1;
break;
default:
throw new Error("Unknown placement given");
}
// Set yFraction
switch (placement) {
case Placement.TOP_LEFT:
case Placement.TOP:
case Placement.TOP_RIGHT:
yFraction = 0;
break;
case Placement.LEFT:
case Placement.CENTER:
case Placement.RIGHT:
yFraction = 0.5;
break;
case Placement.BOTTOM_LEFT:
case Placement.BOTTOM:
case Placement.BOTTOM_RIGHT:
yFraction = 1;
break;
default:
throw new Error("Unknown placement given");
}
return new Graphene.Point({ x: xFraction, y: yFraction });
}
/**
* Sets the pivot point placement, relative to the preview.
*
* @param {Placement} placement
* @return {void}
*/
set_pivot_point_placement(placement) {
let point = this.get_pivot_point_placement(placement);
this.set_pivot_point(point.x, point.y);
}
});
export function findUpperLeftFromCenter(sideSize, position) {
return position - sideSize / 2;
}

View File

@ -0,0 +1,228 @@
<?xml version="1.0" encoding="UTF-8"?>
<schemalist>
<schema path="/org/gnome/shell/extensions/coverflowalttab/" id="org.gnome.shell.extensions.coverflowalttab">
<key type="b" name="hide-panel">
<default>true</default>
<summary>Hide the panel when showing coverflow</summary>
<description>Whether to show or hide the panel when CoverflowAltTab is active.</description>
</key>
<key type="b" name="enforce-primary-monitor">
<default>false</default>
<summary>Always show the switcher on the primary monitor</summary>
<description>Show the switcher on the primary monitor instead of detecting the active monitor.</description>
</key>
<key type="d" name="animation-time">
<range min="0.01" max="60"/>
<default>0.2</default>
<summary>The duration of coverflow animations in ms</summary>
<description>Define the duration of the animations.</description>
</key>
<key type="d" name="dim-factor">
<range min="0" max="1"/>
<default>1</default>
<summary>Dim factor for background</summary>
<description>Define dimming of the background. Bigger means darker. </description>
</key>
<key type="s" name="position">
<choices>
<choice value="Top"/>
<choice value="Bottom"/>
</choices>
<default>"Bottom"</default>
<summary>Position of icon and window title</summary>
<description>Whether the icon and window title should be placed below or above the window preview.</description>
</key>
<key type="i" name="offset">
<range min="-500" max="500"/>
<default>0</default>
<summary>Set a vertical offset</summary>
<description>Set a vertical offset. Positive values move the whole Coverflow up, negative down.</description>
</key>
<key type="s" name="icon-style">
<choices>
<choice value="Classic"/>
<choice value="Overlay"/>
<choice value="Attached"/>
</choices>
<default>"Classic"</default>
<summary>Icon style</summary>
<description>Whether the application icon should be displayed next to the title label or as an overlay.</description>
</key>
<key type="d" name="overlay-icon-opacity">
<range min="0" max="1"/>
<default>1</default>
<summary>The opacity of the overlay icon</summary>
<description>Define the opacity of the overlay icon.</description>
</key>
<key type="d" name="overlay-icon-size">
<range min="16" max="1024"/>
<default>128</default>
<summary>The icon size.</summary>
<description>Size in pixels.</description>
</key>
<key type="s" name="switcher-style">
<choices>
<choice value="Coverflow"/>
<choice value="Timeline"/>
</choices>
<default>"Coverflow"</default>
<summary>Switcher style</summary>
<description>The switcher display style.</description>
</key>
<key type="s" name="easing-function">
<choices>
<choice value="ease-linear"/>
<choice value="ease-in-quad"/>
<choice value="ease-out-quad"/>
<choice value="ease-in-out-quad"/>
<choice value="ease-in-cubic"/>
<choice value="ease-out-cubic"/>
<choice value="ease-in-out-cubic"/>
<choice value="ease-in-quart"/>
<choice value="ease-out-quart"/>
<choice value="ease-in-out-quart"/>
<choice value="ease-in-quint"/>
<choice value="ease-out-quint"/>
<choice value="ease-in-out-quint"/>
<choice value="ease-in-sine"/>
<choice value="ease-out-sine"/>
<choice value="ease-in-out-sine"/>
<choice value="ease-in-expo"/>
<choice value="ease-out-expo"/>
<choice value="ease-in-out-expo"/>
<choice value="ease-in-circ"/>
<choice value="ease-out-circ"/>
<choice value="ease-in-out-circ"/>
<choice value="ease-in-back"/>
<choice value="ease-out-back"/>
<choice value="ease-in-out-back"/>
<choice value="ease-in-elastic"/>
<choice value="ease-out-elastic"/>
<choice value="ease-in-out-elastic"/>
<choice value="ease-in-bounce"/>
<choice value="ease-out-bounce"/>
<choice value="ease-in-out-bounce"/>
<choice value="random"/>
</choices>
<default>"ease-out-cubic"</default>
<summary>Easing function used in animations</summary>
<description>Use this easing function for animations.</description>
</key>
<key type="s" name="current-workspace-only">
<choices>
<choice value="current"/>
<choice value="all"/>
<choice value="all-currentfirst"/>
</choices>
<default>'current'</default>
<summary>Show windows from current workspace only</summary>
<description>Whether to show all windows or windows from current workspace only.</description>
</key>
<key type="b" name="switch-per-monitor">
<default>false</default>
<summary>Per monitor window switch</summary>
<description>Switch between windows on current monitor (monitor with the mouse cursor)</description>
</key>
<key type="b" name="icon-has-shadow">
<default>false</default>
<summary>Icon has shadow switch</summary>
<description>Whether or not the icons have a drop shadow.</description>
</key>
<key type="b" name="randomize-animation-times">
<default>false</default>
<summary>Randomize animation times switch</summary>
<description>Whether or not to have each animation take a different, randomized time to complete.</description>
</key>
<key type="d" name="preview-to-monitor-ratio">
<range min="0" max="1"/>
<default>0.5</default>
<summary>The maximum ratio of the preview dimensions with the monitor dimensions</summary>
<description>Define the ratio of the preview to monitor sizes.</description>
</key>
<key type="d" name="preview-scaling-factor">
<range min="0" max="1"/>
<default>0.8</default>
<summary>In Coverflow Switcher, scales the previews as they spread out to the sides in </summary>
<description>Define the scale factor successively applied each step away from the current preview.</description>
</key>
<key type="b" name="bind-to-switch-applications">
<default>true</default>
<summary>Bind to 'switch-applications' keybinding</summary>
<description>Whether or not to bind to the 'switch-applications' keybinding.</description>
</key>
<key type="b" name="bind-to-switch-windows">
<default>true</default>
<summary>Bind to 'switch-windows' keybinding</summary>
<description>Whether or not to bind to the 'switch-windows' keybinding.</description>
</key>
<key type="b" name="highlight-mouse-over">
<default>false</default>
<summary>Highlight window under mouse</summary>
<description>Whether or not to draw some flare on the window under the mouse so you know.</description>
</key>
<key type="b" name="raise-mouse-over">
<default>true</default>
<summary>Raise window under mouse</summary>
<description>Whether or not to raise the window under the mouse above others.</description>
</key>
<key type="s" name="perspective-correction-method">
<choices>
<choice value="None"/>
<choice value="Move Camera"/>
<choice value="Adjust Angles"/>
</choices>
<default>"Move Camera"</default>
<summary>Method to correct off-center monitor perspective</summary>
<description>Way to make off-center monitor switcher to look like centered.</description>
</key>
<key type="d" name="desaturate-factor">
<default>0.0</default>
<range min="0" max="1"/>
<summary>Amount to Desaturate the Background Application Switcher</summary>
<description>0 for no desaturation, 1 for total desaturation.</description>
</key>
<key type="i" name="blur-radius">
<default>0</default>
<range min="0" max="20"/>
<summary>Radius of Blur Applied to the Background Application Switcher</summary>
<description>The bigger, the blurrier.</description>
</key>
<key type="s" name="switcher-looping-method">
<choices>
<choice value="Flip Stack"/>
<choice value="Carousel"/>
</choices>
<default>"Flip Stack"</default>
<summary>How the windows cycle through the coverflow</summary>
</key>
<key type="b" name="switch-application-behaves-like-switch-windows">
<default>false</default>
<summary>The application-switcher keybinding action behaves the same as the window-switcher</summary>
</key>
<key type="b" name="use-tint">
<default>true</default>
<summary>Whether to Use a Tint Color on the Background Application Switcher</summary>
</key>
<key type="(ddd)" name="tint-color">
<default>(0.,0.,0.)</default>
<summary></summary>
</key>
<key type="(ddd)" name="switcher-background-color">
<default>(0.,0.,0.)</default>
<summary></summary>
</key>
<key type="d" name="tint-blend">
<default>0.0</default>
<range min="0" max="1"/>
<summary>Amount to Blend Tint Color</summary>
</key>
<key type="b" name="use-glitch-effect">
<default>false</default>
<summary>Use a "glitch effect" on the background application switcher</summary>
</key>
<key type="b" name="invert-swipes">
<default>false</default>
<summary>Swipe content instead of view</summary>
</key>
</schema>
</schemalist>

View File

@ -0,0 +1,786 @@
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
/* exported SwipeTracker */
import Clutter from 'gi://Clutter';
import Gio from 'gi://Gio';
import Mtk from 'gi://Mtk';
import GObject from 'gi://GObject';
import * as Params from 'resource:///org/gnome/shell/misc/params.js';
// FIXME: ideally these values matches physical touchpad size. We can get the
// correct values for gnome-shell specifically, since mutter uses libinput
// directly, but GTK apps cannot get it, so use an arbitrary value so that
// it's consistent with apps.
const TOUCHPAD_BASE_HEIGHT = 300;
const TOUCHPAD_BASE_WIDTH = 400;
const EVENT_HISTORY_THRESHOLD_MS = 150;
const SCROLL_MULTIPLIER = 10;
const MIN_ANIMATION_DURATION = 100;
const MAX_ANIMATION_DURATION = 400;
const VELOCITY_THRESHOLD_TOUCH = 0.3;
const VELOCITY_THRESHOLD_TOUCHPAD = 0.6;
const DECELERATION_TOUCH = 0.998;
const DECELERATION_TOUCHPAD = 0.997;
const VELOCITY_CURVE_THRESHOLD = 2;
const DECELERATION_PARABOLA_MULTIPLIER = 0.35;
const DRAG_THRESHOLD_DISTANCE = 16;
// Derivative of easeOutCubic at t=0
const DURATION_MULTIPLIER = 3;
const ANIMATION_BASE_VELOCITY = 0.002;
const EPSILON = 0.005;
const GESTURE_FINGER_COUNT = 3;
const State = {
NONE: 0,
SCROLLING: 1,
};
const TouchpadState = {
NONE: 0,
PENDING: 1,
HANDLING: 2,
IGNORED: 3,
};
const EventHistory = class {
constructor() {
this.reset();
}
reset() {
this._data = [];
}
trim(time) {
const thresholdTime = time - EVENT_HISTORY_THRESHOLD_MS;
const index = this._data.findIndex(r => r.time >= thresholdTime);
this._data.splice(0, index);
}
append(time, delta) {
this.trim(time);
this._data.push({ time, delta });
}
calculateVelocity() {
if (this._data.length < 2)
return 0;
const firstTime = this._data[0].time;
const lastTime = this._data[this._data.length - 1].time;
if (firstTime === lastTime)
return 0;
const totalDelta = this._data.slice(1).map(a => a.delta).reduce((a, b) => a + b);
const period = lastTime - firstTime;
return totalDelta / period;
}
};
const TouchpadSwipeGesture = GObject.registerClass({
Properties: {
'enabled': GObject.ParamSpec.boolean(
'enabled', 'enabled', 'enabled',
GObject.ParamFlags.READWRITE,
true),
'orientation': GObject.ParamSpec.enum(
'orientation', 'orientation', 'orientation',
GObject.ParamFlags.READWRITE,
Clutter.Orientation, Clutter.Orientation.HORIZONTAL),
},
Signals: {
'begin': { param_types: [GObject.TYPE_UINT, GObject.TYPE_DOUBLE, GObject.TYPE_DOUBLE] },
'update': { param_types: [GObject.TYPE_UINT, GObject.TYPE_DOUBLE, GObject.TYPE_DOUBLE] },
'end': { param_types: [GObject.TYPE_UINT, GObject.TYPE_DOUBLE] },
},
}, class TouchpadSwipeGesture extends GObject.Object {
_init(allowedModes) {
super._init();
this._allowedModes = allowedModes;
this._state = TouchpadState.NONE;
this._cumulativeX = 0;
this._cumulativeY = 0;
this._touchpadSettings = new Gio.Settings({
schema_id: 'org.gnome.desktop.peripherals.touchpad',
});
global.stage.connectObject(
'captured-event::touchpad', this._handleEvent.bind(this), this);
}
_handleEvent(actor, event) {
if (event.type() !== Clutter.EventType.TOUCHPAD_SWIPE)
return Clutter.EVENT_PROPAGATE;
if (event.get_gesture_phase() === Clutter.TouchpadGesturePhase.BEGIN)
this._state = TouchpadState.NONE;
if (event.get_touchpad_gesture_finger_count() !== GESTURE_FINGER_COUNT)
return Clutter.EVENT_PROPAGATE;
/* if ((this._allowedModes & Main.actionMode) === 0)
return Clutter.EVENT_PROPAGATE; */
if (!this.enabled)
return Clutter.EVENT_PROPAGATE;
if (this._state === TouchpadState.IGNORED)
return Clutter.EVENT_PROPAGATE;
let time = event.get_time();
const [x, y] = event.get_coords();
const [dx, dy] = event.get_gesture_motion_delta_unaccelerated();
if (this._state === TouchpadState.NONE) {
if (dx === 0 && dy === 0)
return Clutter.EVENT_PROPAGATE;
this._cumulativeX = 0;
this._cumulativeY = 0;
this._state = TouchpadState.PENDING;
}
if (this._state === TouchpadState.PENDING) {
this._cumulativeX += dx;
this._cumulativeY += dy;
const cdx = this._cumulativeX;
const cdy = this._cumulativeY;
const distance = Math.sqrt(cdx * cdx + cdy * cdy);
if (distance >= DRAG_THRESHOLD_DISTANCE) {
const gestureOrientation = Math.abs(cdx) > Math.abs(cdy)
? Clutter.Orientation.HORIZONTAL
: Clutter.Orientation.VERTICAL;
this._cumulativeX = 0;
this._cumulativeY = 0;
if (gestureOrientation === this.orientation) {
this._state = TouchpadState.HANDLING;
this.emit('begin', time, x, y);
} else {
this._state = TouchpadState.IGNORED;
return Clutter.EVENT_PROPAGATE;
}
} else {
return Clutter.EVENT_PROPAGATE;
}
}
const vertical = this.orientation === Clutter.Orientation.VERTICAL;
let delta = vertical ? dy : dx;
const distance = vertical ? TOUCHPAD_BASE_HEIGHT : TOUCHPAD_BASE_WIDTH;
switch (event.get_gesture_phase()) {
case Clutter.TouchpadGesturePhase.BEGIN:
case Clutter.TouchpadGesturePhase.UPDATE:
if (this._touchpadSettings.get_boolean('natural-scroll'))
delta = -delta;
this.emit('update', time, delta, distance);
break;
case Clutter.TouchpadGesturePhase.END:
case Clutter.TouchpadGesturePhase.CANCEL:
this.emit('end', time, distance);
this._state = TouchpadState.NONE;
break;
}
return this._state === TouchpadState.HANDLING
? Clutter.EVENT_STOP
: Clutter.EVENT_PROPAGATE;
}
destroy() {
global.stage.disconnectObject(this);
}
});
const TouchSwipeGesture = GObject.registerClass({
Properties: {
'distance': GObject.ParamSpec.double(
'distance', 'distance', 'distance',
GObject.ParamFlags.READWRITE,
0, Infinity, 0),
'orientation': GObject.ParamSpec.enum(
'orientation', 'orientation', 'orientation',
GObject.ParamFlags.READWRITE,
Clutter.Orientation, Clutter.Orientation.HORIZONTAL),
},
Signals: {
'begin': { param_types: [GObject.TYPE_UINT, GObject.TYPE_DOUBLE, GObject.TYPE_DOUBLE] },
'update': { param_types: [GObject.TYPE_UINT, GObject.TYPE_DOUBLE, GObject.TYPE_DOUBLE] },
'end': { param_types: [GObject.TYPE_UINT, GObject.TYPE_DOUBLE] },
'cancel': { param_types: [GObject.TYPE_UINT, GObject.TYPE_DOUBLE] },
},
}, class TouchSwipeGesture extends Clutter.GestureAction {
_init(allowedModes, nTouchPoints, thresholdTriggerEdge) {
super._init();
this.set_n_touch_points(nTouchPoints);
this.set_threshold_trigger_edge(thresholdTriggerEdge);
this._allowedModes = allowedModes;
this._distance = global.screen_height;
this._lastPosition = 0;
}
get distance() {
return this._distance;
}
set distance(distance) {
if (this._distance === distance)
return;
this._distance = distance;
this.notify('distance');
}
vfunc_gesture_prepare(actor) {
if (!super.vfunc_gesture_prepare(actor))
return false;
/* if ((this._allowedModes & Main.actionMode) === 0)
return false; */
let time = this.get_last_event(0).get_time();
let [xPress, yPress] = this.get_press_coords(0);
let [x, y] = this.get_motion_coords(0);
const [xDelta, yDelta] = [x - xPress, y - yPress];
const swipeOrientation = Math.abs(xDelta) > Math.abs(yDelta)
? Clutter.Orientation.HORIZONTAL : Clutter.Orientation.VERTICAL;
if (swipeOrientation !== this.orientation)
return false;
this._lastPosition =
this.orientation === Clutter.Orientation.VERTICAL ? y : x;
this.emit('begin', time, xPress, yPress);
return true;
}
vfunc_gesture_progress(_actor) {
let [x, y] = this.get_motion_coords(0);
let pos = this.orientation === Clutter.Orientation.VERTICAL ? y : x;
let delta = pos - this._lastPosition;
this._lastPosition = pos;
let time = this.get_last_event(0).get_time();
this.emit('update', time, -delta, this._distance);
return true;
}
vfunc_gesture_end(_actor) {
let time = this.get_last_event(0).get_time();
this.emit('end', time, this._distance);
}
vfunc_gesture_cancel(_actor) {
let time = Clutter.get_current_event_time();
this.emit('cancel', time, this._distance);
}
});
const ScrollGesture = GObject.registerClass({
Properties: {
'enabled': GObject.ParamSpec.boolean(
'enabled', 'enabled', 'enabled',
GObject.ParamFlags.READWRITE,
true),
'orientation': GObject.ParamSpec.enum(
'orientation', 'orientation', 'orientation',
GObject.ParamFlags.READWRITE,
Clutter.Orientation, Clutter.Orientation.HORIZONTAL),
'scroll-modifiers': GObject.ParamSpec.flags(
'scroll-modifiers', 'scroll-modifiers', 'scroll-modifiers',
GObject.ParamFlags.READWRITE,
Clutter.ModifierType, 0),
},
Signals: {
'begin': { param_types: [GObject.TYPE_UINT, GObject.TYPE_DOUBLE, GObject.TYPE_DOUBLE] },
'update': { param_types: [GObject.TYPE_UINT, GObject.TYPE_DOUBLE, GObject.TYPE_DOUBLE] },
'end': { param_types: [GObject.TYPE_UINT, GObject.TYPE_DOUBLE] },
},
}, class ScrollGesture extends GObject.Object {
_init(actor, allowedModes, inverted=false) {
super._init();
this._inverted = inverted;
this._allowedModes = allowedModes;
this._began = false;
this._enabled = true;
actor.connect('scroll-event', this._handleEvent.bind(this));
}
get enabled() {
return this._enabled;
}
set enabled(enabled) {
if (this._enabled === enabled)
return;
this._enabled = enabled;
this._began = false;
this.notify('enabled');
}
canHandleEvent(event) {
if (event.type() !== Clutter.EventType.SCROLL)
return false;
if (event.get_scroll_source() !== Clutter.ScrollSource.FINGER &&
event.get_source_device().get_device_type() !== Clutter.InputDeviceType.TOUCHPAD_DEVICE)
return false;
if (!this.enabled)
return false;
if (!this._began && this.scrollModifiers !== 0 &&
(event.get_state() & this.scrollModifiers) === 0)
return false;
return true;
}
_handleEvent(actor, event) {
if (!this.canHandleEvent(event))
return Clutter.EVENT_PROPAGATE;
if (event.get_scroll_direction() !== Clutter.ScrollDirection.SMOOTH)
return Clutter.EVENT_PROPAGATE;
const vertical = this.orientation === Clutter.Orientation.VERTICAL;
const distance = vertical ? TOUCHPAD_BASE_HEIGHT : TOUCHPAD_BASE_WIDTH;
let time = event.get_time();
let [dx, dy] = event.get_scroll_delta();
if (this._inverted) {
dx = -dx; dy = -dy;
}
if (dx === 0 && dy === 0) {
this.emit('end', time, distance);
this._began = false;
return Clutter.EVENT_STOP;
}
if (!this._began) {
let [x, y] = event.get_coords();
this.emit('begin', time, x, y);
this._began = true;
}
const delta = (vertical ? dy : dx) * SCROLL_MULTIPLIER;
this.emit('update', time, delta, distance);
return Clutter.EVENT_STOP;
}
});
// USAGE:
//
// To correctly implement the gesture, there must be handlers for the following
// signals:
//
// begin(tracker, monitor)
// The handler should check whether a deceleration animation is currently
// running. If it is, it should stop the animation (without resetting
// progress). Then it should call:
// tracker.confirmSwipe(distance, snapPoints, currentProgress, cancelProgress)
// If it's not called, the swipe would be ignored.
// The parameters are:
// * distance: the page size;
// * snapPoints: an (sorted with ascending order) array of snap points;
// * currentProgress: the current progress;
// * cancelprogress: a non-transient value that would be used if the gesture
// is cancelled.
// If no animation was running, currentProgress and cancelProgress should be
// same. The handler may set 'orientation' property here.
//
// update(tracker, progress)
// The handler should set the progress to the given value.
//
// end(tracker, duration, endProgress)
// The handler should animate the progress to endProgress. If endProgress is
// 0, it should do nothing after the animation, otherwise it should change the
// state, e.g. change the current page or switch workspace.
// NOTE: duration can be 0 in some cases, in this case it should finish
// instantly.
/** A class for handling swipe gestures */
export const MySwipeTracker = GObject.registerClass({
Properties: {
'enabled': GObject.ParamSpec.boolean(
'enabled', 'enabled', 'enabled',
GObject.ParamFlags.READWRITE,
true),
'orientation': GObject.ParamSpec.enum(
'orientation', 'orientation', 'orientation',
GObject.ParamFlags.READWRITE,
Clutter.Orientation, Clutter.Orientation.HORIZONTAL),
'distance': GObject.ParamSpec.double(
'distance', 'distance', 'distance',
GObject.ParamFlags.READWRITE,
0, Infinity, 0),
'allow-long-swipes': GObject.ParamSpec.boolean(
'allow-long-swipes', 'allow-long-swipes', 'allow-long-swipes',
GObject.ParamFlags.READWRITE,
false),
'scroll-modifiers': GObject.ParamSpec.flags(
'scroll-modifiers', 'scroll-modifiers', 'scroll-modifiers',
GObject.ParamFlags.READWRITE,
Clutter.ModifierType, 0),
},
Signals: {
'begin': { param_types: [GObject.TYPE_UINT] },
'update': { param_types: [GObject.TYPE_DOUBLE] },
'end': { param_types: [GObject.TYPE_UINT64, GObject.TYPE_DOUBLE] },
},
}, class MySwipeTracker extends GObject.Object {
_init(actor, orientation, allowedModes, params, inverted=false) {
super._init();
params = Params.parse(params, { allowDrag: true, allowScroll: true });
this.orientation = orientation;
this._inverted = inverted;
this._allowedModes = allowedModes;
this._enabled = true;
this._distance = global.screen_height;
this._history = new EventHistory();
this._reset();
this._touchpadGesture = new TouchpadSwipeGesture(allowedModes);
this._touchpadGesture.connect('begin', this._beginGesture.bind(this));
this._touchpadGesture.connect('update', this._updateGesture.bind(this));
this._touchpadGesture.connect('end', this._endTouchpadGesture.bind(this));
this.bind_property('enabled', this._touchpadGesture, 'enabled', 0);
this.bind_property('orientation', this._touchpadGesture, 'orientation',
GObject.BindingFlags.SYNC_CREATE);
this._touchGesture = new TouchSwipeGesture(allowedModes,
GESTURE_FINGER_COUNT,
Clutter.GestureTriggerEdge.AFTER);
this._touchGesture.connect('begin', this._beginTouchSwipe.bind(this));
this._touchGesture.connect('update', this._updateGesture.bind(this));
this._touchGesture.connect('end', this._endTouchGesture.bind(this));
this._touchGesture.connect('cancel', this._cancelTouchGesture.bind(this));
this.bind_property('enabled', this._touchGesture, 'enabled', 0);
this.bind_property('orientation', this._touchGesture, 'orientation',
GObject.BindingFlags.SYNC_CREATE);
this.bind_property('distance', this._touchGesture, 'distance', 0);
global.stage.add_action_full('swipe', Clutter.EventPhase.CAPTURE, this._touchGesture);
if (params.allowDrag) {
this._dragGesture = new TouchSwipeGesture(allowedModes, 1,
Clutter.GestureTriggerEdge.AFTER);
this._dragGesture.connect('begin', this._beginGesture.bind(this));
this._dragGesture.connect('update', this._updateGesture.bind(this));
this._dragGesture.connect('end', this._endTouchGesture.bind(this));
this._dragGesture.connect('cancel', this._cancelTouchGesture.bind(this));
this.bind_property('enabled', this._dragGesture, 'enabled', 0);
this.bind_property('orientation', this._dragGesture, 'orientation',
GObject.BindingFlags.SYNC_CREATE);
this.bind_property('distance', this._dragGesture, 'distance', 0);
actor.add_action_full('drag', Clutter.EventPhase.CAPTURE, this._dragGesture);
} else {
this._dragGesture = null;
}
if (params.allowScroll) {
this._scrollGesture = new ScrollGesture(actor, allowedModes, this._inverted);
this._scrollGesture.connect('begin', this._beginGesture.bind(this));
this._scrollGesture.connect('update', this._updateGesture.bind(this));
this._scrollGesture.connect('end', this._endTouchpadGesture.bind(this));
this.bind_property('enabled', this._scrollGesture, 'enabled', 0);
this.bind_property('orientation', this._scrollGesture, 'orientation',
GObject.BindingFlags.SYNC_CREATE);
this.bind_property('scroll-modifiers',
this._scrollGesture, 'scroll-modifiers', 0);
} else {
this._scrollGesture = null;
}
}
/**
* canHandleScrollEvent:
* @param {Clutter.Event} scrollEvent: an event to check
* @returns {bool} whether the event can be handled by the tracker
*
* This function can be used to combine swipe gesture and mouse
* scrolling.
*/
canHandleScrollEvent(scrollEvent) {
if (!this.enabled || this._scrollGesture === null) {
return false;
}
return this._scrollGesture.canHandleEvent(scrollEvent);
}
get enabled() {
return this._enabled;
}
set enabled(enabled) {
if (this._enabled === enabled)
return;
this._enabled = enabled;
if (!enabled && this._state === State.SCROLLING)
this._interrupt();
this.notify('enabled');
}
get distance() {
return this._distance;
}
set distance(distance) {
if (this._distance === distance)
return;
this._distance = distance;
this.notify('distance');
}
_reset() {
this._state = State.NONE;
this._snapPoints = [];
this._initialProgress = 0;
this._cancelProgress = 0;
this._prevOffset = 0;
this._progress = 0;
this._cancelled = false;
this._history.reset();
}
_interrupt() {
this.emit('end', 0, this._cancelProgress);
this._reset();
}
_beginTouchSwipe(gesture, time, x, y) {
if (this._dragGesture)
this._dragGesture.cancel();
this._beginGesture(gesture, time, x, y);
}
_beginGesture(gesture, time, x, y) {
if (this._state === State.SCROLLING)
return;
this._history.append(time, 0);
let rect = new Mtk.Rectangle({ x, y, width: 1, height: 1 });
let monitor = global.display.get_monitor_index_for_rect(rect);
this.emit('begin', monitor);
}
_findClosestPoint(pos) {
const distances = this._snapPoints.map(x => Math.abs(x - pos));
const min = Math.min(...distances);
return distances.indexOf(min);
}
_findNextPoint(pos) {
return this._snapPoints.findIndex(p => p >= pos);
}
_findPreviousPoint(pos) {
const reversedIndex = this._snapPoints.slice().reverse().findIndex(p => p <= pos);
return this._snapPoints.length - 1 - reversedIndex;
}
_findPointForProjection(pos, velocity) {
const initial = this._findClosestPoint(this._initialProgress);
const prev = this._findPreviousPoint(pos);
const next = this._findNextPoint(pos);
if ((velocity > 0 ? prev : next) === initial)
return velocity > 0 ? next : prev;
return this._findClosestPoint(pos);
}
_getBounds(pos) {
if (this.allowLongSwipes)
return [this._snapPoints[0], this._snapPoints[this._snapPoints.length - 1]];
const closest = this._findClosestPoint(pos);
let prev, next;
if (Math.abs(this._snapPoints[closest] - pos) < EPSILON) {
prev = next = closest;
} else {
prev = this._findPreviousPoint(pos);
next = this._findNextPoint(pos);
}
const lowerIndex = Math.max(prev - 1, 0);
const upperIndex = Math.min(next + 1, this._snapPoints.length - 1);
return [this._snapPoints[lowerIndex], this._snapPoints[upperIndex]];
}
_updateGesture(gesture, time, delta, distance) {
if (this._state !== State.SCROLLING)
return;
if (!this.enabled) {
this._interrupt();
return;
}
if (this.orientation === Clutter.Orientation.HORIZONTAL &&
Clutter.get_default_text_direction() === Clutter.TextDirection.RTL)
delta = -delta;
this._progress += delta / distance;
this._history.append(time, delta);
this._progress = Math.clamp(this._progress, ...this._getBounds(this._initialProgress));
this.emit('update', this._progress);
}
_getEndProgress(velocity, distance, isTouchpad) {
if (this._cancelled)
return this._cancelProgress;
const threshold = isTouchpad ? VELOCITY_THRESHOLD_TOUCHPAD : VELOCITY_THRESHOLD_TOUCH;
if (Math.abs(velocity) < threshold)
return this._snapPoints[this._findClosestPoint(this._progress)];
const decel = isTouchpad ? DECELERATION_TOUCHPAD : DECELERATION_TOUCH;
const slope = decel / (1.0 - decel) / 1000.0;
let pos;
if (Math.abs(velocity) > VELOCITY_CURVE_THRESHOLD) {
const c = slope / 2 / DECELERATION_PARABOLA_MULTIPLIER;
const x = Math.abs(velocity) - VELOCITY_CURVE_THRESHOLD + c;
pos = slope * VELOCITY_CURVE_THRESHOLD +
DECELERATION_PARABOLA_MULTIPLIER * x * x -
DECELERATION_PARABOLA_MULTIPLIER * c * c;
} else {
pos = Math.abs(velocity) * slope;
}
pos = pos * Math.sign(velocity) + this._progress;
pos = Math.clamp(pos, ...this._getBounds(this._initialProgress));
const index = this._findPointForProjection(pos, velocity);
return this._snapPoints[index];
}
_endTouchGesture(_gesture, time, distance) {
this._endGesture(time, distance, false);
}
_endTouchpadGesture(_gesture, time, distance) {
this._endGesture(time, distance, true);
}
_endGesture(time, distance, isTouchpad) {
if (this._state !== State.SCROLLING)
return;
/* if ((this._allowedModes & Main.actionMode) === 0 || !this.enabled) {
this._interrupt();
return;
} */
this._history.trim(time);
let velocity = this._history.calculateVelocity();
const endProgress = this._getEndProgress(velocity, distance, isTouchpad);
velocity /= distance;
if ((endProgress - this._progress) * velocity <= 0)
velocity = ANIMATION_BASE_VELOCITY;
const nPoints = Math.max(1, Math.ceil(Math.abs(this._progress - endProgress)));
const maxDuration = MAX_ANIMATION_DURATION * Math.log2(1 + nPoints);
let duration = Math.abs((this._progress - endProgress) / velocity * DURATION_MULTIPLIER);
if (duration > 0)
duration = Math.clamp(duration, MIN_ANIMATION_DURATION, maxDuration);
this._reset();
this.emit('end', duration, endProgress);
}
_cancelTouchGesture(_gesture, time, distance) {
if (this._state !== State.SCROLLING)
return;
this._cancelled = true;
this._endGesture(time, distance, false);
}
/**
* confirmSwipe:
* @param {number} distance: swipe distance in pixels
* @param {number[]} snapPoints:
* An array of snap points, sorted in ascending order
* @param {number} currentProgress: initial progress value
* @param {number} cancelProgress: the value to be used on cancelling
*
* Confirms a swipe. User has to call this in 'begin' signal handler,
* otherwise the swipe wouldn't start. If there's an animation running,
* it should be stopped first.
*
* @cancel_progress must always be a snap point, or a value matching
* some other non-transient state.
*/
confirmSwipe(distance, snapPoints, currentProgress, cancelProgress) {
this.distance = distance;
this._snapPoints = snapPoints;
this._initialProgress = currentProgress;
this._progress = currentProgress;
this._cancelProgress = cancelProgress;
this._state = State.SCROLLING;
}
destroy() {
if (this._touchpadGesture) {
this._touchpadGesture.destroy();
delete this._touchpadGesture;
}
if (this._touchGesture) {
global.stage.remove_action(this._touchGesture);
delete this._touchGesture;
}
}
});

View File

@ -0,0 +1,293 @@
/*
This file is part of CoverflowAltTab.
CoverflowAltTab is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
CoverflowAltTab is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with CoverflowAltTab. If not, see <http://www.gnu.org/licenses/>.
*/
/* CoverflowAltTab::TimelineSwitcher:
*
* Extends CoverflowAltTab::Switcher, switching tabs using a timeline
*/
import {Switcher} from './switcher.js';
import {Preview, Placement, findUpperLeftFromCenter} from './preview.js'
let TRANSITION_TYPE;
let IN_BOUNDS_TRANSITION_TYPE;
const TILT_ANGLE = 45;
export class TimelineSwitcher extends Switcher {
constructor(...args) {
super(...args);
TRANSITION_TYPE = 'userChoice';
IN_BOUNDS_TRANSITION_TYPE = 'easeInOutQuint';
}
_createPreviews() {
let monitor = this._updateActiveMonitor();
let currentWorkspace = this._manager.workspace_manager.get_active_workspace();
this._previewsCenterPosition = {
x: this.actor.width / 2,
y: this.actor.height / 2 + this._settings.offset
};
for (let windowActor of global.get_window_actors()) {
let metaWin = windowActor.get_meta_window();
let compositor = metaWin.get_compositor_private();
if (compositor) {
let texture = compositor.get_texture();
let width, height;
if (texture.get_size) {
[width, height] = texture.get_size()
} else {
let preferred_size_ok;
[preferred_size_ok, width, height] = texture.get_preferred_size();
}
let previewScale = this._settings.preview_to_monitor_ratio;
let scale = 1.0;
let previewWidth = this.actor.width * previewScale;
let previewHeight = this.actor.height * previewScale;
if (width > previewWidth || height > previewHeight)
scale = Math.min(previewWidth / width, previewHeight / height);
let preview = new Preview(metaWin, this, {
opacity: (!metaWin.minimized && metaWin.get_workspace() == currentWorkspace || metaWin.is_on_all_workspaces()) ? 255: 0,
source: texture.get_size ? texture : compositor,
reactive: true,
name: metaWin.title,
x: (metaWin.minimized ? -(compositor.x + compositor.width / 2) :
compositor.x) - monitor.x,
y: (metaWin.minimized ? -(compositor.y + compositor.height / 2) :
compositor.y) - monitor.y,
rotation_angle_y: 0,
width: width,
height: height,
});
preview.scale = scale;
preview.target_x = findUpperLeftFromCenter(preview.width * preview.scale,
this._previewsCenterPosition.x);
preview.target_y = findUpperLeftFromCenter(preview.height,
this._previewsCenterPosition.y);
preview.set_pivot_point_placement(Placement.LEFT);
if (this._windows.includes(metaWin)) {
this._previews[this._windows.indexOf(metaWin)] = preview;
}
this._allPreviews.push(preview);
this.previewActor.add_child(preview);
preview.make_bottom_layer(this.previewActor);
}
}
}
_previewNext() {
this._setCurrentIndex((this._currentIndex + 1) % this._windows.length);
this._updatePreviews(false, 1);
}
_previewPrevious() {
this._setCurrentIndex((this._windows.length + this._currentIndex - 1) % this._windows.length);
this._updatePreviews(false, -1);
}
_updatePreviews(reorder_only=false, direction=0) {
if (this._previews == null || this._previews.length == 0)
return;
let animation_time = this._settings.animation_time * (this._settings.randomize_animation_times ? this._getRandomArbitrary(0.25, 1) : 1);
if (this._previews.length == 1) {
if (reorder_only) return;
let preview = this._previews[0];
this._manager.platform.tween(preview, {
x: preview.target_x,
y: preview.target_y,
scale_x: preview.scale,
scale_y: preview.scale,
scale_z: preview.scale,
time: animation_time / 2,
transition: TRANSITION_TYPE,
rotation_angle_y: TILT_ANGLE,
});
this._manager.platform.tween(preview, {
opacity: 255,
time: animation_time / 2,
transition: IN_BOUNDS_TRANSITION_TYPE,
onComplete: () => {
preview.set_reactive(true);
}
});
return;
}
for (let i = this._currentIndex; i < this._currentIndex + this._previews.length; i++) {
this._previews[i%this._previews.length].make_bottom_layer(this.previewActor);
}
if (reorder_only) return;
// preview windows
for (let [i, preview] of this._previews.entries()) {
animation_time = this._settings.animation_time * (this._settings.randomize_animation_times ? this._getRandomArbitrary(0.0001, 1) : 1);
let distance = (this._currentIndex > i) ? this._previews.length - this._currentIndex + i : i - this._currentIndex;
if (distance === this._previews.length - 1 && direction > 0) {
preview.__looping = true;
animation_time = this._settings.animation_time;
preview.make_top_layer(this.previewActor);
this._raiseIcons();
let scale = preview.scale * Math.pow(this._settings.preview_scaling_factor, -1);
this._manager.platform.tween(preview, {
x: preview.target_x + 150,
y: preview.target_y + 100,
time: animation_time / 2,
transition: TRANSITION_TYPE,
rotation_angle_y: TILT_ANGLE,
onCompleteParams: [preview, distance, animation_time],
onComplete: this._onFadeForwardComplete,
onCompleteScope: this,
});
this._manager.platform.tween(preview, {
opacity: 0,
scale_x: scale,
scale_y: scale,
scale_z: scale,
time: animation_time / 2,
transition: IN_BOUNDS_TRANSITION_TYPE,
});
} else if (distance === 0 && direction < 0) {
preview.__looping = true;
animation_time = this._settings.animation_time;
let scale = preview.scale * Math.pow(this._settings.preview_scaling_factor, this._previews.length);
preview.make_bottom_layer(this.previewActor);
this._manager.platform.tween(preview, {
time: animation_time / 2,
x: preview.target_x - Math.sqrt(this._previews.length) * 150,
y: preview.target_y - Math.sqrt(this._previews.length) * 100,
transition: TRANSITION_TYPE,
rotation_angle_y: TILT_ANGLE,
onCompleteParams: [preview, distance, animation_time],
onComplete: this._onFadeBackwardsComplete,
onCompleteScope: this,
});
this._manager.platform.tween(preview, {
time: animation_time / 2,
transition: IN_BOUNDS_TRANSITION_TYPE,
scale_x: scale,
scale_y: scale,
scale_x: scale,
opacity: 0,
});
} else {
let scale = preview.scale * Math.pow(this._settings.preview_scaling_factor, distance);//Math.max(preview.scale * ((20 - 2 * distance) / 20), 0);
let tweenparams = {
x: preview.target_x - Math.sqrt(distance) * 150,
y: preview.target_y - Math.sqrt(distance) * 100,
scale_x: scale,
scale_y: scale,
scale_z: scale,
time: animation_time,
rotation_angle_y: TILT_ANGLE,
transition: TRANSITION_TYPE,
onComplete: () => { preview.set_reactive(true); },
};
let opacitytweenparams = {
opacity: 255,
time: animation_time,
transition: IN_BOUNDS_TRANSITION_TYPE,
};
if (preview.__looping || preview.__finalTween)
preview.__finalTween = [tweenparams, opacitytweenparams];
else
this._manager.platform.tween(preview, tweenparams);
this._manager.platform.tween(preview, opacitytweenparams);
}
}
}
_onFadeBackwardsComplete(preview, distance, animation_time) {
preview.__looping = false;
preview.make_top_layer(this.previewActor);
this._raiseIcons();
preview.x = preview.target_x + 150;
preview.y = preview.target_y + 100;
let scale_start = preview.scale * Math.pow(this._settings.preview_scaling_factor, -1);
preview.scale_x = scale_start;
preview.scale_y = scale_start;
preview.scale_z = scale_start;
this._manager.platform.tween(preview, {
x: preview.target_x,
y: preview.target_y,
time: animation_time / 2,
transition: TRANSITION_TYPE,
onCompleteParams: [preview],
onComplete: this._onFinishMove,
onCompleteScope: this,
});
this._manager.platform.tween(preview, {
opacity: 255,
scale_x: preview.scale,
scale_y: preview.scale,
scale_z: preview.scale,
time: animation_time / 2,
transition: IN_BOUNDS_TRANSITION_TYPE,
});
}
_onFadeForwardComplete(preview, distance, animation_time) {
preview.__looping = false;
preview.make_bottom_layer(this.previewActor);
log(distance);
preview.x = preview.target_x - Math.sqrt(distance + 1) * 150;
preview.y = preview.target_y - Math.sqrt(distance + 1) * 100;
let scale_start = preview.scale * Math.pow(this._settings.preview_scaling_factor, distance + 1);
preview.scale_x = scale_start;
preview.scale_y = scale_start;
preview.scale_z = scale_start;
this._manager.platform.tween(preview, {
x: preview.target_x - Math.sqrt(distance) * 150,
y: preview.target_y - Math.sqrt(distance) * 100,
time: animation_time / 2,
transition: TRANSITION_TYPE,
onCompleteParams: [preview],
onComplete: this._onFinishMove,
onCompleteScope: this,
});
let scale_end = preview.scale * Math.pow(this._settings.preview_scaling_factor, distance);
this._manager.platform.tween(preview, {
opacity: 255,
scale_x: scale_end,
scale_y: scale_end,
scale_z: scale_end,
time: animation_time / 2,
transition: IN_BOUNDS_TRANSITION_TYPE,
});
}
_onFinishMove(preview) {
this._updatePreviews(true)
if (preview.__finalTween) {
for (let tween of preview.__finalTween) {
this._manager.platform.tween(preview, tween);
}
preview.__finalTween = null;
}
}
};

View File

@ -0,0 +1,56 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="16"
height="16"
version="1.1"
viewBox="0 0 16 16"
id="svg7"
sodipodi:docname="applications.svg"
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview9"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
showgrid="false"
inkscape:zoom="44.6875"
inkscape:cx="5.5944056"
inkscape:cy="8"
inkscape:window-width="1500"
inkscape:window-height="963"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg7" />
<defs
id="defs3">
<style
id="current-color-scheme"
type="text/css">.ColorScheme-Text { color:#363636; }</style>
</defs>
<path
d="M 2,1 C 0.892,1 0,1.892 0,3 v 9 c 0,1.108 0.892,2 2,2 h 12 c 1.108,0 2,-0.892 2,-2 V 3 C 16,1.892 15.108,1 14,1 Z m 0,1 h 12 c 0.554,0 1,0.446 1,1 H 1 C 1,2.446 1.446,2 2,2 Z M 1,3 h 14 v 9 c 0,0.554 -0.446,1 -1,1 H 2 C 1.446,13 1,12.554 1,12 Z"
style="fill:currentColor"
class="ColorScheme-Text"
id="path5"
sodipodi:nodetypes="sssssssssssccsccssssc" />
<g
id="g68"
transform="translate(0.3993007,0.15314685)">
<path
d="M 9.5,4 C 9.223,4 9,4.223 9,4.5 v 2 A 0.499,0.499 0 0 0 9.5,7 h 2 C 11.777,7 12,6.777 12,6.5 v -2 C 12,4.223 11.777,4 11.5,4 Z M 10,5 h 1 v 1 h -1 z"
fill="#363636"
id="path2" />
<path
d="M 4,6 C 3.446,6 3,6.446 3,7 v 3 c 0,0.554 0.446,1 1,1 h 3 c 0.554,0 1,-0.446 1,-1 V 7 C 8,6.446 7.554,6 7,6 Z m 0,1 h 3 v 3 H 4 Z"
fill="#363636"
id="path8" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
SPDX-FileCopyrightText: 2021 Romain Vigier <contact AT romainvigier.fr>
SPDX-License-Identifier: CC-BY-SA-4.0
-->
<svg width="16" height="16" version="1.1" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
<path d="m3.7578 2.0293a1 1 0 0 0-0.61523 0.45703l-3 5a1.0001 1.0001 0 0 0 0 1.0273l3 5a1 1 0 0 0 1.3711 0.34375 1 1 0 0 0 0.34375-1.3711l-2.6914-4.4863 2.6914-4.4863a1 1 0 0 0-0.34375-1.3711 1 1 0 0 0-0.75586-0.11328z"/>
<path d="m12.242 2.0293a1 1 0 0 0-0.75586 0.11328 1 1 0 0 0-0.34375 1.3711l2.6914 4.4863-2.6914 4.4863a1 1 0 0 0 0.34375 1.3711 1 1 0 0 0 1.3711-0.34375l3-5a1.0001 1.0001 0 0 0 0-1.0273l-3-5a1 1 0 0 0-0.61523-0.45703z"/>
<path d="m9.1953 2.0195a1 1 0 0 0-1.1758 0.78516l-2 10a1 1 0 0 0 0.78516 1.1758 1 1 0 0 0 1.1758-0.78516l2-10a1 1 0 0 0-0.78516-1.1758z"/>
</svg>

After

Width:  |  Height:  |  Size: 848 B

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
SPDX-FileCopyrightText: 2021 Romain Vigier <contact AT romainvigier.fr>
SPDX-License-Identifier: CC-BY-SA-4.0
-->
<svg width="16" height="16" version="1.1" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
<path d="m4.5 1c-2.4853 0-4.5 2.0147-4.5 4.5 0.0032498 1.2489 0.75291 2.3099 1.4414 3.2891 1.7318 2.463 6.5586 6.2109 6.5586 6.2109s4.8267-3.7479 6.5586-6.2109c0.68851-0.97919 1.4382-2.0402 1.4414-3.2891 0-2.4853-2.0147-4.5-4.5-4.5-2.5 0-3.101 2.001-3.5 2s-1-2-3.5-2z"/>
</svg>

After

Width:  |  Height:  |  Size: 535 B

View File

@ -0,0 +1,143 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
sodipodi:docname="coverflow-symbolic.svg"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
id="svg3466"
version="1.1"
viewBox="0 0 16.933333 16.933334"
height="64"
width="64"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview3468"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:document-units="mm"
showgrid="true"
units="px"
inkscape:zoom="2.8293902"
inkscape:cx="-31.808975"
inkscape:cy="124.23172"
inkscape:window-width="2560"
inkscape:window-height="1351"
inkscape:window-x="2560"
inkscape:window-y="53"
inkscape:window-maximized="1"
inkscape:current-layer="layer2"
inkscape:showpageshadow="0"
inkscape:deskcolor="#d1d1d1"
inkscape:lockguides="false">
<inkscape:grid
type="xygrid"
id="grid1030" />
</sodipodi:namedview>
<defs
id="defs3463">
<inkscape:perspective
sodipodi:type="inkscape:persp3d"
inkscape:vp_x="0 : 8.466667 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_z="16.933333 : 8.466667 : 1"
inkscape:persp3d-origin="8.4666665 : 5.6444447 : 1"
id="perspective4871" />
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect130490"
is_visible="true"
lpeversion="1"
satellites_param="F,0,0,1,0,2,0,1 @ F,0,0,1,0,2,0,1 @ F,0,0,1,0,2,0,1 @ F,0,0,1,0,2,0,1"
unit="px"
method="auto"
mode="F"
radius="2"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false"
nodesatellites_param="F,0,0,1,0,2,0,1 @ F,0,0,1,0,2,0,1 @ F,0,0,1,0,2,0,1 @ F,0,0,1,0,2,0,1" />
</defs>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1" />
<g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="Layer 2"
transform="matrix(0.99774183,0,0,0.77115443,0.71193726,0.91633828)">
<g
id="g2666"
transform="matrix(1.2594292,0,0,1.5615248,-2.0148681,-6.7303432)"
style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:0.559234;stroke-dasharray:none;stroke-opacity:1" />
<g
id="g27602"
transform="matrix(0.99277462,0,0,0.99277462,0.05616722,0.07606922)"
style="stroke-width:0.858316;stroke-dasharray:none">
<g
id="g2652"
style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:0.710035;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(1.0822937,0,0,1.3501739,-0.63816917,-4.4362993)">
<g
id="g1028"
transform="matrix(0.83956608,0,0,1.0356734,1.1973153,1.6060768)"
style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:0.761449;stroke-dasharray:none;stroke-opacity:1">
<g
id="g1034"
transform="matrix(1.0572353,0,0,-0.71455331,-21.791344,225.59896)"
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0.876068;stroke-dasharray:none;stroke-opacity:0">
<path
style="color:#000000;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0.876068;stroke-dasharray:none;stroke-opacity:0"
d="m 20.191152,299.66796 c -0.152119,0.002 -0.283123,0.0654 -0.371094,0.13281 -0.17594,0.13474 -0.242446,0.27997 -0.304687,0.42969 -0.124482,0.29943 -0.185547,0.65031 -0.185547,1.00585 v 6.3379 c 0,0.35554 0.06106,0.70642 0.185547,1.00585 0.06224,0.14972 0.128748,0.29495 0.304687,0.42969 0.08797,0.0674 0.218979,0.12916 0.371094,0.13086 0.152116,0.002 0.296554,-0.0626 0.392578,-0.13867 l 1.53125,-1.21094 c 0.232498,-0.18412 0.525391,-0.58185 0.525391,-1.125 v -4.52148 c 0,-0.54315 -0.292885,-0.94087 -0.525391,-1.125 l -1.53125,-1.21289 c -0.09602,-0.0761 -0.240459,-0.14038 -0.392578,-0.13867 z m 0.158203,1.1621 1.496146,0.93165 c 0.128075,0.10142 0.166015,0.091 0.166015,0.38281 v 4.52148 c 0,0.2918 -0.03793,0.27943 -0.166015,0.38086 l -1.496146,0.9336 c -0.0307,-0.13528 -0.07031,-0.2605 -0.07031,-0.40625 v -6.3379 c 0,-0.14532 0.03975,-0.27119 0.07031,-0.40625 z"
id="path14264"
sodipodi:nodetypes="cccsscccccsscccccssccssc" />
</g>
<g
id="g1038"
transform="matrix(1.0572353,0,0,-0.71455331,-21.795226,225.54112)"
style="fill:#000000;fill-opacity:0.965186;stroke:#000000;stroke-width:0.876068;stroke-dasharray:none;stroke-opacity:1">
<path
style="color:#000000;fill:#000000;fill-opacity:0.965186;stroke:#000000;stroke-width:0.876068;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:0"
d="m 35.850097,299.5658 c -0.152119,-0.002 -0.296555,0.0626 -0.392578,0.13867 l -1.53125,1.21289 c -0.232506,0.18413 -0.52539,0.58185 -0.52539,1.125 v 4.52148 c 0,0.54315 0.292893,0.94088 0.52539,1.125 l 1.53125,1.21094 c 0.09602,0.0761 0.240463,0.14038 0.392578,0.13867 0.152116,-0.002 0.285078,-0.0635 0.373047,-0.13086 0.175939,-0.13474 0.242447,-0.27997 0.304688,-0.42969 0.124483,-0.29943 0.183594,-0.65031 0.183594,-1.00585 v -6.3379 c 0,-0.35554 -0.05911,-0.70642 -0.183594,-1.00585 -0.06224,-0.14972 -0.128747,-0.29495 -0.304688,-0.42969 -0.08797,-0.0674 -0.220927,-0.13111 -0.373047,-0.13281 z m -0.158203,1.1621 c 0.03057,0.13508 0.07227,0.26089 0.07227,0.40625 v 6.3379 c 0,0.14579 -0.04155,0.27095 -0.07227,0.40625 l -1.460883,-0.9336 c -0.128084,-0.10143 -0.167969,-0.0891 -0.167969,-0.38086 v -4.52148 c 0,-0.29181 0.03989,-0.28139 0.167969,-0.38281 z"
id="path14266"
sodipodi:nodetypes="cccsscccccssccccssccsscc" />
</g>
<rect
style="display:inline;opacity:1;fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:0.761449;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:52.5354;stroke-opacity:1;paint-order:normal"
id="rect7847"
width="9.6742821"
height="6.4741611"
x="2.9982321"
y="4.8527794"
ry="0.40096685"
rx="0.76381439"
inkscape:label="rect7847" />
</g>
<path
style="fill:#000000;fill-opacity:0;fill-rule:evenodd;stroke:#000000;stroke-width:0.710035;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
d="m 4.9667305,15.136593 5.6340845,-0.01302"
id="path1273" />
</g>
<rect
style="opacity:1;fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:0.858316;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:39.9874;stroke-opacity:1;paint-order:normal"
id="rect27562"
width="2.0045266"
height="2.5935142"
x="6.7461357"
y="7.786335"
rx="0.40091178"
ry="0.51871145" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.2 KiB

View File

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="16"
height="16"
version="1.1"
viewBox="0 0 16 16"
id="svg7"
sodipodi:docname="top-panel.svg"
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview9"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
showgrid="false"
inkscape:zoom="44.6875"
inkscape:cx="5.5944056"
inkscape:cy="8"
inkscape:window-width="1500"
inkscape:window-height="963"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg7" />
<defs
id="defs3">
<style
id="current-color-scheme"
type="text/css">.ColorScheme-Text { color:#363636; }</style>
</defs>
<path
d="M 2,1 C 0.892,1 0,1.892 0,3 v 9 c 0,1.108 0.892,2 2,2 h 12 c 1.108,0 2,-0.892 2,-2 V 3 C 16,1.892 15.108,1 14,1 Z m 0,1 h 12 c 0.554,0 1,0.446 1,1 H 1 C 1,2.446 1.446,2 2,2 Z M 1,3 h 14 v 9 c 0,0.554 -0.446,1 -1,1 H 2 C 1.446,13 1,12.554 1,12 Z m 3,7 c -0.554,0 -1,0.446 -1,1 0,0.554 0.446,1 1,1 h 8 c 0.554,0 1,-0.446 1,-1 0,-0.554 -0.446,-1 -1,-1 z"
style="fill:currentColor"
class="ColorScheme-Text"
id="path5"
sodipodi:nodetypes="sssssssssssccsccsssscsssssss" />
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -0,0 +1,64 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="16"
height="16"
enable-background="new"
version="1.1"
id="svg16"
sodipodi:docname="general-symbolic.svg"
inkscape:version="1.2.1 (9c6d41e410, 2022-07-14)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs20" />
<sodipodi:namedview
id="namedview18"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="24.174578"
inkscape:cx="13.98163"
inkscape:cy="8.3765683"
inkscape:window-width="1500"
inkscape:window-height="963"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="g14" />
<g
fill="#363636"
id="g14"
transform="rotate(90,7.75,8.25)">
<path
d="m 2.9500144,1 c -0.277,0 -0.5,0.223 -0.5,0.5 v 6.5547 a 2.5,2.5 0 0 1 0.5,-0.054688 2.5,2.5 0 0 1 0.5,0.050781 v -6.5508 c 0,-0.277 -0.223,-0.5 -0.5,-0.5 z m 0.5,11.945 a 2.5,2.5 0 0 1 -0.5,0.05469 2.5,2.5 0 0 1 -0.5,-0.05078 v 1.5508 c 0,0.277 0.223,0.5 0.5,0.5 0.277,0 0.5,-0.223 0.5,-0.5 v -1.5547 z"
id="path2" />
<path
d="M 7.5,1 C 7.223,1 7,1.223 7,1.5 V 3.0547 A 2.5,2.5 0 0 1 7.5,3.000012 2.5,2.5 0 0 1 8,3.050793 v -1.5508 c 0,-0.277 -0.223,-0.5 -0.5,-0.5 z M 8,7.9453 A 2.5,2.5 0 0 1 7.5,7.999988 2.5,2.5 0 0 1 7,7.949207 v 6.5508 c 0,0.277 0.223,0.5 0.5,0.5 0.277,0 0.5,-0.223 0.5,-0.5 v -6.5547 z"
id="path4" />
<path
d="m 12.049986,1.0001482 c -0.277,0 -0.5,0.223 -0.5,0.5 v 6.5547 a 2.5,2.5 0 0 1 0.5,-0.054688 2.5,2.5 0 0 1 0.5,0.050781 v -6.5508 c 0,-0.277 -0.223,-0.5 -0.5,-0.5 z m 0.5,11.9450008 a 2.5,2.5 0 0 1 -0.5,0.05469 2.5,2.5 0 0 1 -0.5,-0.05078 v 1.5508 c 0,0.276999 0.223,0.5 0.5,0.5 0.277,0 0.5,-0.223001 0.5,-0.5 v -1.5547 z"
id="path6" />
<circle
cx="2.9500144"
cy="10.5"
id="circle8"
r="1.5" />
<circle
cx="7.5"
cy="5.5"
r="1.5"
id="circle10" />
<circle
cx="12.049986"
cy="10.500149"
id="circle12"
r="1.5" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
SPDX-FileCopyrightText: 2021 Romain Vigier <contact AT romainvigier.fr>
SPDX-License-Identifier: CC-BY-SA-4.0
-->
<svg width="16" height="16" version="1.1" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
<path d="m4.5 1c-2.4853 0-4.5 2.0147-4.5 4.5 0.0032498 1.2489 0.75291 2.3099 1.4414 3.2891 1.7318 2.463 6.5586 6.2109 6.5586 6.2109s4.8267-3.7479 6.5586-6.2109c0.68851-0.97919 1.4382-2.0402 1.4414-3.2891 0-2.4853-2.0147-4.5-4.5-4.5-2.5 0-3.101 2.001-3.5 2s-1-2-3.5-2z"/>
</svg>

After

Width:  |  Height:  |  Size: 535 B

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
SPDX-FileCopyrightText: 2021 Romain Vigier <contact AT romainvigier.fr>
SPDX-License-Identifier: CC-BY-SA-4.0
-->
<svg width="16" height="16" version="1.1" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
<path d="m3 11c0 1.6509 1.3491 3 3 3v-1c-1.1105 0-2-0.88951-2-2h-1z"/>
<path d="m10 2v1c1.1105 0 2 0.88951 2 2h1c0-1.6509-1.3491-3-3-3z"/>
<path d="m2 0c-1.108 0-2 0.892-2 2v6c0 1.108 0.892 2 2 2h4v-2l-0.33398-1h-2.332l-0.33398 1h-1l2-6h1l1.4668 4.4023c0.53433-0.84006 1.4736-1.4023 2.5332-1.4023v-3c0-1.108-0.892-2-2-2h-5zm2.5 3.5-0.83398 2.5h1.668l-0.83398-2.5z"/>
<path d="m9 6c-1.108 0-2 0.892-2 2v6c0 1.108 0.892 2 2 2h5c1.108 0 2-0.892 2-2v-6c0-1.108-0.892-2-2-2zm2 2h1v1h2v1h-0.53906c-0.15383 0.98264-0.74444 1.7805-1.3945 2.3926 0.79475 0.45608 1.5312 0.61719 1.5312 0.61719a0.5 0.5 0 0 1 0.39258 0.58789 0.5 0.5 0 0 1-0.58789 0.39258s-1.0838-0.19928-2.168-0.91797c-0.81221 0.5775-1.5371 0.88672-1.5371 0.88672a0.5 0.5 0 0 1-0.65625-0.26172 0.5 0.5 0 0 1 0.26172-0.65625s0.50729-0.21982 1.1211-0.625c-0.33276-0.3295-0.63597-0.72311-0.87109-1.1934a0.5 0.5 0 0 1 0.22461-0.66992 0.5 0.5 0 0 1 0.38086-0.02734 0.5 0.5 0 0 1 0.28906 0.25195c0.20204 0.40409 0.48152 0.74022 0.79102 1.0195 0.55892-0.49663 1.0381-1.1094 1.2012-1.7969h-3.4395v-1h2z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,965 @@
// This file is part of the AppIndicator/KStatusNotifierItem GNOME Shell extension
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
import Clutter from 'gi://Clutter';
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import GdkPixbuf from 'gi://GdkPixbuf';
import Gio from 'gi://Gio';
import St from 'gi://St';
import * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js';
import * as Signals from 'resource:///org/gnome/shell/misc/signals.js';
import * as DBusInterfaces from './interfaces.js';
import * as PromiseUtils from './promiseUtils.js';
import * as Util from './util.js';
import {DBusProxy} from './dbusProxy.js';
Gio._promisify(GdkPixbuf.Pixbuf, 'new_from_stream_async');
// ////////////////////////////////////////////////////////////////////////
// PART ONE: "ViewModel" backend implementation.
// Both code and design are inspired by libdbusmenu
// ////////////////////////////////////////////////////////////////////////
/**
* Saves menu property values and handles type checking and defaults
*/
export class PropertyStore {
constructor(initialProperties) {
this._props = new Map();
if (initialProperties) {
for (const [prop, value] of Object.entries(initialProperties))
this.set(prop, value);
}
}
set(name, value) {
if (name in PropertyStore.MandatedTypes && value &&
!value.is_of_type(PropertyStore.MandatedTypes[name]))
Util.Logger.warn(`Cannot set property ${name}: type mismatch!`);
else if (value)
this._props.set(name, value);
else
this._props.delete(name);
}
get(name) {
const prop = this._props.get(name);
if (prop)
return prop;
else if (name in PropertyStore.DefaultValues)
return PropertyStore.DefaultValues[name];
else
return null;
}
}
// we list all the properties we know and use here, so we won' have to deal with unexpected type mismatches
PropertyStore.MandatedTypes = {
'visible': GLib.VariantType.new('b'),
'enabled': GLib.VariantType.new('b'),
'label': GLib.VariantType.new('s'),
'type': GLib.VariantType.new('s'),
'children-display': GLib.VariantType.new('s'),
'icon-name': GLib.VariantType.new('s'),
'icon-data': GLib.VariantType.new('ay'),
'toggle-type': GLib.VariantType.new('s'),
'toggle-state': GLib.VariantType.new('i'),
};
PropertyStore.DefaultValues = {
'visible': GLib.Variant.new_boolean(true),
'enabled': GLib.Variant.new_boolean(true),
'label': GLib.Variant.new_string(''),
'type': GLib.Variant.new_string('standard'),
// elements not in here must return null
};
/**
* Represents a single menu item
*/
export class DbusMenuItem extends Signals.EventEmitter {
// will steal the properties object
constructor(client, id, properties, childrenIds) {
super();
this._client = client;
this._id = id;
this._propStore = new PropertyStore(properties);
this._children_ids = childrenIds;
}
propertyGet(propName) {
const prop = this.propertyGetVariant(propName);
return prop ? prop.get_string()[0] : null;
}
propertyGetVariant(propName) {
return this._propStore.get(propName);
}
propertyGetBool(propName) {
const prop = this.propertyGetVariant(propName);
return prop ? prop.get_boolean() : false;
}
propertyGetInt(propName) {
const prop = this.propertyGetVariant(propName);
return prop ? prop.get_int32() : 0;
}
propertySet(prop, value) {
this._propStore.set(prop, value);
this.emit('property-changed', prop, this.propertyGetVariant(prop));
}
resetProperties() {
Object.entries(PropertyStore.DefaultValues).forEach(([prop, value]) =>
this.propertySet(prop, value));
}
getChildrenIds() {
return this._children_ids.concat(); // clone it!
}
addChild(pos, childId) {
this._children_ids.splice(pos, 0, childId);
this.emit('child-added', this._client.getItem(childId), pos);
}
removeChild(childId) {
// find it
let pos = -1;
for (let i = 0; i < this._children_ids.length; ++i) {
if (this._children_ids[i] === childId) {
pos = i;
break;
}
}
if (pos < 0) {
Util.Logger.critical("Trying to remove child which doesn't exist");
} else {
this._children_ids.splice(pos, 1);
this.emit('child-removed', this._client.getItem(childId));
}
}
moveChild(childId, newPos) {
// find the old position
let oldPos = -1;
for (let i = 0; i < this._children_ids.length; ++i) {
if (this._children_ids[i] === childId) {
oldPos = i;
break;
}
}
if (oldPos < 0) {
Util.Logger.critical("tried to move child which wasn't in the list");
return;
}
if (oldPos !== newPos) {
this._children_ids.splice(oldPos, 1);
this._children_ids.splice(newPos, 0, childId);
this.emit('child-moved', oldPos, newPos, this._client.getItem(childId));
}
}
getChildren() {
return this._children_ids.map(el => this._client.getItem(el));
}
handleEvent(event, data, timestamp) {
if (!data)
data = GLib.Variant.new_int32(0);
this._client.sendEvent(this._id, event, data, timestamp);
}
getId() {
return this._id;
}
sendAboutToShow() {
this._client.sendAboutToShow(this._id);
}
}
/**
* The client does the heavy lifting of actually reading layouts and distributing events
*/
export const DBusClient = GObject.registerClass({
Signals: {'ready-changed': {}},
}, class AppIndicatorsDBusClient extends DBusProxy {
static get interfaceInfo() {
if (!this._interfaceInfo) {
this._interfaceInfo = Gio.DBusInterfaceInfo.new_for_xml(
DBusInterfaces.DBusMenu);
}
return this._interfaceInfo;
}
static get baseItems() {
if (!this._baseItems) {
this._baseItems = {
'children-display': GLib.Variant.new_string('submenu'),
};
}
return this._baseItems;
}
static destroy() {
delete this._interfaceInfo;
}
_init(busName, objectPath) {
const {interfaceInfo} = AppIndicatorsDBusClient;
super._init(busName, objectPath, interfaceInfo,
Gio.DBusProxyFlags.DO_NOT_LOAD_PROPERTIES);
this._items = new Map();
this._items.set(0, new DbusMenuItem(this, 0, DBusClient.baseItems, []));
this._flagItemsUpdateRequired = false;
// will be set to true if a layout update is needed once active
this._flagLayoutUpdateRequired = false;
// property requests are queued
this._propertiesRequestedFor = new Set(/* ids */);
this._layoutUpdated = false;
this._active = false;
}
async initAsync(cancellable) {
await super.initAsync(cancellable);
this._requestLayoutUpdate();
}
_onNameOwnerChanged() {
if (this.isReady)
this._requestLayoutUpdate();
}
get isReady() {
return this._layoutUpdated && !!this.gNameOwner;
}
get cancellable() {
return this._cancellable;
}
getRoot() {
return this._items.get(0);
}
_requestLayoutUpdate() {
const cancellable = new Util.CancellableChild(this._cancellable);
this._beginLayoutUpdate(cancellable);
}
async _requestProperties(propertyId, cancellable) {
this._propertiesRequestedFor.add(propertyId);
if (this._propertiesRequest && this._propertiesRequest.pending())
return;
// if we don't have any requests queued, we'll need to add one
this._propertiesRequest = new PromiseUtils.IdlePromise(
GLib.PRIORITY_DEFAULT_IDLE, cancellable);
await this._propertiesRequest;
const requestedProperties = Array.from(this._propertiesRequestedFor);
this._propertiesRequestedFor.clear();
const [result] = await this.GetGroupPropertiesAsync(requestedProperties,
[], cancellable);
result.forEach(([id, properties]) => {
const item = this._items.get(id);
if (!item)
return;
item.resetProperties();
for (const [prop, value] of Object.entries(properties))
item.propertySet(prop, value);
});
}
// Traverses the list of cached menu items and removes everyone that is not in the list
// so we don't keep alive unused items
_gcItems() {
const tag = new Date().getTime();
const toTraverse = [0];
while (toTraverse.length > 0) {
const item = this.getItem(toTraverse.shift());
item._dbusClientGcTag = tag;
Array.prototype.push.apply(toTraverse, item.getChildrenIds());
}
this._items.forEach((i, id) => {
if (i._dbusClientGcTag !== tag)
this._items.delete(id);
});
}
// the original implementation will only request partial layouts if somehow possible
// we try to save us from multiple kinds of race conditions by always requesting a full layout
_beginLayoutUpdate(cancellable) {
this._layoutUpdateUpdateAsync(cancellable).catch(e => {
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
logError(e);
});
}
// the original implementation will only request partial layouts if somehow possible
// we try to save us from multiple kinds of race conditions by always requesting a full layout
async _layoutUpdateUpdateAsync(cancellable) {
// we only read the type property, because if the type changes after reading all properties,
// the view would have to replace the item completely which we try to avoid
if (this._layoutUpdateCancellable)
this._layoutUpdateCancellable.cancel();
this._layoutUpdateCancellable = cancellable;
try {
const [revision_, root] = await this.GetLayoutAsync(0, -1,
['type', 'children-display'], cancellable);
this._updateLayoutState(true);
this._doLayoutUpdate(root, cancellable);
this._gcItems();
this._flagLayoutUpdateRequired = false;
this._flagItemsUpdateRequired = false;
} catch (e) {
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
this._updateLayoutState(false);
throw e;
} finally {
if (this._layoutUpdateCancellable === cancellable)
this._layoutUpdateCancellable = null;
}
}
_updateLayoutState(state) {
const wasReady = this.isReady;
this._layoutUpdated = state;
if (this.isReady !== wasReady)
this.emit('ready-changed');
}
_doLayoutUpdate(item, cancellable) {
const [id, properties, children] = item;
const childrenUnpacked = children.map(c => c.deep_unpack());
const childrenIds = childrenUnpacked.map(([c]) => c);
// make sure all our children exist
childrenUnpacked.forEach(c => this._doLayoutUpdate(c, cancellable));
// make sure we exist
const menuItem = this._items.get(id);
if (menuItem) {
// we do, update our properties if necessary
for (const [prop, value] of Object.entries(properties))
menuItem.propertySet(prop, value);
// make sure our children are all at the right place, and exist
const oldChildrenIds = menuItem.getChildrenIds();
for (let i = 0; i < childrenIds.length; ++i) {
// try to recycle an old child
let oldChild = -1;
for (let j = 0; j < oldChildrenIds.length; ++j) {
if (oldChildrenIds[j] === childrenIds[i]) {
[oldChild] = oldChildrenIds.splice(j, 1);
break;
}
}
if (oldChild < 0) {
// no old child found, so create a new one!
menuItem.addChild(i, childrenIds[i]);
} else {
// old child found, reuse it!
menuItem.moveChild(childrenIds[i], i);
}
}
// remove any old children that weren't reused
oldChildrenIds.forEach(c => menuItem.removeChild(c));
if (!this._flagItemsUpdateRequired)
return id;
}
// we don't, so let's create us
let newMenuItem = menuItem;
if (!newMenuItem) {
newMenuItem = new DbusMenuItem(this, id, properties, childrenIds);
this._items.set(id, newMenuItem);
}
this._requestProperties(id, cancellable).catch(e => {
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
Util.Logger.warn(`Could not get menu properties menu proxy: ${e}`);
});
return id;
}
async _doPropertiesUpdateAsync(cancellable) {
if (this._propertiesUpdateCancellable)
this._propertiesUpdateCancellable.cancel();
this._propertiesUpdateCancellable = cancellable;
try {
const requests = [];
this._items.forEach((_, id) =>
requests.push(this._requestProperties(id, cancellable)));
await Promise.all(requests);
} finally {
if (this._propertiesUpdateCancellable === cancellable)
this._propertiesUpdateCancellable = null;
}
}
_doPropertiesUpdate() {
const cancellable = new Util.CancellableChild(this._cancellable);
this._doPropertiesUpdateAsync(cancellable).catch(e => {
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
Util.Logger.warn(`Could not get menu properties menu proxy: ${e}`);
});
}
set active(active) {
const wasActive = this._active;
this._active = active;
if (active && wasActive !== active) {
if (this._flagLayoutUpdateRequired) {
this._requestLayoutUpdate();
} else if (this._flagItemsUpdateRequired) {
this._doPropertiesUpdate();
this._flagItemsUpdateRequired = false;
}
}
}
_onSignal(_sender, signal, params) {
if (signal === 'LayoutUpdated') {
if (!this._active) {
this._flagLayoutUpdateRequired = true;
return;
}
this._requestLayoutUpdate();
} else if (signal === 'ItemsPropertiesUpdated') {
if (!this._active) {
this._flagItemsUpdateRequired = true;
return;
}
this._onPropertiesUpdated(params.deep_unpack());
}
}
getItem(id) {
const item = this._items.get(id);
if (!item)
Util.Logger.warn(`trying to retrieve item for non-existing id ${id} !?`);
return item || null;
}
// we don't need to cache and burst-send that since it will not happen that frequently
async sendAboutToShow(id) {
if (this._hasAboutToShow === false)
return;
/* Some indicators (you, dropbox!) don't use the right signature
* and don't return a boolean, so we need to support both cases */
try {
const ret = await this.gConnection.call(this.gName, this.gObjectPath,
this.gInterfaceName, 'AboutToShow', new GLib.Variant('(i)', [id]),
null, Gio.DBusCallFlags.NONE, -1, this._cancellable);
if ((ret.is_of_type(new GLib.VariantType('(b)')) &&
ret.get_child_value(0).get_boolean()) ||
ret.is_of_type(new GLib.VariantType('()')))
this._requestLayoutUpdate();
} catch (e) {
if (e.matches(Gio.DBusError, Gio.DBusError.UNKNOWN_METHOD) ||
e.matches(Gio.DBusError, Gio.DBusError.FAILED)) {
this._hasAboutToShow = false;
return;
}
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
logError(e);
}
}
sendEvent(id, event, params, timestamp) {
if (!this.gNameOwner)
return;
this.EventAsync(id, event, params, timestamp, this._cancellable).catch(e => {
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
logError(e);
});
}
_onPropertiesUpdated([changed, removed]) {
changed.forEach(([id, props]) => {
const item = this._items.get(id);
if (!item)
return;
for (const [prop, value] of Object.entries(props))
item.propertySet(prop, value);
});
removed.forEach(([id, propNames]) => {
const item = this._items.get(id);
if (!item)
return;
propNames.forEach(propName => item.propertySet(propName, null));
});
}
});
// ////////////////////////////////////////////////////////////////////////
// PART TWO: "View" frontend implementation.
// ////////////////////////////////////////////////////////////////////////
// https://bugzilla.gnome.org/show_bug.cgi?id=731514
// GNOME 3.10 and 3.12 can't open a nested submenu.
// Patches have been written, but it's not clear when (if?) they will be applied.
// We also don't know whether they will be backported to 3.10, so we will work around
// it in the meantime. Offending versions can be clearly identified:
const NEED_NESTED_SUBMENU_FIX = '_setOpenedSubMenu' in PopupMenu.PopupMenu.prototype;
/**
* Creates new wrapper menu items and injects methods for managing them at runtime.
*
* Many functions in this object will be bound to the created item and executed as event
* handlers, so any `this` will refer to a menu item create in createItem
*/
const MenuItemFactory = {
createItem(client, dbusItem) {
// first, decide whether it's a submenu or not
let shellItem;
if (dbusItem.propertyGet('children-display') === 'submenu')
shellItem = new PopupMenu.PopupSubMenuMenuItem('FIXME');
else if (dbusItem.propertyGet('type') === 'separator')
shellItem = new PopupMenu.PopupSeparatorMenuItem('');
else
shellItem = new PopupMenu.PopupMenuItem('FIXME');
shellItem._dbusItem = dbusItem;
shellItem._dbusClient = client;
if (shellItem instanceof PopupMenu.PopupMenuItem) {
shellItem._icon = new St.Icon({
style_class: 'popup-menu-icon',
xAlign: Clutter.ActorAlign.END,
});
shellItem.add_child(shellItem._icon);
shellItem.label.x_expand = true;
}
// initialize our state
MenuItemFactory._updateLabel.call(shellItem);
MenuItemFactory._updateOrnament.call(shellItem);
MenuItemFactory._updateImage.call(shellItem);
MenuItemFactory._updateVisible.call(shellItem);
MenuItemFactory._updateSensitive.call(shellItem);
// initially create children
if (shellItem instanceof PopupMenu.PopupSubMenuMenuItem) {
dbusItem.getChildren().forEach(c =>
shellItem.menu.addMenuItem(MenuItemFactory.createItem(client, c)));
}
// now, connect various events
Util.connectSmart(dbusItem, 'property-changed',
shellItem, MenuItemFactory._onPropertyChanged);
Util.connectSmart(dbusItem, 'child-added',
shellItem, MenuItemFactory._onChildAdded);
Util.connectSmart(dbusItem, 'child-removed',
shellItem, MenuItemFactory._onChildRemoved);
Util.connectSmart(dbusItem, 'child-moved',
shellItem, MenuItemFactory._onChildMoved);
Util.connectSmart(shellItem, 'activate',
shellItem, MenuItemFactory._onActivate);
shellItem.connect('destroy', () => {
shellItem._dbusItem = null;
shellItem._dbusClient = null;
shellItem._icon = null;
});
if (shellItem.menu) {
Util.connectSmart(shellItem.menu, 'open-state-changed',
shellItem, MenuItemFactory._onOpenStateChanged);
}
return shellItem;
},
_onOpenStateChanged(menu, open) {
if (open) {
if (NEED_NESTED_SUBMENU_FIX) {
// close our own submenus
if (menu._openedSubMenu)
menu._openedSubMenu.close(false);
// register ourselves and close sibling submenus
if (menu._parent._openedSubMenu && menu._parent._openedSubMenu !== menu)
menu._parent._openedSubMenu.close(true);
menu._parent._openedSubMenu = menu;
}
this._dbusItem.handleEvent('opened', null, 0);
this._dbusItem.sendAboutToShow();
} else {
if (NEED_NESTED_SUBMENU_FIX) {
// close our own submenus
if (menu._openedSubMenu)
menu._openedSubMenu.close(false);
}
this._dbusItem.handleEvent('closed', null, 0);
}
},
_onActivate(_item, event) {
const timestamp = event.get_time();
if (timestamp && this._dbusClient.indicator)
this._dbusClient.indicator.provideActivationToken(timestamp);
this._dbusItem.handleEvent('clicked', GLib.Variant.new('i', 0),
timestamp);
},
_onPropertyChanged(dbusItem, prop, _value) {
if (prop === 'toggle-type' || prop === 'toggle-state')
MenuItemFactory._updateOrnament.call(this);
else if (prop === 'label')
MenuItemFactory._updateLabel.call(this);
else if (prop === 'enabled')
MenuItemFactory._updateSensitive.call(this);
else if (prop === 'visible')
MenuItemFactory._updateVisible.call(this);
else if (prop === 'icon-name' || prop === 'icon-data')
MenuItemFactory._updateImage.call(this);
else if (prop === 'type' || prop === 'children-display')
MenuItemFactory._replaceSelf.call(this);
else
Util.Logger.debug(`Unhandled property change: ${prop}`);
},
_onChildAdded(dbusItem, child, position) {
if (!(this instanceof PopupMenu.PopupSubMenuMenuItem)) {
Util.Logger.warn('Tried to add a child to non-submenu item. Better recreate it as whole');
MenuItemFactory._replaceSelf.call(this);
} else {
this.menu.addMenuItem(MenuItemFactory.createItem(this._dbusClient, child), position);
}
},
_onChildRemoved(dbusItem, child) {
if (!(this instanceof PopupMenu.PopupSubMenuMenuItem)) {
Util.Logger.warn('Tried to remove a child from non-submenu item. Better recreate it as whole');
MenuItemFactory._replaceSelf.call(this);
} else {
// find it!
this.menu._getMenuItems().forEach(item => {
if (item._dbusItem === child)
item.destroy();
});
}
},
_onChildMoved(dbusItem, child, oldpos, newpos) {
if (!(this instanceof PopupMenu.PopupSubMenuMenuItem)) {
Util.Logger.warn('Tried to move a child in non-submenu item. Better recreate it as whole');
MenuItemFactory._replaceSelf.call(this);
} else {
MenuUtils.moveItemInMenu(this.menu, child, newpos);
}
},
_updateLabel() {
const label = this._dbusItem.propertyGet('label').replace(/_([^_])/, '$1');
if (this.label) // especially on GS3.8, the separator item might not even have a hidden label
this.label.set_text(label);
},
_updateOrnament() {
if (!this.setOrnament)
return; // separators and alike might not have gotten the polyfill
if (this._dbusItem.propertyGet('toggle-type') === 'checkmark' &&
this._dbusItem.propertyGetInt('toggle-state'))
this.setOrnament(PopupMenu.Ornament.CHECK);
else if (this._dbusItem.propertyGet('toggle-type') === 'radio' &&
this._dbusItem.propertyGetInt('toggle-state'))
this.setOrnament(PopupMenu.Ornament.DOT);
else
this.setOrnament(PopupMenu.Ornament.NONE);
},
async _updateImage() {
if (!this._icon)
return; // might be missing on submenus / separators
const iconName = this._dbusItem.propertyGet('icon-name');
const iconData = this._dbusItem.propertyGetVariant('icon-data');
if (iconName) {
this._icon.icon_name = iconName;
} else if (iconData) {
try {
const inputStream = Gio.MemoryInputStream.new_from_bytes(
iconData.get_data_as_bytes());
this._icon.gicon = await GdkPixbuf.Pixbuf.new_from_stream_async(
inputStream, this._dbusClient.cancellable);
} catch (e) {
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
logError(e);
}
}
},
_updateVisible() {
this.visible = this._dbusItem.propertyGetBool('visible');
},
_updateSensitive() {
this.setSensitive(this._dbusItem.propertyGetBool('enabled'));
},
_replaceSelf(newSelf) {
// create our new self if needed
if (!newSelf)
newSelf = MenuItemFactory.createItem(this._dbusClient, this._dbusItem);
// first, we need to find our old position
let pos = -1;
const family = this._parent._getMenuItems();
for (let i = 0; i < family.length; ++i) {
if (family[i] === this)
pos = i;
}
if (pos < 0)
throw new Error("DBusMenu: can't replace non existing menu item");
// add our new self while we're still alive
this._parent.addMenuItem(newSelf, pos);
// now destroy our old self
this.destroy();
},
};
/**
* Utility functions not necessarily belonging into the item factory
*/
const MenuUtils = {
moveItemInMenu(menu, dbusItem, newpos) {
// HACK: we're really getting into the internals of the PopupMenu implementation
// First, find our wrapper. Children tend to lie. We do not trust the old positioning.
const family = menu._getMenuItems();
for (let i = 0; i < family.length; ++i) {
if (family[i]._dbusItem === dbusItem) {
// now, remove it
menu.box.remove_child(family[i]);
// and add it again somewhere else
if (newpos < family.length && family[newpos] !== family[i])
menu.box.insert_child_below(family[i], family[newpos]);
else
menu.box.add(family[i]);
// skip the rest
return;
}
}
},
};
/**
* Processes DBus events, creates the menu items and handles the actions
*
* Something like a mini-god-object
*/
export class Client extends Signals.EventEmitter {
constructor(busName, path, indicator) {
super();
this._busName = busName;
this._busPath = path;
this._client = new DBusClient(busName, path);
this._rootMenu = null; // the shell menu
this._rootItem = null; // the DbusMenuItem for the root
this.indicator = indicator;
this.cancellable = new Util.CancellableChild(this.indicator.cancellable);
this._client.initAsync(this.cancellable).catch(e => {
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
logError(e);
});
Util.connectSmart(this._client, 'ready-changed', this,
() => this.emit('ready-changed'));
}
get isReady() {
return this._client.isReady;
}
// this will attach the client to an already existing menu that will be used as the root menu.
// it will also connect the client to be automatically destroyed when the menu dies.
attachToMenu(menu) {
this._rootMenu = menu;
this._rootItem = this._client.getRoot();
this._itemsBeingAdded = new Set();
// cleanup: remove existing children (just in case)
this._rootMenu.removeAll();
if (NEED_NESTED_SUBMENU_FIX)
menu._setOpenedSubMenu = this._setOpenedSubmenu.bind(this);
// connect handlers
Util.connectSmart(menu, 'open-state-changed', this, this._onMenuOpened);
Util.connectSmart(menu, 'destroy', this, this.destroy);
Util.connectSmart(this._rootItem, 'child-added', this, this._onRootChildAdded);
Util.connectSmart(this._rootItem, 'child-removed', this, this._onRootChildRemoved);
Util.connectSmart(this._rootItem, 'child-moved', this, this._onRootChildMoved);
// Dropbox requires us to call AboutToShow(0) first
this._rootItem.sendAboutToShow();
// fill the menu for the first time
const children = this._rootItem.getChildren();
children.forEach(child =>
this._onRootChildAdded(this._rootItem, child));
}
_setOpenedSubmenu(submenu) {
if (!submenu)
return;
if (submenu._parent !== this._rootMenu)
return;
if (submenu === this._openedSubMenu)
return;
if (this._openedSubMenu && this._openedSubMenu.isOpen)
this._openedSubMenu.close(true);
this._openedSubMenu = submenu;
}
_onRootChildAdded(dbusItem, child, position) {
// Menu additions can be expensive, so let's do it in different chunks
const basePriority = this.isOpen ? GLib.PRIORITY_DEFAULT : GLib.PRIORITY_LOW;
const idlePromise = new PromiseUtils.IdlePromise(
basePriority + this._itemsBeingAdded.size, this.cancellable);
this._itemsBeingAdded.add(child);
idlePromise.then(() => {
if (!this._itemsBeingAdded.has(child))
return;
this._rootMenu.addMenuItem(
MenuItemFactory.createItem(this, child), position);
}).catch(e => {
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
logError(e);
}).finally(() => this._itemsBeingAdded.delete(child));
}
_onRootChildRemoved(dbusItem, child) {
// children like to play hide and seek
// but we know how to find it for sure!
const item = this._rootMenu._getMenuItems().find(it =>
it._dbusItem === child);
if (item)
item.destroy();
else
this._itemsBeingAdded.delete(child);
}
_onRootChildMoved(dbusItem, child, oldpos, newpos) {
MenuUtils.moveItemInMenu(this._rootMenu, dbusItem, newpos);
}
_onMenuOpened(menu, state) {
if (!this._rootItem)
return;
this._client.active = state;
if (state) {
if (this._openedSubMenu && this._openedSubMenu.isOpen)
this._openedSubMenu.close();
this._rootItem.handleEvent('opened', null, 0);
this._rootItem.sendAboutToShow();
} else {
this._rootItem.handleEvent('closed', null, 0);
}
}
destroy() {
this.emit('destroy');
if (this._client)
this._client.destroy();
this._client = null;
this._rootItem = null;
this._rootMenu = null;
this.indicator = null;
this._itemsBeingAdded = null;
}
}

View File

@ -0,0 +1,103 @@
import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import {CancellableChild, Logger} from './util.js';
Gio._promisify(Gio.DBusProxy.prototype, 'init_async');
export const DBusProxy = GObject.registerClass({
Signals: {'destroy': {}},
}, class DBusProxy extends Gio.DBusProxy {
static get TUPLE_VARIANT_TYPE() {
if (!this._tupleVariantType)
this._tupleVariantType = new GLib.VariantType('(v)');
return this._tupleVariantType;
}
static destroy() {
delete this._tupleType;
}
_init(busName, objectPath, interfaceInfo, flags = Gio.DBusProxyFlags.NONE) {
if (interfaceInfo.signals.length)
Logger.warn('Avoid exposing signals to gjs!');
super._init({
gConnection: Gio.DBus.session,
gInterfaceName: interfaceInfo.name,
gInterfaceInfo: interfaceInfo,
gName: busName,
gObjectPath: objectPath,
gFlags: flags,
});
this._signalIds = [];
if (!(flags & Gio.DBusProxyFlags.DO_NOT_CONNECT_SIGNALS)) {
this._signalIds.push(this.connect('g-signal',
(_proxy, ...args) => this._onSignal(...args)));
}
this._signalIds.push(this.connect('notify::g-name-owner', () =>
this._onNameOwnerChanged()));
}
async initAsync(cancellable) {
cancellable = new CancellableChild(cancellable);
await this.init_async(GLib.PRIORITY_DEFAULT, cancellable);
this._cancellable = cancellable;
this.gInterfaceInfo.methods.map(m => m.name).forEach(method =>
this._ensureAsyncMethod(method));
}
destroy() {
this.emit('destroy');
this._signalIds.forEach(id => this.disconnect(id));
if (this._cancellable)
this._cancellable.cancel();
}
// This can be removed when we will have GNOME 43 as minimum version
_ensureAsyncMethod(method) {
if (this[`${method}Async`])
return;
if (!this[`${method}Remote`])
throw new Error(`Missing remote method '${method}'`);
this[`${method}Async`] = function (...args) {
return new Promise((resolve, reject) => {
this[`${method}Remote`](...args, (ret, e) => {
if (e)
reject(e);
else
resolve(ret);
});
});
};
}
_onSignal() {
}
getProperty(propertyName, cancellable) {
return this.gConnection.call(this.gName,
this.gObjectPath, 'org.freedesktop.DBus.Properties', 'Get',
GLib.Variant.new('(ss)', [this.gInterfaceName, propertyName]),
DBusProxy.TUPLE_VARIANT_TYPE, Gio.DBusCallFlags.NONE, -1,
cancellable);
}
getProperties(cancellable) {
return this.gConnection.call(this.gName,
this.gObjectPath, 'org.freedesktop.DBus.Properties', 'GetAll',
GLib.Variant.new('(s)', [this.gInterfaceName]),
GLib.VariantType.new('(a{sv})'), Gio.DBusCallFlags.NONE, -1,
cancellable);
}
});

View File

@ -0,0 +1,89 @@
// This file is part of the AppIndicator/KStatusNotifierItem GNOME Shell extension
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
import * as Extension from 'resource:///org/gnome/shell/extensions/extension.js';
import * as StatusNotifierWatcher from './statusNotifierWatcher.js';
import * as Interfaces from './interfaces.js';
import * as TrayIconsManager from './trayIconsManager.js';
import * as Util from './util.js';
import {SettingsManager} from './settingsManager.js';
export default class AppIndicatorExtension extends Extension.Extension {
constructor(...args) {
super(...args);
Util.Logger.init(this);
Interfaces.initialize(this);
this._isEnabled = false;
this._statusNotifierWatcher = null;
this._watchDog = new Util.NameWatcher(StatusNotifierWatcher.WATCHER_BUS_NAME);
this._watchDog.connect('vanished', () => this._maybeEnableAfterNameAvailable());
// HACK: we want to leave the watchdog alive when disabling the extension,
// but if we are being reloaded, we destroy it since it could be considered
// a leak and spams our log, too.
/* eslint-disable no-undef */
if (typeof global['--appindicator-extension-on-reload'] === 'function')
global['--appindicator-extension-on-reload']();
global['--appindicator-extension-on-reload'] = () => {
Util.Logger.debug('Reload detected, destroying old watchdog');
this._watchDog.destroy();
this._watchDog = null;
};
/* eslint-enable no-undef */
}
enable() {
this._isEnabled = true;
SettingsManager.initialize(this);
Util.tryCleanupOldIndicators();
this._maybeEnableAfterNameAvailable();
TrayIconsManager.TrayIconsManager.initialize();
}
disable() {
this._isEnabled = false;
TrayIconsManager.TrayIconsManager.destroy();
if (this._statusNotifierWatcher !== null) {
this._statusNotifierWatcher.destroy();
this._statusNotifierWatcher = null;
}
SettingsManager.destroy();
}
// FIXME: when entering/leaving the lock screen, the extension might be
// enabled/disabled rapidly.
// This will create very bad side effects in case we were not done unowning
// the name while trying to own it again. Since g_bus_unown_name doesn't
// fire any callback when it's done, we need to monitor the bus manually
// to find out when the name vanished so we can reclaim it again.
_maybeEnableAfterNameAvailable() {
// by the time we get called whe might not be enabled
if (!this._isEnabled || this._statusNotifierWatcher)
return;
if (this._watchDog.nameAcquired && this._watchDog.nameOnBus)
return;
this._statusNotifierWatcher = new StatusNotifierWatcher.StatusNotifierWatcher(
this._watchDog);
}
}

View File

@ -0,0 +1,179 @@
// This file is part of the AppIndicator/KStatusNotifierItem GNOME Shell extension
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
import GLib from 'gi://GLib';
import Gio from 'gi://Gio';
import * as PromiseUtils from './promiseUtils.js';
import * as Util from './util.js';
// The icon cache caches icon objects in case they're reused shortly aftwerwards.
// This is necessary for some indicators like skype which rapidly switch between serveral icons.
// Without caching, the garbage collection would never be able to handle the amount of new icon data.
// If the lifetime of an icon is over, the cache will destroy the icon. (!)
// The presence of active icons will extend the lifetime.
const GC_INTERVAL = 100; // seconds
const LIFETIME_TIMESPAN = 120; // seconds
// how to use: see IconCache.add, IconCache.get
export class IconCache {
constructor() {
this._cache = new Map();
this._activeIcons = Object.create(null);
this._lifetime = new Map(); // we don't want to attach lifetime to the object
}
add(id, icon) {
if (!(icon instanceof Gio.Icon)) {
Util.Logger.critical('IconCache: Only Gio.Icons are supported');
return null;
}
if (!id) {
Util.Logger.critical('IconCache: Invalid ID provided');
return null;
}
const oldIcon = this._cache.get(id);
if (!oldIcon || !oldIcon.equal(icon)) {
Util.Logger.debug(`IconCache: adding ${id}: ${icon}`);
this._cache.set(id, icon);
} else {
icon = oldIcon;
}
this._renewLifetime(id);
this._checkGC();
return icon;
}
updateActive(iconType, gicon, isActive) {
if (!gicon)
return;
const previousActive = this._activeIcons[iconType];
if (isActive && [...this._cache.values()].some(icon => icon === gicon))
this._activeIcons[iconType] = gicon;
else if (previousActive === gicon)
delete this._activeIcons[iconType];
else
return;
if (previousActive) {
this._cache.forEach((icon, id) => {
if (icon === previousActive)
this._renewLifetime(id);
});
}
}
_remove(id) {
Util.Logger.debug(`IconCache: removing ${id}`);
this._cache.delete(id);
this._lifetime.delete(id);
}
_renewLifetime(id) {
this._lifetime.set(id, new Date().getTime() + LIFETIME_TIMESPAN * 1000);
}
forceDestroy(id) {
const gicon = this._cache.has(id);
if (gicon) {
Object.keys(this._activeIcons).forEach(iconType =>
this.updateActive(iconType, gicon, false));
this._remove(id);
this._checkGC();
}
}
// marks all the icons as removable, if something doesn't claim them before
weakClear() {
this._activeIcons = Object.create(null);
this._checkGC();
}
// removes everything from the cache
clear() {
this._activeIcons = Object.create(null);
this._cache.forEach((_icon, id) => this._remove(id));
this._checkGC();
}
// returns an object from the cache, or null if it can't be found.
get(id) {
const icon = this._cache.get(id);
if (icon) {
Util.Logger.debug(`IconCache: retrieving ${id}: ${icon}`);
this._renewLifetime(id);
return icon;
}
return null;
}
async _checkGC() {
const cacheIsEmpty = this._cache.size === 0;
if (!cacheIsEmpty && !this._gcTimeout) {
Util.Logger.debug('IconCache: garbage collector started');
let anyUnusedInCache = false;
this._gcTimeout = new PromiseUtils.TimeoutSecondsPromise(GC_INTERVAL,
GLib.PRIORITY_LOW);
try {
await this._gcTimeout;
anyUnusedInCache = this._gc();
} catch (e) {
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
logError(e, 'IconCache: garbage collector');
} finally {
delete this._gcTimeout;
}
if (anyUnusedInCache)
this._checkGC();
} else if (cacheIsEmpty && this._gcTimeout) {
Util.Logger.debug('IconCache: garbage collector stopped');
this._gcTimeout.cancel();
}
}
_gc() {
const time = new Date().getTime();
let anyUnused = false;
this._cache.forEach((icon, id) => {
if (Object.values(this._activeIcons).includes(icon)) {
Util.Logger.debug(`IconCache: ${id} is in use.`);
} else if (this._lifetime.get(id) < time) {
this._remove(id);
} else {
anyUnused = true;
Util.Logger.debug(`IconCache: ${id} survived this round.`);
}
});
return anyUnused;
}
destroy() {
this.clear();
}
}

View File

@ -0,0 +1,585 @@
// This file is part of the AppIndicator/KStatusNotifierItem GNOME Shell extension
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
import Clutter from 'gi://Clutter';
import Gio from 'gi://Gio';
import GObject from 'gi://GObject';
import St from 'gi://St';
import * as AppDisplay from 'resource:///org/gnome/shell/ui/appDisplay.js';
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
import * as Panel from 'resource:///org/gnome/shell/ui/panel.js';
import * as PanelMenu from 'resource:///org/gnome/shell/ui/panelMenu.js';
import * as AppIndicator from './appIndicator.js';
import * as PromiseUtils from './promiseUtils.js';
import * as SettingsManager from './settingsManager.js';
import * as Util from './util.js';
import * as DBusMenu from './dbusMenu.js';
const DEFAULT_ICON_SIZE = Panel.PANEL_ICON_SIZE || 16;
export function addIconToPanel(statusIcon) {
if (!(statusIcon instanceof BaseStatusIcon))
throw TypeError(`Unexpected icon type: ${statusIcon}`);
const settings = SettingsManager.getDefaultGSettings();
const indicatorId = `appindicator-${statusIcon.uniqueId}`;
const currentIcon = Main.panel.statusArea[indicatorId];
if (currentIcon) {
if (currentIcon !== statusIcon)
currentIcon.destroy();
Main.panel.statusArea[indicatorId] = null;
}
Main.panel.addToStatusArea(indicatorId, statusIcon, 1,
settings.get_string('tray-pos'));
Util.connectSmart(settings, 'changed::tray-pos', statusIcon, () =>
addIconToPanel(statusIcon));
}
export function getTrayIcons() {
return Object.values(Main.panel.statusArea).filter(
i => i instanceof IndicatorStatusTrayIcon);
}
export function getAppIndicatorIcons() {
return Object.values(Main.panel.statusArea).filter(
i => i instanceof IndicatorStatusIcon);
}
export const BaseStatusIcon = GObject.registerClass(
class IndicatorBaseStatusIcon extends PanelMenu.Button {
_init(menuAlignment, nameText, iconActor, dontCreateMenu) {
super._init(menuAlignment, nameText, dontCreateMenu);
const settings = SettingsManager.getDefaultGSettings();
Util.connectSmart(settings, 'changed::icon-opacity', this, this._updateOpacity);
this.connect('notify::hover', () => this._onHoverChanged());
if (!super._onDestroy)
this.connect('destroy', () => this._onDestroy());
this._box = new St.BoxLayout({style_class: 'panel-status-indicators-box'});
this.add_child(this._box);
this._setIconActor(iconActor);
this._showIfReady();
}
_setIconActor(icon) {
if (!(icon instanceof Clutter.Actor))
throw new Error(`${icon} is not a valid actor`);
if (this._icon && this._icon !== icon)
this._icon.destroy();
this._icon = icon;
this._updateEffects();
this._monitorIconEffects();
if (this._icon) {
this._box.add_child(this._icon);
const id = this._icon.connect('destroy', () => {
this._icon.disconnect(id);
this._icon = null;
this._monitorIconEffects();
});
}
}
_onDestroy() {
if (this._icon)
this._icon.destroy();
if (super._onDestroy)
super._onDestroy();
}
isReady() {
throw new GObject.NotImplementedError('isReady() in %s'.format(this.constructor.name));
}
get icon() {
return this._icon;
}
get uniqueId() {
throw new GObject.NotImplementedError('uniqueId in %s'.format(this.constructor.name));
}
_showIfReady() {
this.visible = this.isReady();
}
_onHoverChanged() {
if (this.hover) {
this.opacity = 255;
if (this._icon)
this._icon.remove_effect_by_name('desaturate');
} else {
this._updateEffects();
}
}
_updateOpacity() {
const settings = SettingsManager.getDefaultGSettings();
const userValue = settings.get_user_value('icon-opacity');
if (userValue)
this.opacity = userValue.unpack();
else
this.opacity = 255;
}
_updateEffects() {
this._updateOpacity();
if (this._icon) {
this._updateSaturation();
this._updateBrightnessContrast();
}
}
_monitorIconEffects() {
const settings = SettingsManager.getDefaultGSettings();
const monitoring = !!this._iconSaturationIds;
if (!this._icon && monitoring) {
Util.disconnectSmart(settings, this, this._iconSaturationIds);
delete this._iconSaturationIds;
Util.disconnectSmart(settings, this, this._iconBrightnessIds);
delete this._iconBrightnessIds;
Util.disconnectSmart(settings, this, this._iconContrastIds);
delete this._iconContrastIds;
} else if (this._icon && !monitoring) {
this._iconSaturationIds =
Util.connectSmart(settings, 'changed::icon-saturation', this,
this._updateSaturation);
this._iconBrightnessIds =
Util.connectSmart(settings, 'changed::icon-brightness', this,
this._updateBrightnessContrast);
this._iconContrastIds =
Util.connectSmart(settings, 'changed::icon-contrast', this,
this._updateBrightnessContrast);
}
}
_updateSaturation() {
const settings = SettingsManager.getDefaultGSettings();
const desaturationValue = settings.get_double('icon-saturation');
let desaturateEffect = this._icon.get_effect('desaturate');
if (desaturationValue > 0) {
if (!desaturateEffect) {
desaturateEffect = new Clutter.DesaturateEffect();
this._icon.add_effect_with_name('desaturate', desaturateEffect);
}
desaturateEffect.set_factor(desaturationValue);
} else if (desaturateEffect) {
this._icon.remove_effect(desaturateEffect);
}
}
_updateBrightnessContrast() {
const settings = SettingsManager.getDefaultGSettings();
const brightnessValue = settings.get_double('icon-brightness');
const contrastValue = settings.get_double('icon-contrast');
let brightnessContrastEffect = this._icon.get_effect('brightness-contrast');
if (brightnessValue !== 0 | contrastValue !== 0) {
if (!brightnessContrastEffect) {
brightnessContrastEffect = new Clutter.BrightnessContrastEffect();
this._icon.add_effect_with_name('brightness-contrast', brightnessContrastEffect);
}
brightnessContrastEffect.set_brightness(brightnessValue);
brightnessContrastEffect.set_contrast(contrastValue);
} else if (brightnessContrastEffect) {
this._icon.remove_effect(brightnessContrastEffect);
}
}
});
/*
* IndicatorStatusIcon implements an icon in the system status area
*/
export const IndicatorStatusIcon = GObject.registerClass(
class IndicatorStatusIcon extends BaseStatusIcon {
_init(indicator) {
super._init(0.5, indicator.accessibleName,
new AppIndicator.IconActor(indicator, DEFAULT_ICON_SIZE));
this._indicator = indicator;
this._lastClickTime = -1;
this._lastClickX = -1;
this._lastClickY = -1;
this._box.add_style_class_name('appindicator-box');
Util.connectSmart(this._indicator, 'ready', this, this._showIfReady);
Util.connectSmart(this._indicator, 'menu', this, this._updateMenu);
Util.connectSmart(this._indicator, 'label', this, this._updateLabel);
Util.connectSmart(this._indicator, 'status', this, this._updateStatus);
Util.connectSmart(this._indicator, 'reset', this, () => {
this._updateStatus();
this._updateLabel();
});
Util.connectSmart(this._indicator, 'accessible-name', this, () =>
this.set_accessible_name(this._indicator.accessibleName));
Util.connectSmart(this._indicator, 'destroy', this, () => this.destroy());
this.connect('notify::visible', () => this._updateMenu());
this._showIfReady();
}
_onDestroy() {
if (this._menuClient) {
this._menuClient.disconnect(this._menuReadyId);
this._menuClient.destroy();
this._menuClient = null;
}
super._onDestroy();
}
get uniqueId() {
return this._indicator.uniqueId;
}
isReady() {
return this._indicator && this._indicator.isReady;
}
_updateLabel() {
const {label} = this._indicator;
if (label) {
if (!this._label || !this._labelBin) {
this._labelBin = new St.Bin({
yAlign: Clutter.ActorAlign.CENTER,
});
this._label = new St.Label();
Util.addActor(this._labelBin, this._label);
Util.addActor(this._box, this._labelBin);
}
this._label.set_text(label);
if (!this._box.contains(this._labelBin))
Util.addActor(this._box, this._labelBin); // FIXME: why is it suddenly necessary?
} else if (this._label) {
this._labelBin.destroy_all_children();
Util.removeActor(this._box, this._labelBin);
this._labelBin.destroy();
delete this._labelBin;
delete this._label;
}
}
_updateStatus() {
const wasVisible = this.visible;
this.visible = this._indicator.status !== AppIndicator.SNIStatus.PASSIVE;
if (this.visible !== wasVisible)
this._indicator.checkAlive().catch(logError);
}
_updateMenu() {
if (this._menuClient) {
this._menuClient.disconnect(this._menuReadyId);
this._menuClient.destroy();
this._menuClient = null;
this.menu.removeAll();
}
if (this.visible && this._indicator.menuPath) {
this._menuClient = new DBusMenu.Client(this._indicator.busName,
this._indicator.menuPath, this._indicator);
if (this._menuClient.isReady)
this._menuClient.attachToMenu(this.menu);
this._menuReadyId = this._menuClient.connect('ready-changed', () => {
if (this._menuClient.isReady)
this._menuClient.attachToMenu(this.menu);
else
this._updateMenu();
});
}
}
_showIfReady() {
if (!this.isReady())
return;
this._updateLabel();
this._updateStatus();
this._updateMenu();
}
_updateClickCount(event) {
const [x, y] = event.get_coords();
const time = event.get_time();
const {doubleClickDistance, doubleClickTime} =
Clutter.Settings.get_default();
if (time > (this._lastClickTime + doubleClickTime) ||
(Math.abs(x - this._lastClickX) > doubleClickDistance) ||
(Math.abs(y - this._lastClickY) > doubleClickDistance))
this._clickCount = 0;
this._lastClickTime = time;
this._lastClickX = x;
this._lastClickY = y;
this._clickCount = (this._clickCount % 2) + 1;
return this._clickCount;
}
_maybeHandleDoubleClick(event) {
if (this._indicator.supportsActivation === false)
return Clutter.EVENT_PROPAGATE;
if (event.get_button() !== Clutter.BUTTON_PRIMARY)
return Clutter.EVENT_PROPAGATE;
if (this._updateClickCount(event) === 2) {
this._indicator.open(...event.get_coords(), event.get_time());
return Clutter.EVENT_STOP;
}
return Clutter.EVENT_PROPAGATE;
}
async _waitForDoubleClick() {
const {doubleClickTime} = Clutter.Settings.get_default();
this._waitDoubleClickPromise = new PromiseUtils.TimeoutPromise(
doubleClickTime);
try {
await this._waitDoubleClickPromise;
this.menu.toggle();
} catch (e) {
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
throw e;
} finally {
delete this._waitDoubleClickPromise;
}
}
vfunc_event(event) {
if (this.menu.numMenuItems && event.type() === Clutter.EventType.TOUCH_BEGIN)
this.menu.toggle();
return Clutter.EVENT_PROPAGATE;
}
vfunc_button_press_event(event) {
if (this._waitDoubleClickPromise)
this._waitDoubleClickPromise.cancel();
// if middle mouse button clicked send SecondaryActivate dbus event and do not show appindicator menu
if (event.get_button() === Clutter.BUTTON_MIDDLE) {
if (Main.panel.menuManager.activeMenu)
Main.panel.menuManager._closeMenu(true, Main.panel.menuManager.activeMenu);
this._indicator.secondaryActivate(event.get_time(), ...event.get_coords());
return Clutter.EVENT_STOP;
}
if (event.get_button() === Clutter.BUTTON_SECONDARY) {
this.menu.toggle();
return Clutter.EVENT_PROPAGATE;
}
const doubleClickHandled = this._maybeHandleDoubleClick(event);
if (doubleClickHandled === Clutter.EVENT_PROPAGATE &&
event.get_button() === Clutter.BUTTON_PRIMARY &&
this.menu.numMenuItems) {
if (this._indicator.supportsActivation !== false)
this._waitForDoubleClick().catch(logError);
else
this.menu.toggle();
}
return Clutter.EVENT_PROPAGATE;
}
vfunc_scroll_event(event) {
// Since Clutter 1.10, clutter will always send a smooth scrolling event
// with explicit deltas, no matter what input device is used
// In fact, for every scroll there will be a smooth and non-smooth scroll
// event, and we can choose which one we interpret.
if (event.get_scroll_direction() === Clutter.ScrollDirection.SMOOTH) {
const [dx, dy] = event.get_scroll_delta();
this._indicator.scroll(dx, dy);
return Clutter.EVENT_STOP;
}
return Clutter.EVENT_PROPAGATE;
}
});
export const IndicatorStatusTrayIcon = GObject.registerClass(
class IndicatorTrayIcon extends BaseStatusIcon {
_init(icon) {
super._init(0.5, icon.wm_class, icon, {dontCreateMenu: true});
Util.Logger.debug(`Adding legacy tray icon ${this.uniqueId}`);
this._box.add_style_class_name('appindicator-trayicons-box');
this.add_style_class_name('appindicator-icon');
this.add_style_class_name('tray-icon');
this.connect('button-press-event', (_actor, _event) => {
this.add_style_pseudo_class('active');
return Clutter.EVENT_PROPAGATE;
});
this.connect('button-release-event', (_actor, event) => {
this._icon.click(event);
this.remove_style_pseudo_class('active');
return Clutter.EVENT_PROPAGATE;
});
this.connect('key-press-event', (_actor, event) => {
this.add_style_pseudo_class('active');
this._icon.click(event);
return Clutter.EVENT_PROPAGATE;
});
this.connect('key-release-event', (_actor, event) => {
this._icon.click(event);
this.remove_style_pseudo_class('active');
return Clutter.EVENT_PROPAGATE;
});
Util.connectSmart(this._icon, 'destroy', this, () => {
icon.clear_effects();
this.destroy();
});
const settings = SettingsManager.getDefaultGSettings();
Util.connectSmart(settings, 'changed::icon-size', this, this._updateIconSize);
const themeContext = St.ThemeContext.get_for_stage(global.stage);
Util.connectSmart(themeContext, 'notify::scale-factor', this, () =>
this._updateIconSize());
this._updateIconSize();
}
_onDestroy() {
Util.Logger.debug(`Destroying legacy tray icon ${this.uniqueId}`);
if (this._waitDoubleClickPromise)
this._waitDoubleClickPromise.cancel();
super._onDestroy();
}
isReady() {
return !!this._icon;
}
get uniqueId() {
return `legacy:${this._icon.wm_class}:${this._icon.pid}`;
}
vfunc_navigate_focus(from, direction) {
this.grab_key_focus();
return super.vfunc_navigate_focus(from, direction);
}
_getSimulatedButtonEvent(touchEvent) {
const event = Clutter.Event.new(Clutter.EventType.BUTTON_RELEASE);
event.set_button(1);
event.set_time(touchEvent.get_time());
event.set_flags(touchEvent.get_flags());
event.set_stage(global.stage);
event.set_source(touchEvent.get_source());
event.set_coords(...touchEvent.get_coords());
event.set_state(touchEvent.get_state());
return event;
}
vfunc_touch_event(event) {
// Under X11 we rely on emulated pointer events
if (!imports.gi.Meta.is_wayland_compositor())
return Clutter.EVENT_PROPAGATE;
const slot = event.get_event_sequence().get_slot();
if (!this._touchPressSlot &&
event.get_type() === Clutter.EventType.TOUCH_BEGIN) {
this.add_style_pseudo_class('active');
this._touchButtonEvent = this._getSimulatedButtonEvent(event);
this._touchPressSlot = slot;
this._touchDelayPromise = new PromiseUtils.TimeoutPromise(
AppDisplay.MENU_POPUP_TIMEOUT);
this._touchDelayPromise.then(() => {
delete this._touchDelayPromise;
delete this._touchPressSlot;
this._touchButtonEvent.set_button(3);
this._icon.click(this._touchButtonEvent);
this.remove_style_pseudo_class('active');
});
} else if (event.get_type() === Clutter.EventType.TOUCH_END &&
this._touchPressSlot === slot) {
delete this._touchPressSlot;
delete this._touchButtonEvent;
if (this._touchDelayPromise) {
this._touchDelayPromise.cancel();
delete this._touchDelayPromise;
}
this._icon.click(this._getSimulatedButtonEvent(event));
this.remove_style_pseudo_class('active');
} else if (event.get_type() === Clutter.EventType.TOUCH_UPDATE &&
this._touchPressSlot === slot) {
this.add_style_pseudo_class('active');
this._touchButtonEvent = this._getSimulatedButtonEvent(event);
}
return Clutter.EVENT_PROPAGATE;
}
vfunc_leave_event(event) {
this.remove_style_pseudo_class('active');
if (this._touchDelayPromise) {
this._touchDelayPromise.cancel();
delete this._touchDelayPromise;
}
return super.vfunc_leave_event(event);
}
_updateIconSize() {
const settings = SettingsManager.getDefaultGSettings();
const {scaleFactor} = St.ThemeContext.get_for_stage(global.stage);
let iconSize = settings.get_int('icon-size');
if (iconSize <= 0)
iconSize = DEFAULT_ICON_SIZE;
this.height = -1;
this._icon.set({
width: iconSize * scaleFactor,
height: iconSize * scaleFactor,
xAlign: Clutter.ActorAlign.CENTER,
yAlign: Clutter.ActorAlign.CENTER,
});
}
});

View File

@ -0,0 +1,66 @@
<interface name="com.canonical.dbusmenu">
<!-- Properties -->
<property name="Version" type="u" access="read" />
<property name="TextDirection" type="s" access="read" />
<property name="Status" type="s" access="read" />
<property name="IconThemePath" type="as" access="read" />
<!-- Functions -->
<method name="GetLayout">
<arg type="i" name="parentId" direction="in" />
<arg type="i" name="recursionDepth" direction="in" />
<arg type="as" name="propertyNames" direction="in" />
<arg type="u" name="revision" direction="out" />
<arg type="(ia{sv}av)" name="layout" direction="out" />
</method>
<method name="GetGroupProperties">
<arg type="ai" name="ids" direction="in" />
<arg type="as" name="propertyNames" direction="in" />
<arg type="a(ia{sv})" name="properties" direction="out" />
</method>
<method name="GetProperty">
<arg type="i" name="id" direction="in" />
<arg type="s" name="name" direction="in" />
<arg type="v" name="value" direction="out" />
</method>
<method name="Event">
<arg type="i" name="id" direction="in" />
<arg type="s" name="eventId" direction="in" />
<arg type="v" name="data" direction="in" />
<arg type="u" name="timestamp" direction="in" />
</method>
<method name="EventGroup">
<arg type="a(isvu)" name="events" direction="in" />
<arg type="ai" name="idErrors" direction="out" />
</method>
<method name="AboutToShow">
<arg type="i" name="id" direction="in" />
<arg type="b" name="needUpdate" direction="out" />
</method>
<method name="AboutToShowGroup">
<arg type="ai" name="ids" direction="in" />
<arg type="ai" name="updatesNeeded" direction="out" />
<arg type="ai" name="idErrors" direction="out" />
</method>
<!-- Signals
<signal name="ItemsPropertiesUpdated">
<arg type="a(ia{sv})" name="updatedProps" direction="out" />
<arg type="a(ias)" name="removedProps" direction="out" />
</signal>
<signal name="LayoutUpdated">
<arg type="u" name="revision" direction="out" />
<arg type="i" name="parent" direction="out" />
</signal>
<signal name="ItemActivationRequested">
<arg type="i" name="id" direction="out" />
<arg type="u" name="timestamp" direction="out" />
</signal>
-->
</interface>

View File

@ -0,0 +1,130 @@
<!-- Based on:
https://invent.kde.org/frameworks/knotifications/-/blob/master/src/org.kde.StatusNotifierItem.xml
-->
<interface name="org.kde.StatusNotifierItem">
<property name="Category" type="s" access="read"/>
<property name="Id" type="s" access="read"/>
<property name="Title" type="s" access="read"/>
<property name="Status" type="s" access="read"/>
<property name="WindowId" type="i" access="read"/>
<!-- An additional path to add to the theme search path to find the icons specified above. -->
<property name="IconThemePath" type="s" access="read"/>
<property name="Menu" type="o" access="read"/>
<property name="ItemIsMenu" type="b" access="read"/>
<!-- main icon -->
<!-- names are preferred over pixmaps -->
<property name="IconName" type="s" access="read"/>
<!--struct containing width, height and image data-->
<property name="IconPixmap" type="a(iiay)" access="read">
<annotation name="org.qtproject.QtDBus.QtTypeName" value="KDbusImageVector"/>
</property>
<property name="OverlayIconName" type="s" access="read"/>
<property name="OverlayIconPixmap" type="a(iiay)" access="read">
<annotation name="org.qtproject.QtDBus.QtTypeName" value="KDbusImageVector"/>
</property>
<!-- Requesting attention icon -->
<property name="AttentionIconName" type="s" access="read"/>
<!--same definition as image-->
<property name="AttentionIconPixmap" type="a(iiay)" access="read">
<annotation name="org.qtproject.QtDBus.QtTypeName" value="KDbusImageVector"/>
</property>
<property name="AttentionMovieName" type="s" access="read"/>
<!-- tooltip data -->
<!--(iiay) is an image-->
<!-- We disable this as we don't support tooltip, so no need to go through it
<property name="ToolTip" type="(sa(iiay)ss)" access="read">
<annotation name="org.qtproject.QtDBus.QtTypeName" value="KDbusToolTipStruct"/>
</property>
-->
<!-- interaction: the systemtray wants the application to do something -->
<method name="ContextMenu">
<!-- we're passing the coordinates of the icon, so the app knows where to put the popup window -->
<arg name="x" type="i" direction="in"/>
<arg name="y" type="i" direction="in"/>
</method>
<method name="Activate">
<arg name="x" type="i" direction="in"/>
<arg name="y" type="i" direction="in"/>
</method>
<method name="ProvideXdgActivationToken">
<arg name="token" type="s" direction="in"/>
</method>
<method name="SecondaryActivate">
<arg name="x" type="i" direction="in"/>
<arg name="y" type="i" direction="in"/>
</method>
<method name="XAyatanaSecondaryActivate">
<arg name="timestamp" type="u" direction="in"/>
</method>
<method name="Scroll">
<arg name="delta" type="i" direction="in"/>
<arg name="orientation" type="s" direction="in"/>
</method>
<!-- Signals: the client wants to change something in the status
<signal name="NewTitle">
</signal>
<signal name="NewIcon">
</signal>
<signal name="NewAttentionIcon">
</signal>
<signal name="NewOverlayIcon">
</signal>
-->
<!-- We disable this as we don't support tooltip, so no need to go through it
<signal name="NewToolTip">
</signal>
-->
<!--
<signal name="NewStatus">
<arg name="status" type="s"/>
</signal>
-->
<!-- The following items are not supported by specs, but widely used
<signal name="NewIconThemePath">
<arg type="s" name="icon_theme_path" direction="out" />
</signal>
<signal name="NewMenu"></signal>
-->
<!-- ayatana labels -->
<!-- These are commented out because GDBusProxy would otherwise require them,
but they are not available for KDE indicators
-->
<!--<signal name="XAyatanaNewLabel">
<arg type="s" name="label" direction="out" />
<arg type="s" name="guide" direction="out" />
</signal>
<property name="XAyatanaLabel" type="s" access="read" />
<property name="XAyatanaLabelGuide" type="s" access="read" />-->
</interface>

View File

@ -0,0 +1,38 @@
<interface name="org.kde.StatusNotifierWatcher">
<!-- methods -->
<method name="RegisterStatusNotifierItem">
<arg name="service" type="s" direction="in"/>
</method>
<method name="RegisterStatusNotifierHost">
<arg name="service" type="s" direction="in"/>
</method>
<!-- properties -->
<property name="RegisteredStatusNotifierItems" type="as" access="read">
<annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="QStringList"/>
</property>
<property name="IsStatusNotifierHostRegistered" type="b" access="read"/>
<property name="ProtocolVersion" type="i" access="read"/>
<!-- signals -->
<signal name="StatusNotifierItemRegistered">
<arg type="s"/>
</signal>
<signal name="StatusNotifierItemUnregistered">
<arg type="s"/>
</signal>
<signal name="StatusNotifierHostRegistered">
</signal>
<signal name="StatusNotifierHostUnregistered">
</signal>
</interface>

View File

@ -0,0 +1,52 @@
// This file is part of the AppIndicator/KStatusNotifierItem GNOME Shell extension
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
export let StatusNotifierItem = null;
export let StatusNotifierWatcher = null;
export let DBusMenu = null;
// loads a xml file into an in-memory string
function loadInterfaceXml(extension, filename) {
const interfacesDir = extension.dir.get_child('interfaces-xml');
const file = interfacesDir.get_child(filename);
const [result, contents] = imports.gi.GLib.file_get_contents(file.get_path());
if (result) {
// HACK: The "" + trick is important as hell because file_get_contents returns
// an object (WTF?) but Gio.makeProxyWrapper requires `typeof() === "string"`
// Otherwise, it will try to check `instanceof XML` and fail miserably because there
// is no `XML` on very recent SpiderMonkey releases (or, if SpiderMonkey is old enough,
// will spit out a TypeError soon).
let nodeContents = contents;
if (contents instanceof Uint8Array)
nodeContents = imports.byteArray.toString(contents);
return `<node>${nodeContents}</node>`;
} else {
throw new Error(`AppIndicatorSupport: Could not load file: ${filename}`);
}
}
export function initialize(extension) {
StatusNotifierItem = loadInterfaceXml(extension, 'StatusNotifierItem.xml');
StatusNotifierWatcher = loadInterfaceXml(extension, 'StatusNotifierWatcher.xml');
DBusMenu = loadInterfaceXml(extension, 'DBusMenu.xml');
}
export function destroy() {
StatusNotifierItem = null;
StatusNotifierWatcher = null;
DBusMenu = null;
}

View File

@ -0,0 +1,14 @@
{
"_generated": "Generated by SweetTooth, do not edit",
"description": "Adds AppIndicator, KStatusNotifierItem and legacy Tray icons support to the Shell",
"gettext-domain": "AppIndicatorExtension",
"name": "AppIndicator and KStatusNotifierItem Support",
"settings-schema": "org.gnome.shell.extensions.appindicator",
"shell-version": [
"45",
"46"
],
"url": "https://github.com/ubuntu/gnome-shell-extension-appindicator",
"uuid": "appindicatorsupport@rgcjonas.gmail.com",
"version": 58
}

View File

@ -0,0 +1,68 @@
// This file is part of the AppIndicator/KStatusNotifierItem GNOME Shell extension
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
export function argbToRgba(src) {
const dest = new Uint8Array(src.length);
for (let j = 0; j < src.length; j += 4) {
const srcAlpha = src[j];
dest[j] = src[j + 1]; /* red */
dest[j + 1] = src[j + 2]; /* green */
dest[j + 2] = src[j + 3]; /* blue */
dest[j + 3] = srcAlpha; /* alpha */
}
return dest;
}
export function getBestPixmap(pixmapsVariant, preferredSize) {
if (!pixmapsVariant)
throw new TypeError('null pixmapsVariant');
const pixmapsVariantsArray = new Array(pixmapsVariant.n_children());
if (!pixmapsVariantsArray.length)
throw TypeError('Empty Icon found');
for (let i = 0; i < pixmapsVariantsArray.length; ++i)
pixmapsVariantsArray[i] = pixmapsVariant.get_child_value(i);
const pixmapsSizedArray = pixmapsVariantsArray.map((pixmapVariant, index) => ({
width: pixmapVariant.get_child_value(0).unpack(),
height: pixmapVariant.get_child_value(1).unpack(),
index,
}));
const sortedIconPixmapArray = pixmapsSizedArray.sort(
({width: widthA, height: heightA}, {width: widthB, height: heightB}) => {
const areaA = widthA * heightA;
const areaB = widthB * heightB;
return areaA - areaB;
});
// we prefer any pixmap that is equal or bigger than our requested size
const qualifiedIconPixmapArray = sortedIconPixmapArray.filter(({width, height}) =>
width >= preferredSize && height >= preferredSize);
const {width, height, index} = qualifiedIconPixmapArray.length > 0
? qualifiedIconPixmapArray[0] : sortedIconPixmapArray.pop();
const pixmapVariant = pixmapsVariantsArray[index].get_child_value(2);
const rowStride = width * 4; // hopefully this is correct
return {pixmapVariant, width, height, rowStride};
}

View File

@ -0,0 +1,326 @@
// -*- mode: js2; indent-tabs-mode: nil; js2-basic-offset: 4 -*-
/* exported init, buildPrefsWidget */
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import Gio from 'gi://Gio';
import Gtk from 'gi://Gtk';
import {
ExtensionPreferences,
gettext as _
} from 'resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js';
const AppIndicatorPreferences = GObject.registerClass(
class AppIndicatorPreferences extends Gtk.Box {
_init(extension) {
super._init({orientation: Gtk.Orientation.VERTICAL, spacing: 30});
this._settings = extension.getSettings();
let label = null;
let widget = null;
this.preferences_vbox = new Gtk.Box({
orientation: Gtk.Orientation.VERTICAL,
spacing: 8,
margin_start: 30,
margin_end: 30,
margin_top: 30,
margin_bottom: 30,
});
this.custom_icons_vbox = new Gtk.Box({
orientation: Gtk.Orientation.HORIZONTAL,
spacing: 10,
margin_start: 10,
margin_end: 10,
margin_top: 10,
margin_bottom: 10,
});
label = new Gtk.Label({
label: _('Enable Legacy Tray Icons support'),
hexpand: true,
halign: Gtk.Align.START,
});
widget = new Gtk.Switch({halign: Gtk.Align.END});
this._settings.bind('legacy-tray-enabled', widget, 'active',
Gio.SettingsBindFlags.DEFAULT);
this.legacy_tray_hbox = new Gtk.Box({
orientation: Gtk.Orientation.HORIZONTAL,
spacing: 10,
margin_start: 10,
margin_end: 10,
margin_top: 10,
margin_bottom: 10,
});
this.legacy_tray_hbox.append(label);
this.legacy_tray_hbox.append(widget);
// Icon opacity
this.opacity_hbox = new Gtk.Box({
orientation: Gtk.Orientation.HORIZONTAL,
spacing: 10,
margin_start: 10,
margin_end: 10,
margin_top: 10,
margin_bottom: 10,
});
label = new Gtk.Label({
label: _('Opacity (min: 0, max: 255)'),
hexpand: true,
halign: Gtk.Align.START,
});
widget = new Gtk.SpinButton({halign: Gtk.Align.END});
widget.set_sensitive(true);
widget.set_range(0, 255);
widget.set_value(this._settings.get_int('icon-opacity'));
widget.set_increments(1, 2);
widget.connect('value-changed', w => {
this._settings.set_int('icon-opacity', w.get_value_as_int());
});
this.opacity_hbox.append(label);
this.opacity_hbox.append(widget);
// Icon saturation
this.saturation_hbox = new Gtk.Box({
orientation: Gtk.Orientation.HORIZONTAL,
spacing: 10,
margin_start: 10,
margin_end: 10,
margin_top: 10,
margin_bottom: 10,
});
label = new Gtk.Label({
label: _('Desaturation (min: 0.0, max: 1.0)'),
hexpand: true,
halign: Gtk.Align.START,
});
widget = new Gtk.SpinButton({halign: Gtk.Align.END, digits: 1});
widget.set_sensitive(true);
widget.set_range(0.0, 1.0);
widget.set_value(this._settings.get_double('icon-saturation'));
widget.set_increments(0.1, 0.2);
widget.connect('value-changed', w => {
this._settings.set_double('icon-saturation', w.get_value());
});
this.saturation_hbox.append(label);
this.saturation_hbox.append(widget);
// Icon brightness
this.brightness_hbox = new Gtk.Box({
orientation: Gtk.Orientation.HORIZONTAL,
spacing: 10,
margin_start: 10,
margin_end: 10,
margin_top: 10,
margin_bottom: 10,
});
label = new Gtk.Label({
label: _('Brightness (min: -1.0, max: 1.0)'),
hexpand: true,
halign: Gtk.Align.START,
});
widget = new Gtk.SpinButton({halign: Gtk.Align.END, digits: 1});
widget.set_sensitive(true);
widget.set_range(-1.0, 1.0);
widget.set_value(this._settings.get_double('icon-brightness'));
widget.set_increments(0.1, 0.2);
widget.connect('value-changed', w => {
this._settings.set_double('icon-brightness', w.get_value());
});
this.brightness_hbox.append(label);
this.brightness_hbox.append(widget);
// Icon contrast
this.contrast_hbox = new Gtk.Box({
orientation: Gtk.Orientation.HORIZONTAL,
spacing: 10,
margin_start: 10,
margin_end: 10,
margin_top: 10,
margin_bottom: 10,
});
label = new Gtk.Label({
label: _('Contrast (min: -1.0, max: 1.0)'),
hexpand: true,
halign: Gtk.Align.START,
});
widget = new Gtk.SpinButton({halign: Gtk.Align.END, digits: 1});
widget.set_sensitive(true);
widget.set_range(-1.0, 1.0);
widget.set_value(this._settings.get_double('icon-contrast'));
widget.set_increments(0.1, 0.2);
widget.connect('value-changed', w => {
this._settings.set_double('icon-contrast', w.get_value());
});
this.contrast_hbox.append(label);
this.contrast_hbox.append(widget);
// Icon size
this.icon_size_hbox = new Gtk.Box({
orientation: Gtk.Orientation.HORIZONTAL,
spacing: 10,
margin_start: 10,
margin_end: 10,
margin_top: 10,
margin_bottom: 10,
});
label = new Gtk.Label({
label: _('Icon size (min: 0, max: 96)'),
hexpand: true,
halign: Gtk.Align.START,
});
widget = new Gtk.SpinButton({halign: Gtk.Align.END});
widget.set_sensitive(true);
widget.set_range(0, 96);
widget.set_value(this._settings.get_int('icon-size'));
widget.set_increments(1, 2);
widget.connect('value-changed', w => {
this._settings.set_int('icon-size', w.get_value_as_int());
});
this.icon_size_hbox.append(label);
this.icon_size_hbox.append(widget);
// Tray position in panel
this.tray_position_hbox = new Gtk.Box({
orientation: Gtk.Orientation.HORIZONTAL,
spacing: 10,
margin_start: 10,
margin_end: 10,
margin_top: 10,
margin_bottom: 10,
});
label = new Gtk.Label({
label: _('Tray horizontal alignment'),
hexpand: true,
halign: Gtk.Align.START,
});
widget = new Gtk.ComboBoxText();
widget.append('center', _('Center'));
widget.append('left', _('Left'));
widget.append('right', _('Right'));
this._settings.bind('tray-pos', widget, 'active-id',
Gio.SettingsBindFlags.DEFAULT);
this.tray_position_hbox.append(label);
this.tray_position_hbox.append(widget);
this.preferences_vbox.append(this.legacy_tray_hbox);
this.preferences_vbox.append(this.opacity_hbox);
this.preferences_vbox.append(this.saturation_hbox);
this.preferences_vbox.append(this.brightness_hbox);
this.preferences_vbox.append(this.contrast_hbox);
this.preferences_vbox.append(this.icon_size_hbox);
this.preferences_vbox.append(this.tray_position_hbox);
// Custom icons section
const customListStore = new Gtk.ListStore();
customListStore.set_column_types([
GObject.TYPE_STRING,
GObject.TYPE_STRING,
GObject.TYPE_STRING,
]);
const customInitArray = this._settings.get_value('custom-icons').deep_unpack();
customInitArray.forEach(pair => {
customListStore.set(customListStore.append(), [0, 1, 2], pair);
});
customListStore.append();
const customTreeView = new Gtk.TreeView({
model: customListStore,
hexpand: true,
vexpand: true,
});
const customTitles = [
_('Indicator ID'),
_('Icon Name'),
_('Attention Icon Name'),
];
const indicatorIdColumn = new Gtk.TreeViewColumn({
title: customTitles[0],
sizing: Gtk.TreeViewColumnSizing.AUTOSIZE,
});
const customIconColumn = new Gtk.TreeViewColumn({
title: customTitles[1],
sizing: Gtk.TreeViewColumnSizing.AUTOSIZE,
});
const customAttentionIconColumn = new Gtk.TreeViewColumn({
title: customTitles[2],
sizing: Gtk.TreeViewColumnSizing.AUTOSIZE,
});
const cellrenderer = new Gtk.CellRendererText({editable: true});
indicatorIdColumn.pack_start(cellrenderer, true);
customIconColumn.pack_start(cellrenderer, true);
customAttentionIconColumn.pack_start(cellrenderer, true);
indicatorIdColumn.add_attribute(cellrenderer, 'text', 0);
customIconColumn.add_attribute(cellrenderer, 'text', 1);
customAttentionIconColumn.add_attribute(cellrenderer, 'text', 2);
customTreeView.insert_column(indicatorIdColumn, 0);
customTreeView.insert_column(customIconColumn, 1);
customTreeView.insert_column(customAttentionIconColumn, 2);
customTreeView.set_grid_lines(Gtk.TreeViewGridLines.BOTH);
this.custom_icons_vbox.append(customTreeView);
cellrenderer.connect('edited', (w, path, text) => {
this.selection = customTreeView.get_selection();
const title = customTreeView.get_cursor()[1].get_title();
const columnIndex = customTitles.indexOf(title);
const selection = this.selection.get_selected();
const iter = selection.at(2);
const text2 = customListStore.get_value(iter, columnIndex ? 0 : 1);
customListStore.set(iter, [columnIndex], [text]);
const storeLength = customListStore.iter_n_children(null);
const customIconArray = [];
for (let i = 0; i < storeLength; i++) {
const returnIter = customListStore.iter_nth_child(null, i);
const [success, iterList] = returnIter;
if (!success)
break;
if (iterList) {
const id = customListStore.get_value(iterList, 0);
const customIcon = customListStore.get_value(iterList, 1);
const customAttentionIcon = customListStore.get_value(iterList, 2);
if (id && customIcon)
customIconArray.push([id, customIcon, customAttentionIcon || '']);
} else {
break;
}
}
this._settings.set_value('custom-icons', new GLib.Variant(
'a(sss)', customIconArray));
if (storeLength === 1 && (text || text2))
customListStore.append();
if (storeLength > 1) {
if ((!text && !text2) && (storeLength - 1 > path))
customListStore.remove(iter);
if ((text || text2) && storeLength - 1 <= path)
customListStore.append();
}
});
this.notebook = new Gtk.Notebook();
this.notebook.append_page(this.preferences_vbox,
new Gtk.Label({label: _('Preferences')}));
this.notebook.append_page(this.custom_icons_vbox,
new Gtk.Label({label: _('Custom Icons')}));
this.append(this.notebook);
}
});
export default class DockPreferences extends ExtensionPreferences {
getPreferencesWidget() {
return new AppIndicatorPreferences(this);
}
}

View File

@ -0,0 +1,324 @@
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import Meta from 'gi://GdkPixbuf';
import * as Signals from 'resource:///org/gnome/shell/misc/signals.js';
export class CancellablePromise extends Promise {
constructor(executor, cancellable) {
if (!(executor instanceof Function))
throw TypeError('executor is not a function');
if (cancellable && !(cancellable instanceof Gio.Cancellable))
throw TypeError('cancellable parameter is not a Gio.Cancellable');
let rejector;
let resolver;
super((resolve, reject) => {
resolver = resolve;
rejector = reject;
});
const {stack: promiseStack} = new Error();
this._promiseStack = promiseStack;
this._resolver = (...args) => {
resolver(...args);
this._resolved = true;
this._cleanup();
};
this._rejector = (...args) => {
rejector(...args);
this._rejected = true;
this._cleanup();
};
if (!cancellable) {
executor(this._resolver, this._rejector);
return;
}
this._cancellable = cancellable;
this._cancelled = cancellable.is_cancelled();
if (this._cancelled) {
this._rejector(new GLib.Error(Gio.IOErrorEnum,
Gio.IOErrorEnum.CANCELLED, 'Promise cancelled'));
return;
}
this._cancellationId = cancellable.connect(() => {
const id = this._cancellationId;
this._cancellationId = 0;
GLib.idle_add(GLib.PRIORITY_DEFAULT, () => cancellable.disconnect(id));
this.cancel();
});
executor(this._resolver, this._rejector);
}
_cleanup() {
if (this._cancellationId)
this._cancellable.disconnect(this._cancellationId);
}
get cancellable() {
return this._chainRoot._cancellable || null;
}
get _chainRoot() {
return this._root ? this._root : this;
}
then(...args) {
const ret = super.then(...args);
/* Every time we call then() on this promise we'd get a new
* CancellablePromise however that won't have the properties that the
* root one has set, and then it won't be possible to cancel a promise
* chain from the last one.
* To allow this we keep track of the root promise, make sure that
* the same method on the root object is called during cancellation
* or any destruction method if you want this to work. */
if (ret instanceof CancellablePromise)
ret._root = this._chainRoot;
return ret;
}
resolved() {
return !!this._chainRoot._resolved;
}
rejected() {
return !!this._chainRoot._rejected;
}
cancelled() {
return !!this._chainRoot._cancelled;
}
pending() {
return !this.resolved() && !this.rejected();
}
cancel() {
if (this._root) {
this._root.cancel();
return this;
}
if (!this.pending())
return this;
this._cancelled = true;
const error = new GLib.Error(Gio.IOErrorEnum,
Gio.IOErrorEnum.CANCELLED, 'Promise cancelled');
error.stack += `## Promise created at:\n${this._promiseStack}`;
this._rejector(error);
return this;
}
}
export class SignalConnectionPromise extends CancellablePromise {
constructor(object, signal, cancellable) {
if (arguments.length === 1 && object instanceof Function) {
super(object);
return;
}
if (!(object.connect instanceof Function))
throw new TypeError('Not a valid object');
if (object instanceof GObject.Object &&
!GObject.signal_lookup(signal.split(':')[0], object.constructor.$gtype))
throw new TypeError(`Signal ${signal} not found on object ${object}`);
let id;
let destroyId;
super(resolve => {
let connectSignal;
if (object instanceof GObject.Object)
connectSignal = (sig, cb) => GObject.signal_connect(object, sig, cb);
else
connectSignal = (sig, cb) => object.connect(sig, cb);
id = connectSignal(signal, (_obj, ...args) => {
if (!args.length)
resolve();
else
resolve(args.length === 1 ? args[0] : args);
});
if (signal !== 'destroy' &&
(!(object instanceof GObject.Object) ||
GObject.signal_lookup('destroy', object.constructor.$gtype)))
destroyId = connectSignal('destroy', () => this.cancel());
}, cancellable);
this._object = object;
this._id = id;
this._destroyId = destroyId;
}
_cleanup() {
if (this._id) {
let disconnectSignal;
if (this._object instanceof GObject.Object)
disconnectSignal = id => GObject.signal_handler_disconnect(this._object, id);
else
disconnectSignal = id => this._object.disconnect(id);
disconnectSignal(this._id);
if (this._destroyId) {
disconnectSignal(this._destroyId);
this._destroyId = 0;
}
this._object = null;
this._id = 0;
}
super._cleanup();
}
get object() {
return this._chainRoot._object;
}
}
export class GSourcePromise extends CancellablePromise {
constructor(gsource, priority, cancellable) {
if (arguments.length === 1 && gsource instanceof Function) {
super(gsource);
return;
}
if (gsource.constructor.$gtype !== GLib.Source.$gtype)
throw new TypeError(`gsource ${gsource} is not of type GLib.Source`);
if (priority === undefined)
priority = GLib.PRIORITY_DEFAULT;
else if (!Number.isInteger(priority))
throw TypeError('Invalid priority');
super(resolve => {
gsource.set_priority(priority);
gsource.set_callback(() => {
resolve();
return GLib.SOURCE_REMOVE;
});
gsource.attach(null);
}, cancellable);
this._gsource = gsource;
this._gsource.set_name(`[gnome-shell] ${this.constructor.name} ${
new Error().stack.split('\n').filter(line =>
!line.match(/misc\/promiseUtils\.js/))[0]}`);
if (this.rejected())
this._gsource.destroy();
}
get gsource() {
return this._chainRoot._gsource;
}
_cleanup() {
if (this._gsource) {
this._gsource.destroy();
this._gsource = null;
}
super._cleanup();
}
}
export class IdlePromise extends GSourcePromise {
constructor(priority, cancellable) {
if (arguments.length === 1 && priority instanceof Function) {
super(priority);
return;
}
if (priority === undefined)
priority = GLib.PRIORITY_DEFAULT_IDLE;
super(GLib.idle_source_new(), priority, cancellable);
}
}
export class TimeoutPromise extends GSourcePromise {
constructor(interval, priority, cancellable) {
if (arguments.length === 1 && interval instanceof Function) {
super(interval);
return;
}
if (!Number.isInteger(interval) || interval < 0)
throw TypeError('Invalid interval');
super(GLib.timeout_source_new(interval), priority, cancellable);
}
}
export class TimeoutSecondsPromise extends GSourcePromise {
constructor(interval, priority, cancellable) {
if (arguments.length === 1 && interval instanceof Function) {
super(interval);
return;
}
if (!Number.isInteger(interval) || interval < 0)
throw TypeError('Invalid interval');
super(GLib.timeout_source_new_seconds(interval), priority, cancellable);
}
}
export class MetaLaterPromise extends CancellablePromise {
constructor(laterType, cancellable) {
if (arguments.length === 1 && laterType instanceof Function) {
super(laterType);
return;
}
if (laterType && laterType.constructor.$gtype !== Meta.LaterType.$gtype)
throw new TypeError(`laterType ${laterType} is not of type Meta.LaterType`);
else if (!laterType)
laterType = Meta.LaterType.BEFORE_REDRAW;
let id;
super(resolve => {
id = Meta.later_add(laterType, () => {
this.remove();
resolve();
return GLib.SOURCE_REMOVE;
});
}, cancellable);
this._id = id;
}
_cleanup() {
if (this._id) {
Meta.later_remove(this._id);
this._id = 0;
}
super._cleanup();
}
}
export function _promisifySignals(proto) {
if (proto.connect_once)
return;
proto.connect_once = function (signal, cancellable) {
return new SignalConnectionPromise(this, signal, cancellable);
};
}
_promisifySignals(GObject.Object.prototype);
_promisifySignals(Signals.EventEmitter.prototype);

View File

@ -0,0 +1,49 @@
<schemalist gettext-domain="AppIndicatorExtension">
<schema id="org.gnome.shell.extensions.appindicator" path="/org/gnome/shell/extensions/appindicator/">
<key name="legacy-tray-enabled" type="b">
<default>true</default>
<summary>Enable legacy tray icons support</summary>
</key>
<key name="icon-saturation" type="d">
<default>0.0</default>
<summary>Saturation</summary>
</key>
<key name="icon-brightness" type="d">
<default>0.0</default>
<summary>Brightness</summary>
</key>
<key name="icon-contrast" type="d">
<default>0.0</default>
<summary>Contrast</summary>
</key>
<key name="icon-opacity" type="i">
<default>240</default>
<summary>Opacity</summary>
</key>
<key name="icon-size" type="i">
<default>0</default>
<summary>Icon size</summary>
<description>Icon size in pixel</description>
</key>
<key name="icon-spacing" type="i">
<default>12</default>
<summary>Icon spacing</summary>
<description>Icon spacing within the tray</description>
</key>
<key name="tray-pos" type="s">
<default>"right"</default>
<summary>Position in tray</summary>
<description>Set where the Icon tray should appear in Gnome tray</description>
</key>
<key name="tray-order" type="i">
<default>1</default>
<summary>Order in tray</summary>
<description>Set where the Icon tray should appear among other trays</description>
</key>
<key name="custom-icons" type="a(sss)">
<default>[]</default>
<summary>Custom icons</summary>
<description>Replace any icons with custom icons from themes</description>
</key>
</schema>
</schemalist>

View File

@ -0,0 +1,55 @@
// This file is part of the AppIndicator/KStatusNotifierItem GNOME Shell extension
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
let settingsManager;
export class SettingsManager {
static initialize(extension) {
SettingsManager._settingsManager = new SettingsManager(extension);
}
static destroy() {
SettingsManager._settingsManager.destroy();
SettingsManager._settingsManager = null;
}
static getDefault() {
return this._settingsManager;
}
get gsettings() {
return this._gsettings;
}
constructor(extension) {
if (settingsManager)
throw new Error('SettingsManager is already constructed');
this._gsettings = extension.getSettings();
}
destroy() {
this._gsettings = null;
}
}
export function getDefault() {
return SettingsManager.getDefault();
}
export function getDefaultGSettings() {
return SettingsManager.getDefault().gsettings;
}

View File

@ -0,0 +1,287 @@
// This file is part of the AppIndicator/KStatusNotifierItem GNOME Shell extension
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import * as AppIndicator from './appIndicator.js';
import * as IndicatorStatusIcon from './indicatorStatusIcon.js';
import * as Interfaces from './interfaces.js';
import * as PromiseUtils from './promiseUtils.js';
import * as Util from './util.js';
import * as DBusMenu from './dbusMenu.js';
import {DBusProxy} from './dbusProxy.js';
// TODO: replace with org.freedesktop and /org/freedesktop when approved
const KDE_PREFIX = 'org.kde';
export const WATCHER_BUS_NAME = `${KDE_PREFIX}.StatusNotifierWatcher`;
const WATCHER_OBJECT = '/StatusNotifierWatcher';
const DEFAULT_ITEM_OBJECT_PATH = '/StatusNotifierItem';
/*
* The StatusNotifierWatcher class implements the StatusNotifierWatcher dbus object
*/
export class StatusNotifierWatcher {
constructor(watchDog) {
this._watchDog = watchDog;
this._dbusImpl = Gio.DBusExportedObject.wrapJSObject(Interfaces.StatusNotifierWatcher, this);
try {
this._dbusImpl.export(Gio.DBus.session, WATCHER_OBJECT);
} catch (e) {
Util.Logger.warn(`Failed to export ${WATCHER_OBJECT}`);
logError(e);
}
this._cancellable = new Gio.Cancellable();
this._everAcquiredName = false;
this._ownName = Gio.DBus.session.own_name(WATCHER_BUS_NAME,
Gio.BusNameOwnerFlags.NONE,
this._acquiredName.bind(this),
this._lostName.bind(this));
this._items = new Map();
try {
this._dbusImpl.emit_signal('StatusNotifierHostRegistered', null);
} catch (e) {
Util.Logger.warn(`Failed to notify registered host ${WATCHER_OBJECT}`);
}
this._seekStatusNotifierItems().catch(e => {
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
logError(e, 'Looking for StatusNotifierItem\'s');
});
}
_acquiredName() {
this._everAcquiredName = true;
this._watchDog.nameAcquired = true;
}
_lostName() {
if (this._everAcquiredName)
Util.Logger.debug(`Lost name${WATCHER_BUS_NAME}`);
else
Util.Logger.warn(`Failed to acquire ${WATCHER_BUS_NAME}`);
this._watchDog.nameAcquired = false;
}
async _registerItem(service, busName, objPath) {
const id = Util.indicatorId(service, busName, objPath);
if (this._items.has(id)) {
Util.Logger.warn(`Item ${id} is already registered`);
return;
}
Util.Logger.debug(`Registering StatusNotifierItem ${id}`);
try {
const indicator = new AppIndicator.AppIndicator(service, busName, objPath);
this._items.set(id, indicator);
indicator.connect('destroy', () => this._onIndicatorDestroyed(indicator));
indicator.connect('name-owner-changed', async () => {
if (!indicator.hasNameOwner) {
try {
await new PromiseUtils.TimeoutPromise(500,
GLib.PRIORITY_DEFAULT, this._cancellable);
if (this._items.has(id) && !indicator.hasNameOwner)
indicator.destroy();
} catch (e) {
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
logError(e);
}
}
});
// if the desktop is not ready delay the icon creation and signal emissions
await Util.waitForStartupCompletion(indicator.cancellable);
const statusIcon = new IndicatorStatusIcon.IndicatorStatusIcon(indicator);
IndicatorStatusIcon.addIconToPanel(statusIcon);
this._dbusImpl.emit_signal('StatusNotifierItemRegistered',
GLib.Variant.new('(s)', [indicator.uniqueId]));
this._dbusImpl.emit_property_changed('RegisteredStatusNotifierItems',
GLib.Variant.new('as', this.RegisteredStatusNotifierItems));
} catch (e) {
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
logError(e);
throw e;
}
}
async _ensureItemRegistered(service, busName, objPath) {
const id = Util.indicatorId(service, busName, objPath);
const item = this._items.get(id);
if (item) {
// delete the old one and add the new indicator
Util.Logger.debug(`Attempting to re-register ${id}; resetting instead`);
item.reset();
return;
}
await this._registerItem(service, busName, objPath);
}
async _seekStatusNotifierItems() {
// Some indicators (*coff*, dropbox, *coff*) do not re-register again
// when the plugin is enabled/disabled, thus we need to manually look
// for the objects in the session bus that implements the
// StatusNotifierItem interface... However let's do it after a low
// priority idle, so that it won't affect startup.
const cancellable = this._cancellable;
const bus = Gio.DBus.session;
const uniqueNames = await Util.getBusNames(bus, cancellable);
const introspectName = async name => {
const nodes = Util.introspectBusObject(bus, name, cancellable,
['org.kde.StatusNotifierItem']);
const services = [...uniqueNames.get(name)];
for await (const node of nodes) {
const {path} = node;
const ids = services.map(s => Util.indicatorId(s, name, path));
if (ids.every(id => !this._items.has(id))) {
const service = services.find(s =>
s && s.startsWith('org.kde.StatusNotifierItem')) || services[0];
const id = Util.indicatorId(
path === DEFAULT_ITEM_OBJECT_PATH ? service : null,
name, path);
Util.Logger.warn(`Using Brute-force mode for StatusNotifierItem ${id}`);
this._registerItem(service, name, path);
}
}
};
await Promise.allSettled([...uniqueNames.keys()].map(n => introspectName(n)));
}
async RegisterStatusNotifierItemAsync(params, invocation) {
// it would be too easy if all application behaved the same
// instead, ayatana patched gnome apps to send a path
// while kde apps send a bus name
const [service] = params;
let busName, objPath;
if (service.charAt(0) === '/') { // looks like a path
busName = invocation.get_sender();
objPath = service;
} else if (service.match(Util.BUS_ADDRESS_REGEX)) {
try {
busName = await Util.getUniqueBusName(invocation.get_connection(),
service, this._cancellable);
} catch (e) {
logError(e);
}
objPath = DEFAULT_ITEM_OBJECT_PATH;
}
if (!busName || !objPath) {
const error = `Impossible to register an indicator for parameters '${
service.toString()}'`;
Util.Logger.warn(error);
invocation.return_dbus_error('org.gnome.gjs.JSError.ValueError',
error);
return;
}
try {
await this._ensureItemRegistered(service, busName, objPath);
invocation.return_value(null);
} catch (e) {
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
logError(e);
invocation.return_dbus_error('org.gnome.gjs.JSError.ValueError',
e.message);
}
}
_onIndicatorDestroyed(indicator) {
const {uniqueId} = indicator;
this._items.delete(uniqueId);
try {
this._dbusImpl.emit_signal('StatusNotifierItemUnregistered',
GLib.Variant.new('(s)', [uniqueId]));
this._dbusImpl.emit_property_changed('RegisteredStatusNotifierItems',
GLib.Variant.new('as', this.RegisteredStatusNotifierItems));
} catch (e) {
Util.Logger.warn(`Failed to emit signals: ${e}`);
}
}
RegisterStatusNotifierHostAsync(_service, invocation) {
invocation.return_error_literal(
Gio.DBusError,
Gio.DBusError.NOT_SUPPORTED,
'Registering additional notification hosts is not supported');
}
IsNotificationHostRegistered() {
return true;
}
get RegisteredStatusNotifierItems() {
return Array.from(this._items.values()).map(i => i.uniqueId);
}
get IsStatusNotifierHostRegistered() {
return true;
}
get ProtocolVersion() {
return 0;
}
destroy() {
if (this._isDestroyed)
return;
// this doesn't do any sync operation and doesn't allow us to hook up
// the event of being finished which results in our unholy debounce hack
// (see extension.js)
this._items.forEach(indicator => indicator.destroy());
this._cancellable.cancel();
try {
this._dbusImpl.emit_signal('StatusNotifierHostUnregistered', null);
} catch (e) {
Util.Logger.warn(`Failed to emit uinregistered signal: ${e}`);
}
Gio.DBus.session.unown_name(this._ownName);
try {
this._dbusImpl.unexport();
} catch (e) {
Util.Logger.warn(`Failed to unexport watcher object: ${e}`);
}
DBusMenu.DBusClient.destroy();
AppIndicator.AppIndicatorProxy.destroy();
DBusProxy.destroy();
Util.destroyDefaultTheme();
this._dbusImpl.run_dispose();
delete this._dbusImpl;
delete this._items;
this._isDestroyed = true;
}
}

View File

@ -0,0 +1,104 @@
// This file is part of the AppIndicator/KStatusNotifierItem GNOME Shell extension
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
import Shell from 'gi://Shell';
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
import * as Signals from 'resource:///org/gnome/shell/misc/signals.js';
import * as IndicatorStatusIcon from './indicatorStatusIcon.js';
import * as Util from './util.js';
import * as SettingsManager from './settingsManager.js';
let trayIconsManager;
export class TrayIconsManager extends Signals.EventEmitter {
static initialize() {
if (!trayIconsManager)
trayIconsManager = new TrayIconsManager();
return trayIconsManager;
}
static destroy() {
trayIconsManager.destroy();
}
constructor() {
super();
if (trayIconsManager)
throw new Error('TrayIconsManager is already constructed');
this._changedId = SettingsManager.getDefaultGSettings().connect(
'changed::legacy-tray-enabled', () => this._toggle());
this._toggle();
}
_toggle() {
if (SettingsManager.getDefaultGSettings().get_boolean('legacy-tray-enabled'))
this._enable();
else
this._disable();
}
_enable() {
if (this._tray)
return;
this._tray = new Shell.TrayManager();
Util.connectSmart(this._tray, 'tray-icon-added', this, this.onTrayIconAdded);
Util.connectSmart(this._tray, 'tray-icon-removed', this, this.onTrayIconRemoved);
this._tray.manage_screen(Main.panel);
}
_disable() {
if (!this._tray)
return;
IndicatorStatusIcon.getTrayIcons().forEach(i => i.destroy());
if (this._tray.unmanage_screen) {
this._tray.unmanage_screen();
this._tray = null;
} else {
// FIXME: This is very ugly, but it's needed by old shell versions
this._tray = null;
imports.system.gc(); // force finalizing tray to unmanage screen
}
}
onTrayIconAdded(_tray, icon) {
const trayIcon = new IndicatorStatusIcon.IndicatorStatusTrayIcon(icon);
IndicatorStatusIcon.addIconToPanel(trayIcon);
}
onTrayIconRemoved(_tray, icon) {
try {
const [trayIcon] = IndicatorStatusIcon.getTrayIcons().filter(i => i.icon === icon);
trayIcon.destroy();
} catch (e) {
Util.Logger.warning(`No icon container found for ${icon.title} (${icon})`);
}
}
destroy() {
this.emit('destroy');
SettingsManager.getDefaultGSettings().disconnect(this._changedId);
this._disable();
trayIconsManager = null;
}
}

View File

@ -0,0 +1,447 @@
// This file is part of the AppIndicator/KStatusNotifierItem GNOME Shell extension
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import St from 'gi://St';
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
import * as Config from 'resource:///org/gnome/shell/misc/config.js';
import * as Signals from 'resource:///org/gnome/shell/misc/signals.js';
import {BaseStatusIcon} from './indicatorStatusIcon.js';
export const BUS_ADDRESS_REGEX = /([a-zA-Z0-9._-]+\.[a-zA-Z0-9.-]+)|(:[0-9]+\.[0-9]+)$/;
Gio._promisify(Gio.DBusConnection.prototype, 'call');
Gio._promisify(Gio._LocalFilePrototype, 'read');
Gio._promisify(Gio.InputStream.prototype, 'read_bytes_async');
export function indicatorId(service, busName, objectPath) {
if (service !== busName && service?.match(BUS_ADDRESS_REGEX))
return service;
return `${busName}@${objectPath}`;
}
export async function getUniqueBusName(bus, name, cancellable) {
if (name[0] === ':')
return name;
if (!bus)
bus = Gio.DBus.session;
const variantName = new GLib.Variant('(s)', [name]);
const [unique] = (await bus.call('org.freedesktop.DBus', '/', 'org.freedesktop.DBus',
'GetNameOwner', variantName, new GLib.VariantType('(s)'),
Gio.DBusCallFlags.NONE, -1, cancellable)).deep_unpack();
return unique;
}
export async function getBusNames(bus, cancellable) {
if (!bus)
bus = Gio.DBus.session;
const [names] = (await bus.call('org.freedesktop.DBus', '/', 'org.freedesktop.DBus',
'ListNames', null, new GLib.VariantType('(as)'), Gio.DBusCallFlags.NONE,
-1, cancellable)).deep_unpack();
const uniqueNames = new Map();
const requests = names.map(name => getUniqueBusName(bus, name, cancellable));
const results = await Promise.allSettled(requests);
for (let i = 0; i < results.length; i++) {
const result = results[i];
if (result.status === 'fulfilled') {
let namesForBus = uniqueNames.get(result.value);
if (!namesForBus) {
namesForBus = new Set();
uniqueNames.set(result.value, namesForBus);
}
namesForBus.add(result.value !== names[i] ? names[i] : null);
} else if (!result.reason.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) {
Logger.debug(`Impossible to get the unique name of ${names[i]}: ${result.reason}`);
}
}
return uniqueNames;
}
async function getProcessId(connectionName, cancellable = null, bus = Gio.DBus.session) {
const res = await bus.call('org.freedesktop.DBus', '/',
'org.freedesktop.DBus', 'GetConnectionUnixProcessID',
new GLib.Variant('(s)', [connectionName]),
new GLib.VariantType('(u)'),
Gio.DBusCallFlags.NONE,
-1,
cancellable);
const [pid] = res.deepUnpack();
return pid;
}
export async function getProcessName(connectionName, cancellable = null,
priority = GLib.PRIORITY_DEFAULT, bus = Gio.DBus.session) {
const pid = await getProcessId(connectionName, cancellable, bus);
const cmdFile = Gio.File.new_for_path(`/proc/${pid}/cmdline`);
const inputStream = await cmdFile.read_async(priority, cancellable);
const bytes = await inputStream.read_bytes_async(2048, priority, cancellable);
const textDecoder = new TextDecoder();
return textDecoder.decode(bytes.toArray().map(v => !v ? 0x20 : v));
}
export async function* introspectBusObject(bus, name, cancellable,
interfaces = undefined, path = undefined) {
if (!path)
path = '/';
const [introspection] = (await bus.call(name, path, 'org.freedesktop.DBus.Introspectable',
'Introspect', null, new GLib.VariantType('(s)'), Gio.DBusCallFlags.NONE,
5000, cancellable)).deep_unpack();
const nodeInfo = Gio.DBusNodeInfo.new_for_xml(introspection);
if (!interfaces || dbusNodeImplementsInterfaces(nodeInfo, interfaces))
yield {nodeInfo, path};
if (path === '/')
path = '';
for (const subNodeInfo of nodeInfo.nodes) {
const subPath = `${path}/${subNodeInfo.path}`;
yield* introspectBusObject(bus, name, cancellable, interfaces, subPath);
}
}
function dbusNodeImplementsInterfaces(nodeInfo, interfaces) {
if (!(nodeInfo instanceof Gio.DBusNodeInfo) || !Array.isArray(interfaces))
return false;
return interfaces.some(iface => nodeInfo.lookup_interface(iface));
}
export class NameWatcher extends Signals.EventEmitter {
constructor(name) {
super();
this._watcherId = Gio.DBus.session.watch_name(name,
Gio.BusNameWatcherFlags.NONE, () => {
this._nameOnBus = true;
Logger.debug(`Name ${name} appeared`);
this.emit('changed');
this.emit('appeared');
}, () => {
this._nameOnBus = false;
Logger.debug(`Name ${name} vanished`);
this.emit('changed');
this.emit('vanished');
});
}
destroy() {
this.emit('destroy');
Gio.DBus.session.unwatch_name(this._watcherId);
delete this._watcherId;
}
get nameOnBus() {
return !!this._nameOnBus;
}
}
function connectSmart3A(src, signal, handler) {
const id = src.connect(signal, handler);
let destroyId = 0;
if (src.connect && (!(src instanceof GObject.Object) || GObject.signal_lookup('destroy', src))) {
destroyId = src.connect('destroy', () => {
src.disconnect(id);
src.disconnect(destroyId);
});
}
return [id, destroyId];
}
function connectSmart4A(src, signal, target, method) {
if (typeof method !== 'function')
throw new TypeError('Unsupported function');
method = method.bind(target);
const signalId = src.connect(signal, method);
const onDestroy = () => {
src.disconnect(signalId);
if (srcDestroyId)
src.disconnect(srcDestroyId);
if (tgtDestroyId)
target.disconnect(tgtDestroyId);
};
// GObject classes might or might not have a destroy signal
// JS Classes will not complain when connecting to non-existent signals
const srcDestroyId = src.connect && (!(src instanceof GObject.Object) ||
GObject.signal_lookup('destroy', src)) ? src.connect('destroy', onDestroy) : 0;
const tgtDestroyId = target.connect && (!(target instanceof GObject.Object) ||
GObject.signal_lookup('destroy', target)) ? target.connect('destroy', onDestroy) : 0;
return [signalId, srcDestroyId, tgtDestroyId];
}
// eslint-disable-next-line valid-jsdoc
/**
* Connect signals to slots, and remove the connection when either source or
* target are destroyed
*
* Usage:
* Util.connectSmart(srcOb, 'signal', tgtObj, 'handler')
* or
* Util.connectSmart(srcOb, 'signal', () => { ... })
*/
export function connectSmart(...args) {
if (arguments.length === 4)
return connectSmart4A(...args);
else
return connectSmart3A(...args);
}
function disconnectSmart3A(src, signalIds) {
const [id, destroyId] = signalIds;
src.disconnect(id);
if (destroyId)
src.disconnect(destroyId);
}
function disconnectSmart4A(src, tgt, signalIds) {
const [signalId, srcDestroyId, tgtDestroyId] = signalIds;
disconnectSmart3A(src, [signalId, srcDestroyId]);
if (tgtDestroyId)
tgt.disconnect(tgtDestroyId);
}
export function disconnectSmart(...args) {
if (arguments.length === 2)
return disconnectSmart3A(...args);
else if (arguments.length === 3)
return disconnectSmart4A(...args);
throw new TypeError('Unexpected number of arguments');
}
let _defaultTheme;
export function getDefaultTheme() {
if (_defaultTheme)
return _defaultTheme;
_defaultTheme = new St.IconTheme();
return _defaultTheme;
}
export function destroyDefaultTheme() {
_defaultTheme = null;
}
// eslint-disable-next-line valid-jsdoc
/**
* Helper function to wait for the system startup to be completed.
* Adding widgets before the desktop is ready to accept them can result in errors.
*/
export async function waitForStartupCompletion(cancellable) {
if (Main.layoutManager._startingUp)
await Main.layoutManager.connect_once('startup-complete', cancellable);
}
/**
* Helper class for logging stuff
*/
export class Logger {
static _logStructured(logLevel, message, extraFields = {}) {
if (!Object.values(GLib.LogLevelFlags).includes(logLevel)) {
Logger._logStructured(GLib.LogLevelFlags.LEVEL_WARNING,
'logLevel is not a valid GLib.LogLevelFlags');
return;
}
if (!Logger._levels.includes(logLevel))
return;
let fields = {
'SYSLOG_IDENTIFIER': this.uuid,
'MESSAGE': `${message}`,
};
let thisFile = null;
const {stack} = new Error();
for (let stackLine of stack.split('\n')) {
stackLine = stackLine.replace('resource:///org/gnome/Shell/', '');
const [code, line] = stackLine.split(':');
const [func, file] = code.split(/@(.+)/);
if (!thisFile || thisFile === file) {
thisFile = file;
continue;
}
fields = Object.assign(fields, {
'CODE_FILE': file || '',
'CODE_LINE': line || '',
'CODE_FUNC': func || '',
});
break;
}
GLib.log_structured(Logger._domain, logLevel, Object.assign(fields, extraFields));
}
static init(extension) {
if (Logger._domain)
return;
const allLevels = Object.values(GLib.LogLevelFlags);
const domains = GLib.getenv('G_MESSAGES_DEBUG');
const {name: domain} = extension.metadata;
this.uuid = extension.metadata.uuid;
Logger._domain = domain.replaceAll(' ', '-');
if (domains === 'all' || (domains && domains.split(' ').includes(Logger._domain))) {
Logger._levels = allLevels;
} else {
Logger._levels = allLevels.filter(
l => l <= GLib.LogLevelFlags.LEVEL_WARNING);
}
}
static debug(message) {
Logger._logStructured(GLib.LogLevelFlags.LEVEL_DEBUG, message);
}
static message(message) {
Logger._logStructured(GLib.LogLevelFlags.LEVEL_MESSAGE, message);
}
static warn(message) {
Logger._logStructured(GLib.LogLevelFlags.LEVEL_WARNING, message);
}
static error(message) {
Logger._logStructured(GLib.LogLevelFlags.LEVEL_ERROR, message);
}
static critical(message) {
Logger._logStructured(GLib.LogLevelFlags.LEVEL_CRITICAL, message);
}
}
export function versionCheck(required) {
const current = Config.PACKAGE_VERSION;
const currentArray = current.split('.');
const [major] = currentArray;
return major >= required;
}
export function tryCleanupOldIndicators() {
const indicatorType = BaseStatusIcon;
const indicators = Object.values(Main.panel.statusArea).filter(i => i instanceof indicatorType);
try {
const panelBoxes = [
Main.panel._leftBox, Main.panel._centerBox, Main.panel._rightBox,
];
panelBoxes.forEach(box =>
indicators.push(...box.get_children().filter(i => i instanceof indicatorType)));
} catch (e) {
logError(e);
}
new Set(indicators).forEach(i => i.destroy());
}
export function addActor(obj, actor) {
if (obj.add_actor)
obj.add_actor(actor);
else
obj.add_child(actor);
}
export function removeActor(obj, actor) {
if (obj.remove_actor)
obj.remove_actor(actor);
else
obj.remove_child(actor);
}
export const CancellableChild = GObject.registerClass({
Properties: {
'parent': GObject.ParamSpec.object(
'parent', 'parent', 'parent',
GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY,
Gio.Cancellable.$gtype),
},
},
class CancellableChild extends Gio.Cancellable {
_init(parent) {
if (parent && !(parent instanceof Gio.Cancellable))
throw TypeError('Not a valid cancellable');
super._init({parent});
if (parent) {
if (parent.is_cancelled()) {
this.cancel();
return;
}
this._connectToParent();
}
}
_connectToParent() {
this._connectId = this.parent.connect(() => {
this._realCancel();
if (this._disconnectIdle)
return;
this._disconnectIdle = GLib.idle_add(GLib.PRIORITY_DEFAULT, () => {
delete this._disconnectIdle;
this._disconnectFromParent();
return GLib.SOURCE_REMOVE;
});
});
}
_disconnectFromParent() {
if (this._connectId && !this._disconnectIdle) {
this.parent.disconnect(this._connectId);
delete this._connectId;
}
}
_realCancel() {
Gio.Cancellable.prototype.cancel.call(this);
}
cancel() {
this._disconnectFromParent();
this._realCancel();
}
});

View File

@ -0,0 +1,232 @@
// App Menu Is Back
// GNOME Shell extension
// @fthx 2024
// Almost all the code comes from GS 44 original code
import Atk from 'gi://Atk';
import Clutter from 'gi://Clutter';
import GObject from 'gi://GObject';
import Shell from 'gi://Shell';
import St from 'gi://St';
import * as Animation from 'resource:///org/gnome/shell/ui/animation.js';
import * as AppMenu from 'resource:///org/gnome/shell/ui/appMenu.js';
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
import * as Overview from 'resource:///org/gnome/shell/ui/overview.js';
import * as PanelMenu from 'resource:///org/gnome/shell/ui/panelMenu.js';
const PANEL_ICON_SIZE = 16;
const APP_MENU_ICON_MARGIN = 0;
const AppMenuButton = GObject.registerClass({
Signals: { 'changed': {} },
}, class AppMenuButton extends PanelMenu.Button {
_init() {
super._init(0.0, null, true);
this.accessible_role = Atk.Role.MENU;
this._startingApps = [];
this._menuManager = Main.panel.menuManager;
this._targetApp = null;
let bin = new St.Bin({ name: 'appMenu' });
this.add_child(bin);
this.bind_property("reactive", this, "can-focus", 0);
this.reactive = false;
this._container = new St.BoxLayout({
style_class: 'panel-status-menu-box',
});
bin.set_child(this._container);
let textureCache = St.TextureCache.get_default();
textureCache.connect('icon-theme-changed',
this._onIconThemeChanged.bind(this));
let iconEffect = new Clutter.DesaturateEffect();
this._iconBox = new St.Bin({
style_class: 'app-menu-icon',
y_align: Clutter.ActorAlign.CENTER,
});
this._iconBox.add_effect(iconEffect);
this._container.add_child(this._iconBox);
this._iconBox.connect('style-changed', () => {
let themeNode = this._iconBox.get_theme_node();
iconEffect.enabled = themeNode.get_icon_style() == St.IconStyle.SYMBOLIC;
});
this._label = new St.Label({
y_expand: true,
y_align: Clutter.ActorAlign.CENTER,
});
this._container.add_child(this._label);
this._visible = !Main.overview.visible;
if (!this._visible)
this.hide();
Main.overview.connectObject(
'hiding', this._sync.bind(this),
'showing', this._sync.bind(this), this);
this._spinner = new Animation.Spinner(PANEL_ICON_SIZE, {
animate: true,
hideOnStop: true,
});
this._container.add_child(this._spinner);
let menu = new AppMenu.AppMenu(this);
this.setMenu(menu);
this._menuManager.addMenu(menu);
Shell.WindowTracker.get_default().connectObject('notify::focus-app',
this._focusAppChanged.bind(this), this);
Shell.AppSystem.get_default().connectObject('app-state-changed',
this._onAppStateChanged.bind(this), this);
global.window_manager.connectObject('switch-workspace',
this._sync.bind(this), this);
this._sync();
}
fadeIn() {
if (this._visible)
return;
this._visible = true;
this.reactive = true;
this.remove_all_transitions();
this.ease({
opacity: 255,
duration: Overview.ANIMATION_TIME,
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
});
}
fadeOut() {
if (!this._visible)
return;
this._visible = false;
this.reactive = false;
this.remove_all_transitions();
this.ease({
opacity: 0,
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
duration: Overview.ANIMATION_TIME,
});
}
_syncIcon(app) {
const icon = app.create_icon_texture(PANEL_ICON_SIZE - APP_MENU_ICON_MARGIN);
this._iconBox.set_child(icon);
}
_onIconThemeChanged() {
if (this._iconBox.child == null)
return;
if (this._targetApp)
this._syncIcon(this._targetApp);
}
stopAnimation() {
this._spinner.stop();
}
startAnimation() {
this._spinner.play();
}
_onAppStateChanged(appSys, app) {
let state = app.state;
if (state != Shell.AppState.STARTING)
this._startingApps = this._startingApps.filter(a => a != app);
else if (state == Shell.AppState.STARTING)
this._startingApps.push(app);
this._sync();
}
_focusAppChanged() {
let tracker = Shell.WindowTracker.get_default();
let focusedApp = tracker.focus_app;
if (!focusedApp) {
if (global.stage.key_focus != null)
return;
}
this._sync();
}
_findTargetApp() {
let workspaceManager = global.workspace_manager;
let workspace = workspaceManager.get_active_workspace();
let tracker = Shell.WindowTracker.get_default();
let focusedApp = tracker.focus_app;
if (focusedApp && focusedApp.is_on_workspace(workspace))
return focusedApp;
for (let i = 0; i < this._startingApps.length; i++) {
if (this._startingApps[i].is_on_workspace(workspace))
return this._startingApps[i];
}
return null;
}
_sync() {
let targetApp = this._findTargetApp();
if (this._targetApp != targetApp) {
this._targetApp?.disconnectObject(this);
this._targetApp = targetApp;
if (this._targetApp) {
this._targetApp.connectObject('notify::busy', this._sync.bind(this), this);
this._label.set_text(this._targetApp.get_name());
this.set_accessible_name(this._targetApp.get_name());
this._syncIcon(this._targetApp);
}
}
let visible = this._targetApp != null && !Main.overview.visibleTarget;
if (visible)
this.fadeIn();
else
this.fadeOut();
let isBusy = this._targetApp != null &&
(this._targetApp.get_state() == Shell.AppState.STARTING ||
this._targetApp.get_busy());
if (isBusy)
this.startAnimation();
else
this.stopAnimation();
this.reactive = visible && !isBusy;
this.menu.setApp(this._targetApp);
this.emit('changed');
}
});
export default class AppMenuIsBackExtension {
enable() {
this._app_menu = new AppMenuButton();
Main.panel.addToStatusArea('appmenu-indicator', this._app_menu, -1, 'left');
}
disable() {
Main.panel.menuManager.removeMenu(this._app_menu.menu);
this._app_menu.menu = null;
this._app_menu.destroy();
delete this._app_menu;
}
}

View File

@ -0,0 +1,11 @@
{
"_generated": "Generated by SweetTooth, do not edit",
"description": "The good old original app menu. For GNOME Shell 45+.\n\n Code mostly copied from GNOME Shell code itself. For a customizable app menu, please consider 'Window title is back' extension.",
"name": "App menu is back",
"shell-version": [
"46"
],
"url": "https://github.com/fthx/appmenu-is-back",
"uuid": "appmenu-is-back@fthx",
"version": 3
}

View File

@ -0,0 +1,255 @@
import Shell from 'gi://Shell';
import Clutter from 'gi://Clutter';
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
import { PaintSignals } from '../conveniences/paint_signals.js';
// TODO drop Tweener in favour of Clutter's `ease` (will need to extend the blur effect for it)
const Tweener = imports.tweener.tweener;
const transparent = Clutter.Color.from_pixel(0x00000000);
const FOLDER_DIALOG_ANIMATION_TIME = 200;
const DIALOGS_STYLES = [
"appfolder-dialogs-transparent",
"appfolder-dialogs-light",
"appfolder-dialogs-dark"
];
let original_zoomAndFadeIn = null;
let original_zoomAndFadeOut = null;
let sigma;
let brightness;
let _zoomAndFadeIn = function () {
let [sourceX, sourceY] =
this._source.get_transformed_position();
let [dialogX, dialogY] =
this.child.get_transformed_position();
this.child.set({
translation_x: sourceX - dialogX,
translation_y: sourceY - dialogY,
scale_x: this._source.width / this.child.width,
scale_y: this._source.height / this.child.height,
opacity: 0,
});
this.set_background_color(transparent);
let blur_effect = this.get_effect("appfolder-blur");
blur_effect.radius = 0;
blur_effect.brightness = 1.0;
Tweener.addTween(blur_effect,
{
radius: sigma * 2,
brightness: brightness,
time: FOLDER_DIALOG_ANIMATION_TIME / 1000,
transition: 'easeOutQuad'
}
);
this.child.ease({
translation_x: 0,
translation_y: 0,
scale_x: 1,
scale_y: 1,
opacity: 255,
duration: FOLDER_DIALOG_ANIMATION_TIME,
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
});
this._needsZoomAndFade = false;
if (this._sourceMappedId === 0) {
this._sourceMappedId = this._source.connect(
'notify::mapped', this._zoomAndFadeOut.bind(this));
}
};
let _zoomAndFadeOut = function () {
if (!this._isOpen)
return;
if (!this._source.mapped) {
this.hide();
return;
}
let [sourceX, sourceY] =
this._source.get_transformed_position();
let [dialogX, dialogY] =
this.child.get_transformed_position();
this.set_background_color(transparent);
let blur_effect = this.get_effect("appfolder-blur");
Tweener.addTween(blur_effect,
{
radius: 0,
brightness: 1.0,
time: FOLDER_DIALOG_ANIMATION_TIME / 1000,
transition: 'easeInQuad'
}
);
this.child.ease({
translation_x: sourceX - dialogX,
translation_y: sourceY - dialogY,
scale_x: this._source.width / this.child.width,
scale_y: this._source.height / this.child.height,
opacity: 0,
duration: FOLDER_DIALOG_ANIMATION_TIME,
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
onComplete: () => {
this.child.set({
translation_x: 0,
translation_y: 0,
scale_x: 1,
scale_y: 1,
opacity: 255,
});
this.hide();
this._popdownCallbacks.forEach(func => func());
this._popdownCallbacks = [];
},
});
this._needsZoomAndFade = false;
};
export const AppFoldersBlur = class AppFoldersBlur {
// we do not use the effects manager and dummy pipelines here because we
// really want to manage our sigma value ourself during the transition
constructor(connections, settings, _) {
this.connections = connections;
this.paint_signals = new PaintSignals(connections);
this.settings = settings;
}
enable() {
this._log("blurring appfolders");
brightness = this.settings.appfolder.BRIGHTNESS;
sigma = this.settings.appfolder.SIGMA;
let appDisplay = Main.overview._overview.controls._appDisplay;
if (appDisplay._folderIcons.length > 0) {
this.blur_appfolders();
}
this.connections.connect(
appDisplay, 'view-loaded', _ => this.blur_appfolders()
);
}
blur_appfolders() {
let appDisplay = Main.overview._overview.controls._appDisplay;
if (this.settings.HACKS_LEVEL === 1)
this._log("appfolders hack level 1");
appDisplay._folderIcons.forEach(icon => {
icon._ensureFolderDialog();
if (original_zoomAndFadeIn == null) {
original_zoomAndFadeIn = icon._dialog._zoomAndFadeIn;
}
if (original_zoomAndFadeOut == null) {
original_zoomAndFadeOut = icon._dialog._zoomAndFadeOut;
}
let blur_effect = new Shell.BlurEffect({
name: "appfolder-blur",
radius: sigma * 2,
brightness: brightness,
mode: Shell.BlurMode.BACKGROUND
});
icon._dialog.remove_effect_by_name("appfolder-blur");
icon._dialog.add_effect(blur_effect);
DIALOGS_STYLES.forEach(
style => icon._dialog._viewBox.remove_style_class_name(style)
);
if (this.settings.appfolder.STYLE_DIALOGS > 0)
icon._dialog._viewBox.add_style_class_name(
DIALOGS_STYLES[this.settings.appfolder.STYLE_DIALOGS - 1]
);
// finally override the builtin functions
icon._dialog._zoomAndFadeIn = _zoomAndFadeIn;
icon._dialog._zoomAndFadeOut = _zoomAndFadeOut;
// 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 panel
// buttons to cause artifacts on the panel itself
//
// [1]: https://gitlab.gnome.org/GNOME/gnome-shell/-/issues/2857
if (this.settings.HACKS_LEVEL === 1) {
this.paint_signals.disconnect_all_for_actor(icon._dialog);
this.paint_signals.connect(icon._dialog, blur_effect);
} else {
this.paint_signals.disconnect_all();
}
});
};
set_sigma(s) {
sigma = s;
if (this.settings.appfolder.BLUR)
this.blur_appfolders();
}
set_brightness(b) {
brightness = b;
if (this.settings.appfolder.BLUR)
this.blur_appfolders();
}
disable() {
this._log("removing blur from appfolders");
let appDisplay = Main.overview._overview.controls._appDisplay;
if (original_zoomAndFadeIn != null) {
appDisplay._folderIcons.forEach(icon => {
if (icon._dialog)
icon._dialog._zoomAndFadeIn = original_zoomAndFadeIn;
});
}
if (original_zoomAndFadeOut != null) {
appDisplay._folderIcons.forEach(icon => {
if (icon._dialog)
icon._dialog._zoomAndFadeOut = original_zoomAndFadeOut;
});
}
appDisplay._folderIcons.forEach(icon => {
if (icon._dialog) {
icon._dialog.remove_effect_by_name("appfolder-blur");
DIALOGS_STYLES.forEach(
s => icon._dialog._viewBox.remove_style_class_name(s)
);
}
});
this.connections.disconnect_all();
}
_log(str) {
if (this.settings.DEBUG)
console.log(`[Blur my Shell > appfolders] ${str}`);
}
};

View File

@ -0,0 +1,451 @@
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}`);
}
};

View File

@ -0,0 +1,421 @@
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 { 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
) {
// 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.settings = dash_blur.settings;
this.old_style = this.dash._background.style;
this.dash_destroy_id = dash.connect('destroy', () => this.remove_dash_blur(false));
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.update_size()),
this.dash_blur.connect('change-blur-type', () => this.change_blur_type()),
this.dash_blur.connect('update-pipeline', () => this.update_pipeline())
);
}
// IMPORTANT: do never call this in a mutable `this.dash_blur.forEach`
remove_dash_blur(dash_not_already_destroyed = true) {
// remove the style and destroy the effects
this.remove_style();
this.destroy_dash(dash_not_already_destroyed);
// 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
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();
this.dash.set_style_class_name(
DASH_STYLES[this.settings.dash_to_dock.STYLE_DASH_TO_DOCK]
);
}
remove_style() {
this.dash._background.style = this.old_style;
DASH_STYLES.forEach(
style => this.dash.remove_style_class_name(style)
);
}
destroy_dash(dash_not_already_destroyed = true) {
if (!dash_not_already_destroyed)
this.bg_manager.backgroundActor = null;
this.paint_signals?.disconnect_all();
this.dash.get_parent().remove_child(this.background_group);
this.bg_manager._bms_pipeline.destroy();
this.bg_manager.destroy();
this.background_group.destroy();
}
change_blur_type() {
this.destroy_dash();
let [
background, background_group, bg_manager, paint_signals
] = this.dash_blur.add_blur(this.dash);
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);
this.update_size();
}
update_pipeline() {
this.bg_manager._bms_pipeline.change_pipeline_to(
this.settings.dash_to_dock.PIPELINE
);
}
update_size() {
if (this.dash_blur.is_static) {
let [x, y] = this.get_dash_position(this.dash_container, this.dash_background);
this.background.x = -x;
this.background.y = -y;
if (this.dash_container.get_style_class_name().includes("top"))
this.background.set_clip(
x,
y + this.dash.y + this.dash_background.y,
this.dash_background.width,
this.dash_background.height
);
else if (this.dash_container.get_style_class_name().includes("bottom"))
this.background.set_clip(
x,
y + this.dash.y + this.dash_background.y,
this.dash_background.width,
this.dash_background.height
);
else if (this.dash_container.get_style_class_name().includes("left"))
this.background.set_clip(
x + this.dash.x + this.dash_background.x,
y + this.dash.y + this.dash_background.y,
this.dash_background.width,
this.dash_background.height
);
else if (this.dash_container.get_style_class_name().includes("right"))
this.background.set_clip(
x + this.dash.x + this.dash_background.x,
y + this.dash.y + this.dash_background.y,
this.dash_background.width,
this.dash_background.height
);
} 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;
}
}
get_dash_position(dash_container, dash_background) {
var x, y;
let monitor = Main.layoutManager.findMonitorForActor(dash_container);
let dash_box = dash_container._slider.get_child();
if (dash_container.get_style_class_name().includes("top")) {
x = (monitor.width - dash_background.width) / 2;
y = dash_box.y;
} else if (dash_container.get_style_class_name().includes("bottom")) {
x = (monitor.width - dash_background.width) / 2;
y = monitor.height - dash_container.height;
} else if (dash_container.get_style_class_name().includes("left")) {
x = dash_box.x;
y = dash_container.y + (dash_container.height - dash_background.height) / 2 - dash_background.y;
} else if (dash_container.get_style_class_name().includes("right")) {
x = monitor.width - dash_container.width;
y = dash_container.y + (dash_container.height - dash_background.height) / 2 - dash_background.y;
}
return [x, 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));
}
// Tries to blur the dash contained in the given actor
try_blur(dash_container) {
let dash_box = dash_container._slider.get_child();
// verify that we did not already blur that dash
if (!dash_box.get_children().some(child =>
child.get_name() === "bms-dash-backgroundgroup"
)) {
this._log("dash to dock found, blurring it");
// finally blur the dash
let dash = dash_box.get_children().find(child => {
return child.get_name() === 'dash';
});
this.dashes.push(this.blur_dash_from(dash, dash_container));
}
}
// Blurs the dash and returns a `DashInfos` containing its information
blur_dash_from(dash, dash_container) {
let [background, background_group, bg_manager, paint_signals] = this.add_blur(dash);
// 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'],
_ => this.update_size()
);
const dash_background = dash.get_children().find(child => {
return child.get_style_class_name() === 'dash-background';
});
// create infos
let infos = new DashInfos(
this,
dash,
dash_container,
dash_background,
background,
background_group,
bg_manager,
paint_signals
);
this.update_size();
this.update_background();
// returns infos
return infos;
}
add_blur(dash) {
const monitor = Main.layoutManager.findMonitorForActor(dash);
if (!monitor)
return;
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');
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}`);
}
};

View File

@ -0,0 +1,89 @@
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
import { UnlockDialog } from 'resource:///org/gnome/shell/ui/unlockDialog.js';
import { Pipeline } from '../conveniences/pipeline.js';
const original_createBackground =
UnlockDialog.prototype._createBackground;
const original_updateBackgroundEffects =
UnlockDialog.prototype._updateBackgroundEffects;
const original_updateBackgrounds =
UnlockDialog.prototype._updateBackgrounds;
export const LockscreenBlur = class LockscreenBlur {
constructor(connections, settings, effects_manager) {
this.connections = connections;
this.settings = settings;
this.effects_manager = effects_manager;
this.enabled = false;
}
enable() {
this._log("blurring lockscreen");
this.update_lockscreen();
this.enabled = true;
}
update_lockscreen() {
UnlockDialog.prototype._createBackground =
this._createBackground;
UnlockDialog.prototype._updateBackgroundEffects =
this._updateBackgroundEffects;
UnlockDialog.prototype._updateBackgrounds =
this._updateBackgrounds;
}
_createBackground(monitor_index) {
let pipeline = new Pipeline(
global.blur_my_shell._effects_manager, global.blur_my_shell._pipelines_manager,
global.blur_my_shell._settings.lockscreen.PIPELINE
);
pipeline.create_background_with_effects(
monitor_index,
this._bgManagers,
this._backgroundGroup,
"screen-shield-background"
);
}
_updateBackgroundEffects() {
this._updateBackgrounds();
}
_updateBackgrounds() {
for (let i = 0; i < this._bgManagers.length; i++) {
this._bgManagers[i]._bms_pipeline.destroy();
this._bgManagers[i].destroy();
}
this._bgManagers = [];
this._backgroundGroup.destroy_all_children();
for (let i = 0; i < Main.layoutManager.monitors.length; i++)
this._createBackground(i);
}
disable() {
this._log("removing blur from lockscreen");
UnlockDialog.prototype._createBackground =
original_createBackground;
UnlockDialog.prototype._updateBackgroundEffects =
original_updateBackgroundEffects;
UnlockDialog.prototype._updateBackgrounds =
original_updateBackgrounds;
this.connections.disconnect_all();
this.enabled = false;
}
_log(str) {
if (this.settings.DEBUG)
console.log(`[Blur my Shell > lockscreen] ${str}`);
}
};

View File

@ -0,0 +1,209 @@
import Meta from 'gi://Meta';
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
import { WorkspaceAnimationController } from 'resource:///org/gnome/shell/ui/workspaceAnimation.js';
const wac_proto = WorkspaceAnimationController.prototype;
import { Pipeline } from '../conveniences/pipeline.js';
const OVERVIEW_COMPONENTS_STYLE = [
"overview-components-light",
"overview-components-dark",
"overview-components-transparent"
];
export const OverviewBlur = class OverviewBlur {
constructor(connections, settings, effects_manager) {
this.connections = connections;
this.settings = settings;
this.effects_manager = effects_manager;
this.overview_background_managers = [];
this.overview_background_group = new Meta.BackgroundGroup(
{ name: 'bms-overview-backgroundgroup' }
);
this.animation_background_managers = [];
this.animation_background_group = new Meta.BackgroundGroup(
{ name: 'bms-animation-backgroundgroup' }
);
this.enabled = false;
}
enable() {
this._log("blurring overview");
// add css class name for workspace-switch background
Main.uiGroup.add_style_class_name("blurred-overview");
// add css class name to make components semi-transparent if wanted
this.update_components_classname();
// update backgrounds when the component is enabled
this.update_backgrounds();
// connect to monitors change
this.connections.connect(Main.layoutManager, 'monitors-changed',
_ => this.update_backgrounds()
);
// part for the workspace animation switch
// make sure not to do this part if the extension was enabled prior, as
// the functions would call themselves and cause infinite recursion
if (!this.enabled) {
// store original workspace switching methods for restoring them on
// disable()
this._original_PrepareSwitch = wac_proto._prepareWorkspaceSwitch;
this._original_FinishSwitch = wac_proto._finishWorkspaceSwitch;
const w_m = global.workspace_manager;
const outer_this = this;
// create a blurred background actor for each monitor during a
// workspace switch
wac_proto._prepareWorkspaceSwitch = function (...params) {
outer_this._log("prepare workspace switch");
outer_this._original_PrepareSwitch.apply(this, params);
// this permits to show the blur behind windows that are on
// workspaces on the left and right
if (
outer_this.settings.applications.BLUR
) {
let ws_index = w_m.get_active_workspace_index();
[ws_index - 1, ws_index + 1].forEach(
i => w_m.get_workspace_by_index(i)?.list_windows().forEach(
window => window.get_compositor_private().show()
)
);
}
Main.uiGroup.insert_child_above(
outer_this.animation_background_group,
global.window_group
);
outer_this.animation_background_managers.forEach(bg_manager => {
if (bg_manager._bms_pipeline.actor)
if (
Meta.prefs_get_workspaces_only_on_primary() &&
bg_manager._monitorIndex !== Main.layoutManager.primaryMonitor.index
)
bg_manager._bms_pipeline.actor.visible = false;
else
bg_manager._bms_pipeline.actor.visible = true;
});
};
// remove the workspace-switch actors when the switch is done
wac_proto._finishWorkspaceSwitch = function (...params) {
outer_this._log("finish workspace switch");
outer_this._original_FinishSwitch.apply(this, params);
// this hides windows that are not on the current workspace
if (
outer_this.settings.applications.BLUR
)
for (let i = 0; i < w_m.get_n_workspaces(); i++) {
if (i != w_m.get_active_workspace_index())
w_m.get_workspace_by_index(i)?.list_windows().forEach(
window => window.get_compositor_private().hide()
);
}
Main.uiGroup.remove_child(outer_this.animation_background_group);
};
}
this.enabled = true;
}
update_backgrounds() {
// remove every old background
this.remove_background_actors();
// create new backgrounds for the overview and the animation
for (let i = 0; i < Main.layoutManager.monitors.length; i++) {
const pipeline_overview = new Pipeline(
this.effects_manager,
global.blur_my_shell._pipelines_manager,
this.settings.overview.PIPELINE
);
pipeline_overview.create_background_with_effects(
i, this.overview_background_managers,
this.overview_background_group, 'bms-overview-blurred-widget'
);
const pipeline_animation = new Pipeline(
this.effects_manager,
global.blur_my_shell._pipelines_manager,
this.settings.overview.PIPELINE
);
pipeline_animation.create_background_with_effects(
i, this.animation_background_managers,
this.animation_background_group, 'bms-animation-blurred-widget'
);
}
// add the container widget for the overview only to the overview group
Main.layoutManager.overviewGroup.insert_child_at_index(this.overview_background_group, 0);
}
/// Updates the classname to style overview components with semi-transparent
/// backgrounds.
update_components_classname() {
OVERVIEW_COMPONENTS_STYLE.forEach(
style => Main.uiGroup.remove_style_class_name(style)
);
if (this.settings.overview.STYLE_COMPONENTS > 0)
Main.uiGroup.add_style_class_name(
OVERVIEW_COMPONENTS_STYLE[this.settings.overview.STYLE_COMPONENTS - 1]
);
}
remove_background_actors() {
this.overview_background_group.remove_all_children();
this.animation_background_group.remove_all_children();
this.overview_background_managers.forEach(background_manager => {
background_manager._bms_pipeline.destroy();
background_manager.destroy();
});
this.animation_background_managers.forEach(background_manager => {
background_manager._bms_pipeline.destroy();
background_manager.destroy();
});
this.overview_background_managers = [];
this.animation_background_managers = [];
}
disable() {
this._log("removing blur from overview");
this.remove_background_actors();
Main.uiGroup.remove_style_class_name("blurred-overview");
OVERVIEW_COMPONENTS_STYLE.forEach(
style => Main.uiGroup.remove_style_class_name(style)
);
// make sure to absolutely not do this if the component was not enabled
// prior, as this would cause infinite recursion
if (this.enabled) {
// restore original behavior
if (this._original_PrepareSwitch)
wac_proto._prepareWorkspaceSwitch = this._original_PrepareSwitch;
if (this._original_FinishSwitch)
wac_proto._finishWorkspaceSwitch = this._original_FinishSwitch;
}
this.connections.disconnect_all();
this.enabled = false;
}
_log(str) {
if (this.settings.DEBUG)
console.log(`[Blur my Shell > overview] ${str}`);
}
_warn(str) {
console.warn(`[Blur my Shell > overview] ${str}`);
}
};

View File

@ -0,0 +1,542 @@
import St from 'gi://St';
import Meta from 'gi://Meta';
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
import { PaintSignals } from '../conveniences/paint_signals.js';
import { Pipeline } from '../conveniences/pipeline.js';
import { DummyPipeline } from '../conveniences/dummy_pipeline.js';
const DASH_TO_PANEL_UUID = 'dash-to-panel@jderose9.github.com';
const PANEL_STYLES = [
"transparent-panel",
"light-panel",
"dark-panel",
"contrasted-panel"
];
export const PanelBlur = class PanelBlur {
constructor(connections, settings, effects_manager) {
this.connections = connections;
this.window_signal_ids = new Map();
this.settings = settings;
this.effects_manager = effects_manager;
this.actors_list = [];
this.enabled = false;
}
enable() {
this._log("blurring top panel");
// check for panels when Dash to Panel is activated
this.connections.connect(
Main.extensionManager,
'extension-state-changed',
(_, extension) => {
if (extension.uuid === DASH_TO_PANEL_UUID
&& extension.state === 1
) {
this.connections.connect(
global.dashToPanel,
'panels-created',
_ => this.blur_dtp_panels()
);
this.blur_existing_panels();
}
}
);
this.blur_existing_panels();
// connect to overview being opened/closed, and dynamically show or not
// the blur when a window is near a panel
this.connect_to_windows_and_overview();
// update the classname if the panel to have or have not light text
this.update_light_text_classname();
// connect to monitors change
this.connections.connect(Main.layoutManager, 'monitors-changed',
_ => this.reset()
);
this.enabled = true;
}
reset() {
this._log("resetting...");
this.disable();
setTimeout(_ => this.enable(), 1);
}
/// Check for already existing panels and blur them if they are not already
blur_existing_panels() {
// check if dash-to-panel is present
if (global.dashToPanel) {
// blur already existing ones
if (global.dashToPanel.panels)
this.blur_dtp_panels();
} else {
// if no dash-to-panel, blur the main and only panel
this.maybe_blur_panel(Main.panel);
}
}
blur_dtp_panels() {
// FIXME when Dash to Panel changes its size, it seems it creates new
// panels; but I can't get to delete old widgets
// blur every panel found
global.dashToPanel.panels.forEach(p => {
this.maybe_blur_panel(p.panel);
});
// if main panel is not included in the previous panels, blur it
if (
!global.dashToPanel.panels
.map(p => p.panel)
.includes(Main.panel)
&&
this.settings.dash_to_panel.BLUR_ORIGINAL_PANEL
)
this.maybe_blur_panel(Main.panel);
};
/// Blur a panel only if it is not already blurred (contained in the list)
maybe_blur_panel(panel) {
// check if the panel is contained in the list
let actors = this.actors_list.find(
actors => actors.widgets.panel == panel
);
if (!actors)
// if the actors is not blurred, blur it
this.blur_panel(panel);
}
/// Blur a panel
blur_panel(panel) {
let panel_box = panel.get_parent();
let is_dtp_panel = false;
if (!panel_box.name) {
is_dtp_panel = true;
panel_box = panel_box.get_parent();
}
let monitor = Main.layoutManager.findMonitorForActor(panel);
if (!monitor)
return;
let background_group = new Meta.BackgroundGroup(
{ name: 'bms-panel-backgroundgroup', width: 0, height: 0 }
);
let background, bg_manager;
let static_blur = this.settings.panel.STATIC_BLUR;
if (static_blur) {
let bg_manager_list = [];
const pipeline = new Pipeline(
this.effects_manager,
global.blur_my_shell._pipelines_manager,
this.settings.panel.PIPELINE
);
background = pipeline.create_background_with_effects(
monitor.index, bg_manager_list,
background_group, 'bms-panel-blurred-widget'
);
bg_manager = bg_manager_list[0];
}
else {
const pipeline = new DummyPipeline(this.effects_manager, this.settings.panel);
[background, bg_manager] = pipeline.create_background_with_effect(
background_group, 'bms-panel-blurred-widget'
);
let 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 panel
// buttons to cause artifacts on the panel itself
//
// [1]: https://gitlab.gnome.org/GNOME/gnome-shell/-/issues/2857
{
if (this.settings.HACKS_LEVEL === 1) {
this._log("panel hack level 1");
paint_signals.disconnect_all();
paint_signals.connect(background, pipeline.effect);
} else {
paint_signals.disconnect_all();
}
}
}
// insert the background group to the panel box
panel_box.insert_child_at_index(background_group, 0);
// the object that is used to remembering each elements that is linked to the blur effect
let actors = {
widgets: { panel, panel_box, background, background_group },
static_blur,
monitor,
bg_manager,
is_dtp_panel
};
this.actors_list.push(actors);
// update the size of the actor
this.update_size(actors);
// connect to panel, panel_box and its parent position or size change
// this should fire update_size every time one of its params change
this.connections.connect(
panel,
'notify::position',
_ => this.update_size(actors)
);
this.connections.connect(
panel_box,
['notify::size', 'notify::position'],
_ => this.update_size(actors)
);
this.connections.connect(
panel_box.get_parent(),
'notify::position',
_ => this.update_size(actors)
);
// connect to the panel getting destroyed
this.connections.connect(
panel,
'destroy',
_ => this.destroy_blur(actors, true)
);
}
update_size(actors) {
let panel = actors.widgets.panel;
let panel_box = actors.widgets.panel_box;
let background = actors.widgets.background;
let monitor = Main.layoutManager.findMonitorForActor(panel);
if (!monitor)
return;
let [width, height] = panel_box.get_size();
// if static blur, need to clip the background
if (actors.static_blur) {
// an alternative to panel.get_transformed_position, because it
// sometimes yields NaN (probably when the actor is not fully
// positionned yet)
let [p_x, p_y] = panel_box.get_position();
let [p_p_x, p_p_y] = panel_box.get_parent().get_position();
let x = p_x + p_p_x - monitor.x;
let y = p_y + p_p_y - monitor.y;
background.set_clip(x, y, width, height);
background.x = -x;
background.y = -y;
} else {
background.x = panel.x;
background.y = panel.y;
background.width = width;
background.height = height;
}
// update the monitor panel is on
actors.monitor = Main.layoutManager.findMonitorForActor(panel);
}
/// Connect when overview if opened/closed to hide/show the blur accordingly
///
/// If HIDETOPBAR is set, we need just to hide the blur when showing appgrid
/// (so no shadow is cropped)
connect_to_overview() {
// may be called when panel blur is disabled, if hidetopbar
// compatibility is toggled on/off
// if this is the case, do nothing as only the panel blur interfers with
// hidetopbar
if (
this.settings.panel.BLUR &&
this.settings.panel.UNBLUR_IN_OVERVIEW
) {
if (!this.settings.hidetopbar.COMPATIBILITY) {
this.connections.connect(
Main.overview, 'showing', _ => this.hide()
);
this.connections.connect(
Main.overview, 'hidden', _ => this.show()
);
} else {
let appDisplay = Main.overview._overview._controls._appDisplay;
this.connections.connect(
appDisplay, 'show', _ => this.hide()
);
this.connections.connect(
appDisplay, 'hide', _ => this.show()
);
this.connections.connect(
Main.overview, 'hidden', _ => this.show()
);
}
}
}
/// Connect to windows disable transparency when a window is too close
connect_to_windows() {
if (
this.settings.panel.OVERRIDE_BACKGROUND_DYNAMICALLY
) {
// connect to overview opening/closing
this.connections.connect(Main.overview, ['showing', 'hiding'],
_ => this.update_visibility()
);
// connect to session mode update
this.connections.connect(Main.sessionMode, 'updated',
_ => this.update_visibility()
);
// manage already-existing windows
for (const meta_window_actor of global.get_window_actors()) {
this.on_window_actor_added(
meta_window_actor.get_parent(), meta_window_actor
);
}
// manage windows at their creation/removal
this.connections.connect(global.window_group, 'child-added',
this.on_window_actor_added.bind(this)
);
this.connections.connect(global.window_group, 'child-removed',
this.on_window_actor_removed.bind(this)
);
// connect to a workspace change
this.connections.connect(global.window_manager, 'switch-workspace',
_ => this.update_visibility()
);
// perform early update
this.update_visibility();
} else {
// reset transparency for every panels
this.actors_list.forEach(
actors => this.set_should_override_panel(actors, true)
);
}
}
/// An helper to connect to both the windows and overview signals.
/// This is the only function that should be directly called, to prevent
/// inconsistencies with signals not being disconnected.
connect_to_windows_and_overview() {
this.disconnect_from_windows_and_overview();
this.connect_to_overview();
this.connect_to_windows();
}
/// Disconnect all the connections created by connect_to_windows
disconnect_from_windows_and_overview() {
// disconnect the connections to actors
for (const actor of [
Main.overview, Main.sessionMode,
global.window_group, global.window_manager,
Main.overview._overview._controls._appDisplay
]) {
this.connections.disconnect_all_for(actor);
}
// disconnect the connections from windows
for (const [actor, ids] of this.window_signal_ids) {
for (const id of ids) {
actor.disconnect(id);
}
}
this.window_signal_ids = new Map();
}
/// Update the css classname of the panel for light theme
update_light_text_classname(disable = false) {
if (this.settings.panel.FORCE_LIGHT_TEXT && !disable)
Main.panel.add_style_class_name("panel-light-text");
else
Main.panel.remove_style_class_name("panel-light-text");
}
/// Callback when a new window is added
on_window_actor_added(container, meta_window_actor) {
this.window_signal_ids.set(meta_window_actor, [
meta_window_actor.connect('notify::allocation',
_ => this.update_visibility()
),
meta_window_actor.connect('notify::visible',
_ => this.update_visibility()
)
]);
this.update_visibility();
}
/// Callback when a window is removed
on_window_actor_removed(container, meta_window_actor) {
for (const signalId of this.window_signal_ids.get(meta_window_actor)) {
meta_window_actor.disconnect(signalId);
}
this.window_signal_ids.delete(meta_window_actor);
this.update_visibility();
}
/// Update the visibility of the blur effect
update_visibility() {
if (
Main.panel.has_style_pseudo_class('overview')
|| !Main.sessionMode.hasWindows
) {
this.actors_list.forEach(
actors => this.set_should_override_panel(actors, true)
);
return;
}
if (!Main.layoutManager.primaryMonitor)
return;
// get all the windows in the active workspace that are visible
const workspace = global.workspace_manager.get_active_workspace();
const windows = workspace.list_windows().filter(meta_window =>
meta_window.showing_on_its_workspace()
&& !meta_window.is_hidden()
&& meta_window.get_window_type() !== Meta.WindowType.DESKTOP
// exclude Desktop Icons NG
&& meta_window.get_gtk_application_id() !== "com.rastersoft.ding"
&& meta_window.get_gtk_application_id() !== "com.desktop.ding"
);
// check if at least one window is near enough to each panel and act
// accordingly
const scale = St.ThemeContext.get_for_stage(global.stage).scale_factor;
this.actors_list
// do not apply for dtp panels, as it would only cause bugs and it
// can be done from its preferences anyway
.filter(actors => !actors.is_dtp_panel)
.forEach(actors => {
let panel = actors.widgets.panel;
let panel_top = panel.get_transformed_position()[1];
let panel_bottom = panel_top + panel.get_height();
// check if at least a window is near enough the panel
let window_overlap_panel = false;
windows.forEach(meta_window => {
let window_monitor_i = meta_window.get_monitor();
let same_monitor = actors.monitor.index == window_monitor_i;
let window_vertical_pos = meta_window.get_frame_rect().y;
// if so, and if in the same monitor, then it overlaps
if (same_monitor
&&
window_vertical_pos < panel_bottom + 5 * scale
)
window_overlap_panel = true;
});
// if no window overlaps, then the panel is transparent
this.set_should_override_panel(
actors, !window_overlap_panel
);
});
}
/// Choose wether or not the panel background should be overriden, in
/// respect to its argument and the `override-background` setting.
set_should_override_panel(actors, should_override) {
let panel = actors.widgets.panel;
PANEL_STYLES.forEach(style => panel.remove_style_class_name(style));
if (
this.settings.panel.OVERRIDE_BACKGROUND
&&
should_override
)
panel.add_style_class_name(
PANEL_STYLES[this.settings.panel.STYLE_PANEL]
);
}
update_pipeline() {
this.actors_list.forEach(actors =>
actors.bg_manager._bms_pipeline.change_pipeline_to(
this.settings.panel.PIPELINE
)
);
}
show() {
this.actors_list.forEach(actors => {
actors.widgets.background.show();
});
}
hide() {
this.actors_list.forEach(actors => {
actors.widgets.background.hide();
});
}
// IMPORTANT: do never call this in a mutable `this.actors_list.forEach`
destroy_blur(actors, panel_already_destroyed) {
this.set_should_override_panel(actors, false);
actors.bg_manager._bms_pipeline.destroy();
if (panel_already_destroyed)
actors.bg_manager.backgroundActor = null;
actors.bg_manager.destroy();
if (!panel_already_destroyed) {
actors.widgets.panel_box.remove_child(actors.widgets.background_group);
actors.widgets.background_group.destroy_all_children();
actors.widgets.background_group.destroy();
}
let index = this.actors_list.indexOf(actors);
if (index >= 0)
this.actors_list.splice(index, 1);
}
disable() {
this._log("removing blur from top panel");
this.disconnect_from_windows_and_overview();
this.update_light_text_classname(true);
const immutable_actors_list = [...this.actors_list];
immutable_actors_list.forEach(actors => this.destroy_blur(actors, false));
this.actors_list = [];
this.connections.disconnect_all();
this.enabled = false;
}
_log(str) {
if (this.settings.DEBUG)
console.log(`[Blur my Shell > panel] ${str}`);
}
_warn(str) {
console.warn(`[Blur my Shell > panel] ${str}`);
}
};

View File

@ -0,0 +1,110 @@
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
import { Pipeline } from '../conveniences/pipeline.js';
export const ScreenshotBlur = class ScreenshotBlur {
constructor(connections, settings, effects_manager) {
this.connections = connections;
this.settings = settings;
this.screenshot_background_managers = [];
this.effects_manager = effects_manager;
}
enable() {
this._log("blurring screenshot's window selector");
// connect to monitors change
this.connections.connect(Main.layoutManager, 'monitors-changed',
_ => this.update_backgrounds()
);
// update backgrounds when the component is enabled
this.update_backgrounds();
}
update_backgrounds() {
// remove every old background
this.remove_background_actors();
// create new backgrounds for the screenshot window selector
for (let i = 0; i < Main.screenshotUI._windowSelectors.length; i++) {
const window_selector = Main.screenshotUI._windowSelectors[i];
const pipeline = new Pipeline(
this.effects_manager,
global.blur_my_shell._pipelines_manager,
this.settings.screenshot.PIPELINE
);
pipeline.create_background_with_effects(
window_selector._monitorIndex, this.screenshot_background_managers,
window_selector, 'bms-screenshot-blurred-widget'
);
// prevent old `BackgroundActor` from being accessed, which creates a whole bug of logs
this.connections.connect(window_selector.get_parent(), 'destroy', _ => {
this.screenshot_background_managers.forEach(background_manager => {
if (background_manager.backgroundActor) {
let widget = background_manager.backgroundActor.get_parent();
let parent = widget?.get_parent();
if (parent == window_selector) {
background_manager._bms_pipeline.destroy();
parent.remove_child(widget);
}
}
background_manager.destroy();
});
window_selector.get_children().forEach(child => {
if (child.get_name() == 'bms-screenshot-blurred-widget')
window_selector.remove_child(child);
});
let index = this.screenshot_background_managers.indexOf(window_selector);
this.screenshot_background_managers.splice(index, 1);
});
}
}
update_pipeline() {
this.screenshot_background_managers.forEach(background_manager =>
background_manager._bms_pipeline.change_pipeline_to(
this.settings.screenshot.PIPELINE
)
);
}
remove_background_actors() {
this.screenshot_background_managers.forEach(background_manager => {
background_manager._bms_pipeline.destroy();
if (background_manager.backgroundActor) {
let widget = background_manager.backgroundActor.get_parent();
widget?.get_parent()?.remove_child(widget);
}
background_manager.destroy();
});
Main.screenshotUI._windowSelectors.forEach(window_selector =>
window_selector.get_children().forEach(child => {
if (child.get_name() == 'bms-screenshot-blurred-widget')
window_selector.remove_child(child);
})
);
this.screenshot_background_managers = [];
}
disable() {
this._log("removing blur from screenshot's window selector");
this.remove_background_actors();
this.connections.disconnect_all();
}
_log(str) {
if (this.settings.DEBUG)
console.log(`[Blur my Shell > screenshot] ${str}`);
}
_warn(str) {
console.warn(`[Blur my Shell > screenshot] ${str}`);
}
};

View File

@ -0,0 +1,148 @@
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
import { PaintSignals } from '../conveniences/paint_signals.js';
import { DummyPipeline } from '../conveniences/dummy_pipeline.js';
export const WindowListBlur = class WindowListBlur {
constructor(connections, settings, effects_manager) {
this.connections = connections;
this.settings = settings;
this.paint_signals = new PaintSignals(connections);
this.effects_manager = effects_manager;
this.pipelines = [];
}
enable() {
this._log("blurring window list");
// blur if window-list is found
Main.layoutManager.uiGroup.get_children().forEach(
child => this.try_blur(child)
);
// listen to new actors in `Main.layoutManager.uiGroup` and blur it if
// if is window-list
this.connections.connect(
Main.layoutManager.uiGroup,
'child-added',
(_, child) => this.try_blur(child)
);
// connect to overview
this.connections.connect(Main.overview, 'showing', _ => {
this.hide();
});
this.connections.connect(Main.overview, 'hidden', _ => {
this.show();
});
}
try_blur(actor) {
if (
actor.constructor.name === "WindowList" &&
actor.style !== "background:transparent;"
) {
this._log("found window list to blur");
const pipeline = new DummyPipeline(
this.effects_manager, this.settings.window_list
);
pipeline.attach_effect_to_actor(actor);
this.pipelines.push(pipeline);
actor.set_style("background:transparent;");
actor._windowList.get_children().forEach(
window => this.style_window_button(window)
);
this.connections.connect(
actor._windowList,
'child-added',
(_, window) => this.style_window_button(window)
);
this.connections.connect(
actor,
'destroy',
_ => this.destroy_blur(pipeline, true)
);
// 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 panel
// buttons to cause artifacts on the panel itself
//
// [1]: https://gitlab.gnome.org/GNOME/gnome-shell/-/issues/2857
if (this.settings.HACKS_LEVEL === 1) {
this._log("window list hack level 1");
this.paint_signals.disconnect_all_for_actor(actor);
this.paint_signals.connect(actor, pipeline.effect);
} else {
this.paint_signals.disconnect_all_for_actor(actor);
}
}
}
style_window_button(window) {
window.get_child_at_index(0).set_style(
"box-shadow:none; background-color:rgba(0,0,0,0.2); border-radius:5px;"
);
}
// IMPORTANT: do never call this in a mutable `this.pipelines.forEach`
destroy_blur(pipeline, actor_destroyed = false) {
if (!actor_destroyed) {
this.remove_style(pipeline.actor);
this.paint_signals.disconnect_all_for_actor(pipeline.actor);
}
pipeline.destroy();
let index = this.pipelines.indexOf(pipeline);
if (index >= 0)
this.pipelines.splice(pipeline, 1);
}
remove_style(actor) {
if (
actor.constructor.name === "WindowList" &&
actor.style === "background:transparent;"
) {
actor.style = null;
actor._windowList.get_children().forEach(
child => child.get_child_at_index(0).set_style(null)
);
}
}
hide() {
this.pipelines.forEach(pipeline => pipeline.effect?.set_enabled(false));
}
show() {
this.pipelines.forEach(pipeline => pipeline.effect?.set_enabled(true));
}
disable() {
this._log("removing blur from window list");
const immutable_pipelines_list = [...this.pipelines];
immutable_pipelines_list.forEach(pipeline => this.destroy_blur(pipeline));
this.pipelines = [];
this.connections.disconnect_all();
}
_log(str) {
if (this.settings.DEBUG)
console.log(`[Blur my Shell > window list] ${str}`);
}
};

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);
}

Some files were not shown because too many files have changed in this diff Show More