// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- 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 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; const BackgroundCache = new Lang.Class({ Name: 'BackgroundCache', _init: function() { this._patterns = []; this._images = []; this._pendingFileLoads = []; this._fileMonitors = {}; }, getPatternContent: function(params) { params = Params.parse(params, { monitorIndex: 0, color: null, secondColor: null, shadingType: null, effects: Meta.BackgroundEffects.NONE }); let content = null; let candidateContent = null; for (let i = 0; i < this._patterns.length; i++) { if (this._patterns[i].get_shading() != params.shadingType) continue; if (!params.color.equal(this._patterns[i].get_color())) continue; if (params.shadingType != GDesktopEnums.BackgroundShading.SOLID && !params.secondColor.equal(this._patterns[i].get_second_color())) continue; candidateContent = this._patterns[i]; if (params.effects != this._patterns[i].effects) continue; break; } if (candidateContent) { content = candidateContent.copy(params.monitorIndex, params.effects); } else { content = new Meta.Background({ meta_screen: global.screen, monitor: params.monitorIndex, effects: params.effects }); if (params.shadingType == GDesktopEnums.BackgroundShading.SOLID) { content.load_color(params.color); } else { content.load_gradient(params.shadingType, params.color, params.secondColor); } } this._patterns.push(content); return content; }, _monitorFile: function(filename) { if (this._fileMonitors[filename]) return; let file = Gio.File.new_for_path(filename); let monitor = file.monitor(Gio.FileMonitorFlags.NONE, null); let signalId = monitor.connect('changed', Lang.bind(this, function() { for (let i = 0; i < this._images.length; i++) { if (this._images[i].get_filename() == filename) this._images.splice(i, 1); } monitor.disconnect(signalId); this.emit('file-changed', filename); })); this._fileMonitors[filename] = monitor; }, _removeContent: function(contentList, content) { let index = contentList.indexOf(content); if (index < 0) throw new Error("Trying to remove invalid content: " + content); contentList.splice(index, 1); }, removePatternContent: function(content) { this._removeContent(this._patterns, content); }, removeImageContent: function(content) { let filename = content.get_filename(); let hasOtherUsers = this._images.some(function(content) { return filename == content.get_filename(); }); if (!hasOtherUsers) delete this._fileMonitors[filename]; this._removeContent(this._images, content); }, _attachCallerToFileLoad: function(caller, fileLoad) { fileLoad.callers.push(caller); if (!caller.cancellable) return; caller.cancellable.connect(Lang.bind(this, function() { let idx = fileLoad.callers.indexOf(caller); fileLoad.callers.splice(idx, 1); if (fileLoad.callers.length == 0) { fileLoad.cancellable.cancel(); let idx = this._pendingFileLoads.indexOf(fileLoad); this._pendingFileLoads.splice(idx, 1); } })); }, _loadImageContent: function(params) { params = Params.parse(params, { monitorIndex: 0, style: null, filename: null, effects: Meta.BackgroundEffects.NONE, cancellable: null, onFinished: null }); let caller = { monitorIndex: params.monitorIndex, effects: params.effects, cancellable: params.cancellable, onFinished: params.onFinished }; for (let i = 0; i < this._pendingFileLoads.length; i++) { let fileLoad = this._pendingFileLoads[i]; if (fileLoad.filename == params.filename && fileLoad.style == params.style) { this._attachCallerToFileLoad(caller, fileLoad); return; } } let fileLoad = { filename: params.filename, style: params.style, cancellable: new Gio.Cancellable(), callers: [] }; this._attachCallerToFileLoad(caller, fileLoad); let content = new Meta.Background({ meta_screen: global.screen }); content.load_file_async(params.filename, params.style, params.cancellable, Lang.bind(this, function(object, result) { try { content.load_file_finish(result); this._monitorFile(params.filename); } catch(e) { content = null; } for (let i = 0; i < fileLoad.callers.length; i++) { let caller = fileLoad.callers[i]; if (caller.onFinished) { let newContent; if (content) { newContent = content.copy(caller.monitorIndex, caller.effects); this._images.push(newContent); } caller.onFinished(newContent); } } let idx = this._pendingFileLoads.indexOf(fileLoad); this._pendingFileLoads.splice(idx, 1); })); }, getImageContent: function(params) { params = Params.parse(params, { monitorIndex: 0, style: null, filename: null, effects: Meta.BackgroundEffects.NONE, cancellable: null, onFinished: null }); let content = null; let candidateContent = null; for (let i = 0; i < this._images.length; i++) { if (this._images[i].get_style() != params.style) continue; if (this._images[i].get_filename() != params.filename) continue; if (params.style == GDesktopEnums.BackgroundStyle.SPANNED && this._images[i].monitor != params.monitorIndex) continue; candidateContent = this._images[i]; if (params.effects != this._images[i].effects) continue; break; } if (candidateContent) { content = candidateContent.copy(params.monitorIndex, params.effects); if (params.cancellable && params.cancellable.is_cancelled()) content = null; else this._images.push(content); if (params.onFinished) params.onFinished(content); } else { this._loadImageContent({ filename: params.filename, style: params.style, effects: params.effects, monitorIndex: params.monitorIndex, cancellable: params.cancellable, onFinished: params.onFinished }); } }, getAnimation: function(params) { params = Params.parse(params, { filename: null, onLoaded: null }); if (this._animationFilename == params.filename) { 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'); } } let animation = new Animation({ filename: params.filename }); animation.load(Lang.bind(this, function() { this._monitorFile(params.filename); this._animationFilename = params.filename; 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'); } })); } }); 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, effects: Meta.BackgroundEffects.NONE, settings: null, overrideImage: null }); this.actor = new Meta.BackgroundGroup(); this.actor._delegate = this; this._destroySignalId = this.actor.connect('destroy', Lang.bind(this, this._destroy)); this._settings = params.settings; this._overrideImage = params.overrideImage; this._monitorIndex = params.monitorIndex; this._layoutManager = params.layoutManager; this._effects = params.effects; this._fileWatches = {}; this._pattern = null; // contains a single image for static backgrounds and // two images (from and to) for slide shows this._images = {}; this._brightness = 1.0; this._vignetteSharpness = 0.2; 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(); if (this._updateAnimationTimeoutId) { GLib.source_remove (this._updateAnimationTimeoutId); this._updateAnimationTimeoutId = 0; } 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._pattern) { if (this._pattern.content) this._cache.removePatternContent(this._pattern.content); this._pattern.destroy(); this._pattern = null; } keys = Object.keys(this._images); for (i = 0; i < keys.length; i++) { let actor = this._images[keys[i]]; if (actor.content) this._cache.removeImageContent(actor.content); actor.destroy(); this._images[keys[i]] = null; } this.actor.disconnect(this._destroySignalId); this._destroySignalId = 0; if (this._settingsChangedSignalId != 0) this._settings.disconnect(this._settingsChangedSignalId); this._settingsChangedSignalId = 0; }, _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); let content = this._cache.getPatternContent({ monitorIndex: this._monitorIndex, effects: this._effects, color: color, secondColor: secondColor, shadingType: shadingType }); this._pattern = new Meta.BackgroundActor(); this.actor.add_child(this._pattern); this._pattern.content = content; }, _watchCacheFile: function(filename) { if (this._fileWatches[filename]) return; let signalId = this._cache.connect('file-changed', Lang.bind(this, function(cache, changedFile) { if (changedFile == filename) { this.emit('changed'); } })); this._fileWatches[filename] = signalId; }, _ensureImage: function(index) { if (this._images[index]) return; let actor = new Meta.BackgroundActor(); // The background pattern is the first actor in // the group, and all images should be above that. this.actor.insert_child_at_index(actor, index + 1); this._images[index] = actor; }, _updateImage: function(index, content, filename) { content.brightness = this._brightness; content.vignette_sharpness = this._vignetteSharpness; let image = this._images[index]; if (image.content) this._cache.removeImageContent(image.content); image.content = content; this._watchCacheFile(filename); }, _updateAnimationProgress: function() { if (this._images[1]) this._images[1].opacity = this._animation.transitionProgress * 255; this._queueUpdateAnimation(); }, _updateAnimation: function() { this._updateAnimationTimeoutId = 0; this._animation.update(this._layoutManager.monitors[this._monitorIndex]); let files = this._animation.keyFrameFiles; if (files.length == 0) { this._setLoaded(); this._queueUpdateAnimation(); return; } let numPendingImages = files.length; for (let i = 0; i < files.length; i++) { if (this._images[i] && this._images[i].content && this._images[i].content.get_filename() == files[i]) { numPendingImages--; if (numPendingImages == 0) this._updateAnimationProgress(); continue; } this._cache.getImageContent({ monitorIndex: this._monitorIndex, effects: this._effects, style: this._style, filename: files[i], cancellable: this._cancellable, onFinished: Lang.bind(this, function(content, i) { numPendingImages--; if (!content) { this._setLoaded(); if (numPendingImages == 0) this._updateAnimationProgress(); return; } this._ensureImage(i); this._updateImage(i, content, files[i]); if (numPendingImages == 0) { this._setLoaded(); this._updateAnimationProgress(); } }, i) }); } }, _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(filename) { this._cache.getAnimation({ filename: filename, onLoaded: Lang.bind(this, function(animation) { this._animation = animation; if (!this._animation || this._cancellable.is_cancelled()) { this._setLoaded(); return; } this._updateAnimation(); this._watchCacheFile(filename); }) }); }, _loadImage: function(filename) { this._cache.getImageContent({ monitorIndex: this._monitorIndex, effects: this._effects, style: this._style, filename: filename, cancellable: this._cancellable, onFinished: Lang.bind(this, function(content) { if (content) { this._ensureImage(0); this._updateImage(0, content, filename); } this._setLoaded(); }) }); }, _loadFile: function(filename) { if (filename.endsWith('.xml')) this._loadAnimation(filename); else this._loadImage(filename); }, _load: function () { this._cache = getBackgroundCache(); this._loadPattern(); let filename; if (this._overrideImage != null) { filename = this._overrideImage; this._style = GDesktopEnums.BackgroundStyle.ZOOM; // Hardcode } else { this._style = this._settings.get_enum(BACKGROUND_STYLE_KEY); if (this._style == GDesktopEnums.BackgroundStyle.NONE) { this._setLoaded(); return; } let uri = this._settings.get_string(PICTURE_URI_KEY); if (GLib.uri_parse_scheme(uri) != null) filename = Gio.File.new_for_uri(uri).get_path(); else filename = uri; } if (!filename) { this._setLoaded(); return; } this._loadFile(filename); }, get brightness() { return this._brightness; }, set brightness(factor) { this._brightness = factor; if (this._pattern && this._pattern.content) this._pattern.content.brightness = factor; let keys = Object.keys(this._images); for (let i = 0; i < keys.length; i++) { let image = this._images[keys[i]]; if (image && image.content) image.content.brightness = factor; } }, get vignetteSharpness() { return this._vignetteSharpness; }, set vignetteSharpness(sharpness) { this._vignetteSharpness = sharpness; if (this._pattern && this._pattern.content) this._pattern.content.vignette_sharpness = sharpness; let keys = Object.keys(this._images); for (let i = 0; i < keys.length; i++) { let image = this._images[keys[i]]; if (image && image.content) image.content.vignette_sharpness = sharpness; } } }); Signals.addSignalMethods(Background.prototype); const SystemBackground = new Lang.Class({ Name: 'SystemBackground', _init: function() { this._cache = getBackgroundCache(); this.actor = new Meta.BackgroundActor(); this._cache.getImageContent({ style: GDesktopEnums.BackgroundStyle.WALLPAPER, filename: global.datadir + '/theme/noise-texture.png', effects: Meta.BackgroundEffects.NONE, onFinished: Lang.bind(this, function(content) { this.actor.content = content; this.emit('loaded'); }) }); this.actor.connect('destroy', Lang.bind(this, this._onDestroy)); }, _onDestroy: function() { let content = this.actor.content; if (content) this._cache.removeImageContent(content); }, }); Signals.addSignalMethods(SystemBackground.prototype); const Animation = new Lang.Class({ Name: 'Animation', _init: function(params) { params = Params.parse(params, { filename: null }); this.filename = params.filename; this.keyFrameFiles = []; this.transitionProgress = 0.0; this.transitionDuration = 0.0; this.loaded = false; }, load: function(callback) { let file = Gio.File.new_for_path(this.filename); this._show = new GnomeDesktop.BGSlideShow({ filename: this.filename }); 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, file1, file2] = this._show.get_current_slide(monitor.width, monitor.height); this.transitionDuration = duration; this.transitionProgress = progress; if (file1) this.keyFrameFiles.push(file1); if (file2) this.keyFrameFiles.push(file2); }, }); 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, effects: Meta.BackgroundEffects.NONE, controlPosition: true, settingsSchema: BACKGROUND_SCHEMA }); // Allow override the background image setting for performance testing this._overrideImage = GLib.getenv('SHELL_BACKGROUND_IMAGE'); this._settings = new Gio.Settings({ schema_id: params.settingsSchema }); this._container = params.container; this._layoutManager = params.layoutManager; this._effects = params.effects; this._monitorIndex = params.monitorIndex; this._controlPosition = params.controlPosition; this.background = this._createBackground(); this._newBackground = null; }, destroy: function() { if (this._newBackground) { this._newBackground.actor.destroy(); this._newBackground = null; } if (this.background) { this.background.actor.destroy(); this.background = null; } }, _updateBackground: function() { let newBackground = this._createBackground(); newBackground.vignetteSharpness = this.background.vignetteSharpness; newBackground.brightness = this.background.brightness; newBackground.visible = this.background.visible; newBackground.loadedSignalId = newBackground.connect('loaded', Lang.bind(this, function() { newBackground.disconnect(newBackground.loadedSignalId); newBackground.loadedSignalId = 0; if (this._newBackground != newBackground) { /* Not interesting, we queued another load */ newBackground.actor.destroy(); return; } Tweener.addTween(this.background.actor, { opacity: 0, time: FADE_ANIMATION_TIME, transition: 'easeOutQuad', onComplete: Lang.bind(this, function() { this.background.actor.destroy(); this.background = newBackground; this._newBackground = null; this.emit('changed'); }) }); })); this._newBackground = newBackground; }, _createBackground: function() { let background = new Background({ monitorIndex: this._monitorIndex, layoutManager: this._layoutManager, effects: this._effects, settings: this._settings, overrideImage: this._overrideImage }); this._container.add_child(background.actor); let monitor = this._layoutManager.monitors[this._monitorIndex]; background.actor.set_size(monitor.width, monitor.height); if (this._controlPosition) { background.actor.set_position(monitor.x, monitor.y); background.actor.lower_bottom(); } background.changeSignalId = background.connect('changed', Lang.bind(this, function() { background.disconnect(background.changeSignalId); background.changeSignalId = 0; this._updateBackground(); })); background.actor.connect('destroy', Lang.bind(this, function() { if (background.changeSignalId) background.disconnect(background.changeSignalId); if (background.loadedSignalId) background.disconnect(background.loadedSignalId); })); return background; }, }); Signals.addSignalMethods(BackgroundManager.prototype);