gnome-shell/js/ui/background.js
Florian Müllner 5c550daecb backgroundManager: Always emit 'loaded' signal
As backgrounds are cached, it is possible that we never emit the
'loaded' signal added in commit f386103bc1. We are relying on the
signal though, so do the same as Background and emit the signal
from an idle if the background was already loaded.

https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/1371
2020-07-22 11:02:24 +00:00

797 lines
27 KiB
JavaScript

// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
/* exported SystemBackground */
// READ THIS FIRST
// Background handling is a maze of objects, both objects in this file, and
// also objects inside Mutter. They all have a role.
//
// BackgroundManager
// The only object that other parts of GNOME Shell deal with; a
// BackgroundManager creates background actors and adds them to
// the specified container. When the background is changed by the
// user it will fade out the old actor and fade in the new actor.
// (This is separate from the fading for an animated background,
// since using two actors is quite inefficient.)
//
// MetaBackgroundImage
// An object represented an image file that will be used for drawing
// the background. MetaBackgroundImage objects asynchronously load,
// so they are first created in an unloaded state, then later emit
// a ::loaded signal when the Cogl object becomes available.
//
// MetaBackgroundImageCache
// A cache from filename to MetaBackgroundImage.
//
// BackgroundSource
// An object that is created for each GSettings schema (separate
// settings schemas are used for the lock screen and main background),
// and holds a reference to shared Background objects.
//
// MetaBackground
// Holds the specification of a background - a background color
// or gradient and one or two images blended together.
//
// Background
// JS delegate object that Connects a MetaBackground to the GSettings
// schema for the background.
//
// Animation
// A helper object that handles loading a XML-based animation; it is a
// wrapper for GnomeDesktop.BGSlideShow
//
// MetaBackgroundActor
// An actor that draws the background for a single monitor
//
// BackgroundCache
// A cache of Settings schema => BackgroundSource and of a single Animation.
// Also used to share file monitors.
//
// A static image, background color or gradient is relatively straightforward. The
// calling code creates a separate BackgroundManager for each monitor. Since they
// are created for the same GSettings schema, they will use the same BackgroundSource
// object, which provides a single Background and correspondingly a single
// MetaBackground object.
//
// BackgroundManager BackgroundManager
// | \ / |
// | BackgroundSource | looked up in BackgroundCache
// | | |
// | Background |
// | | |
// MetaBackgroundActor | MetaBackgroundActor
// \ | /
// `------- MetaBackground ------'
// |
// MetaBackgroundImage looked up in MetaBackgroundImageCache
//
// The animated case is tricker because the animation XML file can specify different
// files for different monitor resolutions and aspect ratios. For this reason,
// the BackgroundSource provides different Background share a single Animation object,
// which tracks the animation, but use different MetaBackground objects. In the
// common case, the different MetaBackground objects will be created for the
// same filename and look up the *same* MetaBackgroundImage object, so there is
// little wasted memory:
//
// BackgroundManager BackgroundManager
// | \ / |
// | BackgroundSource | looked up in BackgroundCache
// | / \ |
// | Background Background |
// | | \ / | |
// | | Animation | | looked up in BackgroundCache
// MetaBackgroundA|tor Me|aBackgroundActor
// \ | | /
// MetaBackground MetaBackground
// \ /
// MetaBackgroundImage looked up in MetaBackgroundImageCache
// MetaBackgroundImage
//
// But the case of different filenames and different background images
// is possible as well:
// ....
// MetaBackground MetaBackground
// | |
// MetaBackgroundImage MetaBackgroundImage
// MetaBackgroundImage MetaBackgroundImage
const { Clutter, GDesktopEnums, Gio, GLib, GObject, GnomeDesktop, Meta } = imports.gi;
const Signals = imports.signals;
const LoginManager = imports.misc.loginManager;
const Main = imports.ui.main;
const Params = imports.misc.params;
var DEFAULT_BACKGROUND_COLOR = Clutter.Color.from_pixel(0x2e3436ff);
const BACKGROUND_SCHEMA = 'org.gnome.desktop.background';
const PRIMARY_COLOR_KEY = 'primary-color';
const SECONDARY_COLOR_KEY = 'secondary-color';
const COLOR_SHADING_TYPE_KEY = 'color-shading-type';
const BACKGROUND_STYLE_KEY = 'picture-options';
const PICTURE_URI_KEY = 'picture-uri';
var FADE_ANIMATION_TIME = 1000;
// These parameters affect how often we redraw.
// The first is how different (percent crossfaded) the slide show
// has to look before redrawing and the second is the minimum
// frequency (in seconds) we're willing to wake up
var ANIMATION_OPACITY_STEP_INCREMENT = 4.0;
var ANIMATION_MIN_WAKEUP_INTERVAL = 1.0;
let _backgroundCache = null;
function _fileEqual0(file1, file2) {
if (file1 == file2)
return true;
if (!file1 || !file2)
return false;
return file1.equal(file2);
}
var BackgroundCache = class BackgroundCache {
constructor() {
this._fileMonitors = {};
this._backgroundSources = {};
this._animations = {};
}
monitorFile(file) {
let key = file.hash();
if (this._fileMonitors[key])
return;
let monitor = file.monitor(Gio.FileMonitorFlags.NONE, null);
monitor.connect('changed',
(obj, theFile, otherFile, eventType) => {
// Ignore CHANGED and CREATED events, since in both cases
// we'll get a CHANGES_DONE_HINT event when done.
if (eventType != Gio.FileMonitorEvent.CHANGED &&
eventType != Gio.FileMonitorEvent.CREATED)
this.emit('file-changed', file);
});
this._fileMonitors[key] = monitor;
}
getAnimation(params) {
params = Params.parse(params, { file: null,
settingsSchema: null,
onLoaded: null });
let animation = this._animations[params.settingsSchema];
if (animation && _fileEqual0(animation.file, params.file)) {
if (params.onLoaded) {
let id = GLib.idle_add(GLib.PRIORITY_DEFAULT, () => {
params.onLoaded(this._animations[params.settingsSchema]);
return GLib.SOURCE_REMOVE;
});
GLib.Source.set_name_by_id(id, '[gnome-shell] params.onLoaded');
}
return;
}
animation = new Animation({ file: params.file });
animation.load(() => {
this._animations[params.settingsSchema] = animation;
if (params.onLoaded) {
let id = GLib.idle_add(GLib.PRIORITY_DEFAULT, () => {
params.onLoaded(this._animations[params.settingsSchema]);
return GLib.SOURCE_REMOVE;
});
GLib.Source.set_name_by_id(id, '[gnome-shell] params.onLoaded');
}
});
}
getBackgroundSource(layoutManager, settingsSchema) {
// The layoutManager is always the same one; we pass in it since
// Main.layoutManager may not be set yet
if (!(settingsSchema in this._backgroundSources)) {
this._backgroundSources[settingsSchema] = new BackgroundSource(layoutManager, settingsSchema);
this._backgroundSources[settingsSchema]._useCount = 1;
} else {
this._backgroundSources[settingsSchema]._useCount++;
}
return this._backgroundSources[settingsSchema];
}
releaseBackgroundSource(settingsSchema) {
if (settingsSchema in this._backgroundSources) {
let source = this._backgroundSources[settingsSchema];
source._useCount--;
if (source._useCount == 0) {
delete this._backgroundSources[settingsSchema];
source.destroy();
}
}
}
};
Signals.addSignalMethods(BackgroundCache.prototype);
function getBackgroundCache() {
if (!_backgroundCache)
_backgroundCache = new BackgroundCache();
return _backgroundCache;
}
var Background = GObject.registerClass({
Signals: { 'loaded': {}, 'bg-changed': {} },
}, class Background extends Meta.Background {
_init(params) {
params = Params.parse(params, { monitorIndex: 0,
layoutManager: Main.layoutManager,
settings: null,
file: null,
style: null });
super._init({ meta_display: global.display });
this._settings = params.settings;
this._file = params.file;
this._style = params.style;
this._monitorIndex = params.monitorIndex;
this._layoutManager = params.layoutManager;
this._fileWatches = {};
this._cancellable = new Gio.Cancellable();
this.isLoaded = false;
this._clock = new GnomeDesktop.WallClock();
this._timezoneChangedId = this._clock.connect('notify::timezone',
() => {
if (this._animation)
this._loadAnimation(this._animation.file);
});
let loginManager = LoginManager.getLoginManager();
this._prepareForSleepId = loginManager.connect('prepare-for-sleep',
(lm, aboutToSuspend) => {
if (aboutToSuspend)
return;
this._refreshAnimation();
});
this._settingsChangedSignalId =
this._settings.connect('changed', this._emitChangedSignal.bind(this));
this._load();
}
destroy() {
this._cancellable.cancel();
this._removeAnimationTimeout();
let i;
let keys = Object.keys(this._fileWatches);
for (i = 0; i < keys.length; i++)
this._cache.disconnect(this._fileWatches[keys[i]]);
this._fileWatches = null;
if (this._timezoneChangedId != 0)
this._clock.disconnect(this._timezoneChangedId);
this._timezoneChangedId = 0;
this._clock = null;
if (this._prepareForSleepId != 0)
LoginManager.getLoginManager().disconnect(this._prepareForSleepId);
this._prepareForSleepId = 0;
if (this._settingsChangedSignalId != 0)
this._settings.disconnect(this._settingsChangedSignalId);
this._settingsChangedSignalId = 0;
if (this._changedIdleId) {
GLib.source_remove(this._changedIdleId);
this._changedIdleId = 0;
}
}
_emitChangedSignal() {
if (this._changedIdleId)
return;
this._changedIdleId = GLib.idle_add(GLib.PRIORITY_DEFAULT, () => {
this._changedIdleId = 0;
this.emit('bg-changed');
return GLib.SOURCE_REMOVE;
});
GLib.Source.set_name_by_id(this._changedIdleId,
'[gnome-shell] Background._emitChangedSignal');
}
updateResolution() {
if (this._animation)
this._refreshAnimation();
}
_refreshAnimation() {
if (!this._animation)
return;
this._removeAnimationTimeout();
this._updateAnimation();
}
_setLoaded() {
if (this.isLoaded)
return;
this.isLoaded = true;
let id = GLib.idle_add(GLib.PRIORITY_DEFAULT, () => {
this.emit('loaded');
return GLib.SOURCE_REMOVE;
});
GLib.Source.set_name_by_id(id, '[gnome-shell] Background._setLoaded Idle');
}
_loadPattern() {
let colorString, res_, color, secondColor;
colorString = this._settings.get_string(PRIMARY_COLOR_KEY);
[res_, color] = Clutter.Color.from_string(colorString);
colorString = this._settings.get_string(SECONDARY_COLOR_KEY);
[res_, secondColor] = Clutter.Color.from_string(colorString);
let shadingType = this._settings.get_enum(COLOR_SHADING_TYPE_KEY);
if (shadingType == GDesktopEnums.BackgroundShading.SOLID)
this.set_color(color);
else
this.set_gradient(shadingType, color, secondColor);
}
_watchFile(file) {
let key = file.hash();
if (this._fileWatches[key])
return;
this._cache.monitorFile(file);
let signalId = this._cache.connect('file-changed',
(cache, changedFile) => {
if (changedFile.equal(file)) {
let imageCache = Meta.BackgroundImageCache.get_default();
imageCache.purge(changedFile);
this._emitChangedSignal();
}
});
this._fileWatches[key] = signalId;
}
_removeAnimationTimeout() {
if (this._updateAnimationTimeoutId) {
GLib.source_remove(this._updateAnimationTimeoutId);
this._updateAnimationTimeoutId = 0;
}
}
_updateAnimation() {
this._updateAnimationTimeoutId = 0;
this._animation.update(this._layoutManager.monitors[this._monitorIndex]);
let files = this._animation.keyFrameFiles;
let finish = () => {
this._setLoaded();
if (files.length > 1) {
this.set_blend(files[0], files[1],
this._animation.transitionProgress,
this._style);
} else if (files.length > 0) {
this.set_file(files[0], this._style);
} else {
this.set_file(null, this._style);
}
this._queueUpdateAnimation();
};
let cache = Meta.BackgroundImageCache.get_default();
let numPendingImages = files.length;
for (let i = 0; i < files.length; i++) {
this._watchFile(files[i]);
let image = cache.load(files[i]);
if (image.is_loaded()) {
numPendingImages--;
if (numPendingImages == 0)
finish();
} else {
// eslint-disable-next-line no-loop-func
let id = image.connect('loaded', () => {
image.disconnect(id);
numPendingImages--;
if (numPendingImages == 0)
finish();
});
}
}
}
_queueUpdateAnimation() {
if (this._updateAnimationTimeoutId != 0)
return;
if (!this._cancellable || this._cancellable.is_cancelled())
return;
if (!this._animation.transitionDuration)
return;
let nSteps = 255 / ANIMATION_OPACITY_STEP_INCREMENT;
let timePerStep = (this._animation.transitionDuration * 1000) / nSteps;
let interval = Math.max(ANIMATION_MIN_WAKEUP_INTERVAL * 1000,
timePerStep);
if (interval > GLib.MAXUINT32)
return;
this._updateAnimationTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT,
interval,
() => {
this._updateAnimationTimeoutId = 0;
this._updateAnimation();
return GLib.SOURCE_REMOVE;
});
GLib.Source.set_name_by_id(this._updateAnimationTimeoutId, '[gnome-shell] this._updateAnimation');
}
_loadAnimation(file) {
this._cache.getAnimation({
file,
settingsSchema: this._settings.schema_id,
onLoaded: animation => {
this._animation = animation;
if (!this._animation || this._cancellable.is_cancelled()) {
this._setLoaded();
return;
}
this._updateAnimation();
this._watchFile(file);
},
});
}
_loadImage(file) {
this.set_file(file, this._style);
this._watchFile(file);
let cache = Meta.BackgroundImageCache.get_default();
let image = cache.load(file);
if (image.is_loaded()) {
this._setLoaded();
} else {
let id = image.connect('loaded', () => {
this._setLoaded();
image.disconnect(id);
});
}
}
_loadFile(file) {
if (file.get_basename().endsWith('.xml'))
this._loadAnimation(file);
else
this._loadImage(file);
}
_load() {
this._cache = getBackgroundCache();
this._loadPattern();
if (!this._file) {
this._setLoaded();
return;
}
this._loadFile(this._file);
}
});
let _systemBackground;
var SystemBackground = GObject.registerClass({
Signals: { 'loaded': {} },
}, class SystemBackground extends Meta.BackgroundActor {
_init() {
if (_systemBackground == null) {
_systemBackground = new Meta.Background({ meta_display: global.display });
_systemBackground.set_color(DEFAULT_BACKGROUND_COLOR);
}
super._init({
meta_display: global.display,
monitor: 0,
});
this.content.background = _systemBackground;
let id = GLib.idle_add(GLib.PRIORITY_DEFAULT, () => {
this.emit('loaded');
return GLib.SOURCE_REMOVE;
});
GLib.Source.set_name_by_id(id, '[gnome-shell] SystemBackground.loaded');
}
});
var BackgroundSource = class BackgroundSource {
constructor(layoutManager, settingsSchema) {
// Allow override the background image setting for performance testing
this._layoutManager = layoutManager;
this._overrideImage = GLib.getenv('SHELL_BACKGROUND_IMAGE');
this._settings = new Gio.Settings({ schema_id: settingsSchema });
this._backgrounds = [];
let monitorManager = Meta.MonitorManager.get();
this._monitorsChangedId =
monitorManager.connect('monitors-changed',
this._onMonitorsChanged.bind(this));
}
_onMonitorsChanged() {
for (let monitorIndex in this._backgrounds) {
let background = this._backgrounds[monitorIndex];
if (monitorIndex < this._layoutManager.monitors.length) {
background.updateResolution();
} else {
background.disconnect(background._changedId);
background.destroy();
delete this._backgrounds[monitorIndex];
}
}
}
getBackground(monitorIndex) {
let file = null;
let style;
// We don't watch changes to settings here,
// instead we rely on Background to watch those
// and emit 'bg-changed' at the right time
if (this._overrideImage != null) {
file = Gio.File.new_for_path(this._overrideImage);
style = GDesktopEnums.BackgroundStyle.ZOOM; // Hardcode
} else {
style = this._settings.get_enum(BACKGROUND_STYLE_KEY);
if (style != GDesktopEnums.BackgroundStyle.NONE) {
let uri = this._settings.get_string(PICTURE_URI_KEY);
file = Gio.File.new_for_commandline_arg(uri);
}
}
// Animated backgrounds are (potentially) per-monitor, since
// they can have variants that depend on the aspect ratio and
// size of the monitor; for other backgrounds we can use the
// same background object for all monitors.
if (file == null || !file.get_basename().endsWith('.xml'))
monitorIndex = 0;
if (!(monitorIndex in this._backgrounds)) {
let background = new Background({
monitorIndex,
layoutManager: this._layoutManager,
settings: this._settings,
file,
style,
});
background._changedId = background.connect('bg-changed', () => {
background.disconnect(background._changedId);
background.destroy();
delete this._backgrounds[monitorIndex];
});
this._backgrounds[monitorIndex] = background;
}
return this._backgrounds[monitorIndex];
}
destroy() {
let monitorManager = Meta.MonitorManager.get();
monitorManager.disconnect(this._monitorsChangedId);
for (let monitorIndex in this._backgrounds) {
let background = this._backgrounds[monitorIndex];
background.disconnect(background._changedId);
background.destroy();
}
this._backgrounds = null;
}
};
var Animation = GObject.registerClass(
class Animation extends GnomeDesktop.BGSlideShow {
_init(params) {
super._init(params);
this.keyFrameFiles = [];
this.transitionProgress = 0.0;
this.transitionDuration = 0.0;
this.loaded = false;
}
load(callback) {
this.load_async(null, () => {
this.loaded = true;
if (callback)
callback();
});
}
update(monitor) {
this.keyFrameFiles = [];
if (this.get_num_slides() < 1)
return;
let [progress, duration, isFixed_, filename1, filename2] =
this.get_current_slide(monitor.width, monitor.height);
this.transitionDuration = duration;
this.transitionProgress = progress;
if (filename1)
this.keyFrameFiles.push(Gio.File.new_for_path(filename1));
if (filename2)
this.keyFrameFiles.push(Gio.File.new_for_path(filename2));
}
});
var BackgroundManager = class BackgroundManager {
constructor(params) {
params = Params.parse(params, { container: null,
layoutManager: Main.layoutManager,
monitorIndex: null,
vignette: false,
controlPosition: true,
settingsSchema: BACKGROUND_SCHEMA });
let cache = getBackgroundCache();
this._settingsSchema = params.settingsSchema;
this._backgroundSource = cache.getBackgroundSource(params.layoutManager, params.settingsSchema);
this._container = params.container;
this._layoutManager = params.layoutManager;
this._vignette = params.vignette;
this._monitorIndex = params.monitorIndex;
this._controlPosition = params.controlPosition;
this.backgroundActor = this._createBackgroundActor();
this._newBackgroundActor = null;
}
destroy() {
let cache = getBackgroundCache();
cache.releaseBackgroundSource(this._settingsSchema);
this._backgroundSource = null;
if (this._newBackgroundActor) {
this._newBackgroundActor.destroy();
this._newBackgroundActor = null;
}
if (this.backgroundActor) {
this.backgroundActor.destroy();
this.backgroundActor = null;
}
}
_swapBackgroundActor() {
let oldBackgroundActor = this.backgroundActor;
this.backgroundActor = this._newBackgroundActor;
this._newBackgroundActor = null;
this.emit('changed');
oldBackgroundActor.ease({
opacity: 0,
duration: FADE_ANIMATION_TIME,
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
onComplete: () => oldBackgroundActor.destroy(),
});
}
_updateBackgroundActor() {
if (this._newBackgroundActor) {
/* Skip displaying existing background queued for load */
this._newBackgroundActor.destroy();
this._newBackgroundActor = null;
}
let newBackgroundActor = this._createBackgroundActor();
const oldContent = this.backgroundActor.content;
const newContent = newBackgroundActor.content;
newContent.vignette_sharpness = oldContent.vignette_sharpness;
newContent.brightness = oldContent.brightness;
newBackgroundActor.visible = this.backgroundActor.visible;
this._newBackgroundActor = newBackgroundActor;
const { background } = newBackgroundActor.content;
if (background.isLoaded) {
this._swapBackgroundActor();
} else {
newBackgroundActor.loadedSignalId = background.connect('loaded',
() => {
background.disconnect(newBackgroundActor.loadedSignalId);
newBackgroundActor.loadedSignalId = 0;
this._swapBackgroundActor();
});
}
}
_createBackgroundActor() {
let background = this._backgroundSource.getBackground(this._monitorIndex);
let backgroundActor = new Meta.BackgroundActor({
meta_display: global.display,
monitor: this._monitorIndex,
});
backgroundActor.content.set({
background,
vignette: this._vignette,
vignette_sharpness: 0.5,
brightness: 0.5,
});
this._container.add_child(backgroundActor);
if (this._controlPosition) {
let monitor = this._layoutManager.monitors[this._monitorIndex];
backgroundActor.set_position(monitor.x, monitor.y);
this._container.set_child_below_sibling(backgroundActor, null);
}
let changeSignalId = background.connect('bg-changed', () => {
background.disconnect(changeSignalId);
changeSignalId = null;
this._updateBackgroundActor();
});
let loadedSignalId;
if (background.isLoaded) {
GLib.idle_add(GLib.PRIORITY_DEFAULT, () => {
this.emit('loaded');
return GLib.SOURCE_REMOVE;
});
} else {
loadedSignalId = background.connect('loaded', () => {
background.disconnect(loadedSignalId);
loadedSignalId = null;
this.emit('loaded');
});
}
backgroundActor.connect('destroy', () => {
if (changeSignalId)
background.disconnect(changeSignalId);
if (loadedSignalId)
background.disconnect(loadedSignalId);
if (backgroundActor.loadedSignalId)
background.disconnect(backgroundActor.loadedSignalId);
});
return backgroundActor;
}
};
Signals.addSignalMethods(BackgroundManager.prototype);