gnome-shell/js/ui/background.js
Florian Müllner 965aedb0bb background: Reload animation on timezone changes
Animated backgrounds are based on a start time in local time - in case
of a timezone change, that time is no longer accurate. To fix, we need
to either make BGSlideShow aware of timezone changes (and notify us to
update the animation), or just reload the animation - timezone changes
should be a rare event, so go with the simpler second option.

https://bugzilla.gnome.org/show_bug.cgi?id=758939
2015-12-02 17:18:39 +01:00

772 lines
28 KiB
JavaScript

// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
// 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 = imports.gi.Clutter;
const GDesktopEnums = imports.gi.GDesktopEnums;
const Gio = imports.gi.Gio;
const GLib = imports.gi.GLib;
const GnomeDesktop = imports.gi.GnomeDesktop;
const Lang = imports.lang;
const Meta = imports.gi.Meta;
const Signals = imports.signals;
const Main = imports.ui.main;
const Params = imports.misc.params;
const Tweener = imports.ui.tweener;
const 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_OPACITY_KEY = 'picture-opacity';
const PICTURE_URI_KEY = 'picture-uri';
const FADE_ANIMATION_TIME = 1.0;
// 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
const ANIMATION_OPACITY_STEP_INCREMENT = 4.0;
const 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);
}
const BackgroundCache = new Lang.Class({
Name: 'BackgroundCache',
_init: function() {
this._pendingFileLoads = [];
this._fileMonitors = {};
this._backgroundSources = {};
this._animations = {};
},
monitorFile: function(file) {
let key = file.hash();
if (this._fileMonitors[key])
return;
let monitor = file.monitor(Gio.FileMonitorFlags.NONE, null);
monitor.connect('changed',
Lang.bind(this, function() {
this.emit('file-changed', file);
}));
this._fileMonitors[key] = monitor;
},
getAnimation: function(params) {
params = Params.parse(params, { file: null,
settingsSchema: null,
onLoaded: null });
if (this._animations[params.settingsSchema] && _fileEqual0(this._animationFile, params.file)) {
if (params.onLoaded) {
let id = GLib.idle_add(GLib.PRIORITY_DEFAULT, Lang.bind(this, function() {
params.onLoaded(this._animations[params.settingsSchema]);
return GLib.SOURCE_REMOVE;
}));
GLib.Source.set_name_by_id(id, '[gnome-shell] params.onLoaded');
}
return;
}
let animation = new Animation({ file: params.file });
animation.load(Lang.bind(this, function() {
this._animations[params.settingsSchema] = animation;
if (params.onLoaded) {
let id = GLib.idle_add(GLib.PRIORITY_DEFAULT, Lang.bind(this, function() {
params.onLoaded(this._animations[params.settingsSchema]);
return GLib.SOURCE_REMOVE;
}));
GLib.Source.set_name_by_id(id, '[gnome-shell] params.onLoaded');
}
}));
},
getBackgroundSource: function(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: function(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;
}
const Background = new Lang.Class({
Name: 'Background',
_init: function(params) {
params = Params.parse(params, { monitorIndex: 0,
layoutManager: Main.layoutManager,
settings: null,
file: null,
style: null });
this.background = new Meta.Background({ meta_screen: global.screen });
this.background._delegate = this;
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',
Lang.bind(this, function() {
if (this._animation)
this._loadAnimation(this._animation.file);
}));
this._settingsChangedSignalId = this._settings.connect('changed', Lang.bind(this, function() {
this.emit('changed');
}));
this._load();
},
destroy: function() {
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;
if (this._settingsChangedSignalId != 0)
this._settings.disconnect(this._settingsChangedSignalId);
this._settingsChangedSignalId = 0;
},
updateResolution: function() {
if (this._animation) {
this._removeAnimationTimeout();
this._updateAnimation();
}
},
_setLoaded: function() {
if (this.isLoaded)
return;
this.isLoaded = true;
let id = GLib.idle_add(GLib.PRIORITY_DEFAULT, Lang.bind(this, function() {
this.emit('loaded');
return GLib.SOURCE_REMOVE;
}));
GLib.Source.set_name_by_id(id, '[gnome-shell] this.emit');
},
_loadPattern: function() {
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.background.set_color(color);
else
this.background.set_gradient(shadingType, color, secondColor);
},
_watchFile: function(file) {
let key = file.hash();
if (this._fileWatches[key])
return;
this._cache.monitorFile(file);
let signalId = this._cache.connect('file-changed',
Lang.bind(this, function(cache, changedFile) {
if (changedFile.equal(file)) {
let imageCache = Meta.BackgroundImageCache.get_default();
imageCache.purge(changedFile);
this.emit('changed');
}
}));
this._fileWatches[key] = signalId;
},
_removeAnimationTimeout: function() {
if (this._updateAnimationTimeoutId) {
GLib.source_remove(this._updateAnimationTimeoutId);
this._updateAnimationTimeoutId = 0;
}
},
_updateAnimation: function() {
this._updateAnimationTimeoutId = 0;
this._animation.update(this._layoutManager.monitors[this._monitorIndex]);
let files = this._animation.keyFrameFiles;
let finish = Lang.bind(this, function() {
this._setLoaded();
if (files.length > 1) {
this.background.set_blend(files[0], files[1],
this._animation.transitionProgress,
this._style);
} else if (files.length > 0) {
this.background.set_file(files[0], this._style);
} else {
this.background.set_file(null, this._style);
}
this._queueUpdateAnimation();
});
let cache = Meta.BackgroundImageCache.get_default();
let numPendingImages = files.length;
let images = [];
for (let i = 0; i < files.length; i++) {
this._watchFile(files[i]);
let image = cache.load(files[i]);
images.push(image);
if (image.is_loaded()) {
numPendingImages--;
if (numPendingImages == 0)
finish();
} else {
let id = image.connect('loaded',
Lang.bind(this, function() {
image.disconnect(id);
numPendingImages--;
if (numPendingImages == 0)
finish();
}));
}
}
},
_queueUpdateAnimation: function() {
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,
Lang.bind(this, function() {
this._updateAnimationTimeoutId = 0;
this._updateAnimation();
return GLib.SOURCE_REMOVE;
}));
GLib.Source.set_name_by_id(this._updateAnimationTimeoutId, '[gnome-shell] this._updateAnimation');
},
_loadAnimation: function(file) {
this._cache.getAnimation({ file: file,
settingsSchema: this._settings.schema_id,
onLoaded: Lang.bind(this, function(animation) {
this._animation = animation;
if (!this._animation || this._cancellable.is_cancelled()) {
this._setLoaded();
return;
}
this._updateAnimation();
this._watchFile(file);
})
});
},
_loadImage: function(file) {
this.background.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',
Lang.bind(this, function() {
this._setLoaded();
image.disconnect(id);
}));
}
},
_loadFile: function(file) {
if (file.get_basename().endsWith('.xml'))
this._loadAnimation(file);
else
this._loadImage(file);
},
_load: function () {
this._cache = getBackgroundCache();
this._loadPattern();
if (!this._file) {
this._setLoaded();
return;
}
this._loadFile(this._file);
},
});
Signals.addSignalMethods(Background.prototype);
let _systemBackground;
const SystemBackground = new Lang.Class({
Name: 'SystemBackground',
_init: function() {
let file = Gio.File.new_for_uri('resource:///org/gnome/shell/theme/noise-texture.png');
if (_systemBackground == null) {
_systemBackground = new Meta.Background({ meta_screen: global.screen });
_systemBackground.set_color(DEFAULT_BACKGROUND_COLOR);
_systemBackground.set_file(file, GDesktopEnums.BackgroundStyle.WALLPAPER);
}
this.actor = new Meta.BackgroundActor({ meta_screen: global.screen,
monitor: 0,
background: _systemBackground });
let cache = Meta.BackgroundImageCache.get_default();
let image = cache.load(file);
if (image.is_loaded()) {
image = null;
let id = GLib.idle_add(GLib.PRIORITY_DEFAULT, Lang.bind(this, function() {
this.emit('loaded');
return GLib.SOURCE_REMOVE;
}));
GLib.Source.set_name_by_id(id, '[gnome-shell] SystemBackground.loaded');
} else {
let id = image.connect('loaded',
Lang.bind(this, function() {
this.emit('loaded');
image.disconnect(id);
image = null;
}));
}
},
});
Signals.addSignalMethods(SystemBackground.prototype);
const BackgroundSource = new Lang.Class({
Name: 'BackgroundSource',
_init: function(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 = [];
this._monitorsChangedId = global.screen.connect('monitors-changed',
Lang.bind(this, this._onMonitorsChanged));
},
_onMonitorsChanged: function() {
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: function(monitorIndex) {
let file = null;
let style;
// We don't watch changes to settings here,
// instead we rely on Background to watch those
// and emit '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: monitorIndex,
layoutManager: this._layoutManager,
settings: this._settings,
file: file,
style: style
});
background._changedId = background.connect('changed', Lang.bind(this, function() {
background.disconnect(background._changedId);
background.destroy();
delete this._backgrounds[monitorIndex];
}));
this._backgrounds[monitorIndex] = background;
}
return this._backgrounds[monitorIndex];
},
destroy: function() {
global.screen.disconnect(this._monitorsChangedId);
for (let monitorIndex in this._backgrounds) {
let background = this._backgrounds[monitorIndex];
background.disconnect(background._changedId);
background.destroy();
}
this._backgrounds = null;
}
});
const Animation = new Lang.Class({
Name: 'Animation',
_init: function(params) {
params = Params.parse(params, { file: null });
this.file = params.file;
this.keyFrameFiles = [];
this.transitionProgress = 0.0;
this.transitionDuration = 0.0;
this.loaded = false;
},
load: function(callback) {
this._show = new GnomeDesktop.BGSlideShow({ filename: this.file.get_path() });
this._show.load_async(null,
Lang.bind(this,
function(object, result) {
this.loaded = true;
if (callback)
callback();
}));
},
update: function(monitor) {
this.keyFrameFiles = [];
if (!this._show)
return;
if (this._show.get_num_slides() < 1)
return;
let [progress, duration, isFixed, filename1, filename2] = this._show.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));
},
});
Signals.addSignalMethods(Animation.prototype);
const BackgroundManager = new Lang.Class({
Name: 'BackgroundManager',
_init: function(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: function() {
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: function() {
let oldBackgroundActor = this.backgroundActor;
this.backgroundActor = this._newBackgroundActor;
this._newBackgroundActor = null;
this.emit('changed');
Tweener.addTween(oldBackgroundActor,
{ opacity: 0,
time: FADE_ANIMATION_TIME,
transition: 'easeOutQuad',
onComplete: function() {
oldBackgroundActor.destroy();
}
});
},
_updateBackgroundActor: function() {
if (this._newBackgroundActor) {
/* Skip displaying existing background queued for load */
this._newBackgroundActor.destroy();
this._newBackgroundActor = null;
}
let newBackgroundActor = this._createBackgroundActor();
newBackgroundActor.vignette_sharpness = this.backgroundActor.vignette_sharpness;
newBackgroundActor.brightness = this.backgroundActor.brightness;
newBackgroundActor.visible = this.backgroundActor.visible;
this._newBackgroundActor = newBackgroundActor;
let background = newBackgroundActor.background._delegate;
if (background.isLoaded) {
this._swapBackgroundActor();
} else {
newBackgroundActor.loadedSignalId = background.connect('loaded',
Lang.bind(this, function() {
background.disconnect(newBackgroundActor.loadedSignalId);
newBackgroundActor.loadedSignalId = 0;
this._swapBackgroundActor();
}));
}
},
_createBackgroundActor: function() {
let background = this._backgroundSource.getBackground(this._monitorIndex);
let backgroundActor = new Meta.BackgroundActor({ meta_screen: global.screen,
monitor: this._monitorIndex,
background: background.background,
vignette: this._vignette,
vignette_sharpness: 0.5,
brightness: 0.5,
});
this._container.add_child(backgroundActor);
let monitor = this._layoutManager.monitors[this._monitorIndex];
backgroundActor.set_size(monitor.width, monitor.height);
if (this._controlPosition) {
backgroundActor.set_position(monitor.x, monitor.y);
backgroundActor.lower_bottom();
}
let changeSignalId = background.connect('changed', Lang.bind(this, function() {
background.disconnect(changeSignalId);
changeSignalId = null;
this._updateBackgroundActor();
}));
backgroundActor.connect('destroy', Lang.bind(this, function() {
if (changeSignalId)
background.disconnect(changeSignalId);
if (backgroundActor.loadedSignalId)
background.disconnect(backgroundActor.loadedSignalId);
}));
return backgroundActor;
},
});
Signals.addSignalMethods(BackgroundManager.prototype);