// -*- 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 = {}; }, 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, onLoaded: null }); if (_fileEqual0(this._animationFile, params.file)) { if (params.onLoaded) { let id = GLib.idle_add(GLib.PRIORITY_DEFAULT, Lang.bind(this, function() { params.onLoaded(this._animation); 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._animationFile = params.file; this._animation = animation; if (params.onLoaded) { let id = GLib.idle_add(GLib.PRIORITY_DEFAULT, Lang.bind(this, function() { params.onLoaded(this._animation); 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._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._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, 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);