From 50aa15dec9c64d6b08fc863bdb909ab07438c110 Mon Sep 17 00:00:00 2001 From: Giovanni Campagna Date: Tue, 20 Dec 2011 18:41:23 +0100 Subject: [PATCH] Reintroduce Wanda The Fish When transitioning from gnome-panel to gnome-shell in 3.0 we lost the ability to summon the wisdom of the mythical fish. This patch restores this, for the few adepts that are aware of the magical incantation. (Not as configurable as the original one, but it's an easter egg after all...) https://bugzilla.gnome.org/show_bug.cgi?id=666606 --- js/Makefile.am | 1 + js/ui/overview.js | 3 + js/ui/wanda.js | 210 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 214 insertions(+) create mode 100644 js/ui/wanda.js diff --git a/js/Makefile.am b/js/Makefile.am index ca7756ed8..63c11a0dc 100644 --- a/js/Makefile.am +++ b/js/Makefile.am @@ -72,6 +72,7 @@ nobase_dist_js_DATA = \ ui/tweener.js \ ui/userMenu.js \ ui/viewSelector.js \ + ui/wanda.js \ ui/windowAttentionHandler.js \ ui/windowManager.js \ ui/workspace.js \ diff --git a/js/ui/overview.js b/js/ui/overview.js index fa3650a50..566d3f1a4 100644 --- a/js/ui/overview.js +++ b/js/ui/overview.js @@ -23,6 +23,7 @@ const Params = imports.misc.params; const PlaceDisplay = imports.ui.placeDisplay; const Tweener = imports.ui.tweener; const ViewSelector = imports.ui.viewSelector; +const Wanda = imports.ui.wanda; const WorkspacesView = imports.ui.workspacesView; const WorkspaceThumbnail = imports.ui.workspaceThumbnail; @@ -201,6 +202,8 @@ const Overview = new Lang.Class({ this._viewSelector.addViewTab('applications', _("Applications"), appView.actor, 'system-run'); // Default search providers + // Wanda comes obviously first + this.addSearchProvider(new Wanda.WandaSearchProvider()); this.addSearchProvider(new AppDisplay.AppSearchProvider()); this.addSearchProvider(new AppDisplay.SettingsSearchProvider()); this.addSearchProvider(new PlaceDisplay.PlaceSearchProvider()); diff --git a/js/ui/wanda.js b/js/ui/wanda.js new file mode 100644 index 000000000..98228aae6 --- /dev/null +++ b/js/ui/wanda.js @@ -0,0 +1,210 @@ +// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- + +const Clutter = imports.gi.Clutter; +const GdkPixbuf = imports.gi.GdkPixbuf; +const GLib = imports.gi.GLib; +const Lang = imports.lang; +const Shell = imports.gi.Shell; +const Signals = imports.signals; +const St = imports.gi.St; + +const IconGrid = imports.ui.iconGrid; +const Main = imports.ui.main; +const Search = imports.ui.search; + +// we could make these gsettings +const FISH_NAME = 'wanda'; +const FISH_SPEED = 300; +const FISH_COMMAND = 'fortune'; + +const GNOME_PANEL_PIXMAPDIR = '../gnome-panel/pixmaps'; +const FISH_GROUP = 'Fish Animation'; + +const MAGIC_FISH_KEY = 'free the fish'; + +const WandaIcon = new Lang.Class({ + Name: 'WandaIcon', + Extends: IconGrid.BaseIcon, + + _init : function(fish, label, params) { + this._fish = fish; + let file = GLib.build_filenamev([global.datadir, GNOME_PANEL_PIXMAPDIR, fish + '.fish']); + + if (GLib.file_test(file, GLib.FileTest.EXISTS)) { + this._keyfile = new GLib.KeyFile(); + this._keyfile.load_from_file(file, GLib.KeyFileFlags.NONE); + + this._imageFile = GLib.build_filenamev([global.datadir, GNOME_PANEL_PIXMAPDIR, + this._keyfile.get_string(FISH_GROUP, 'image')]); + + let tmpPixbuf = GdkPixbuf.Pixbuf.new_from_file(this._imageFile); + + this._imgHeight = tmpPixbuf.height; + this._imgWidth = tmpPixbuf.width / this._keyfile.get_integer(FISH_GROUP, 'frames'); + } else { + this._imageFile = null; + } + + this.parent(label, params); + }, + + createIcon: function(iconSize) { + if (this._animations) + this._animations.destroy(); + + if (!this._imageFile) { + return new St.Icon({ icon_name: 'face-smile', + icon_type: St.IconType.FULLCOLOR, + icon_size: iconSize + }); + } + + this._animations = St.TextureCache.get_default().load_sliced_image(this._imageFile, this._imgWidth, this._imgHeight); + this._animations.connect('destroy', Lang.bind(this, function() { + if (this._timeoutId) + GLib.source_remove(this._timeoutId); + this._timeoutId = 0; + this._animations = null; + })); + this._animations.connect('notify::mapped', Lang.bind(this, function() { + if (this._animations.mapped && !this._timeoutId) { + this._timeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, FISH_SPEED, Lang.bind(this, this._update)); + + this._i = 0; + this._update(); + } else if (!this._animations.mapped && this._timeoutId) { + GLib.source_remove(this._timeoutId); + this._timeoutId = 0; + } + })); + + this._i = 0; + + return this._animations; + }, + + _update: function() { + let n = this._animations.get_n_children(); + if (n == 0) { + return true; + } + + this._animations.get_nth_child(this._i).hide(); + this._i = (this._i + 1) % n; + this._animations.get_nth_child(this._i).show(); + + return true; + }, +}); + +const WandaIconBin = new Lang.Class({ + Name: 'WandaIconBin', + + _init: function(fish, label, params) { + this.actor = new St.Bin({ style_class: 'search-result-content', + reactive: true, + track_hover: true }); + this.icon = new WandaIcon(fish, label, params); + + this.actor.child = this.icon.actor; + this.actor.label_actor = this.icon.label; + }, +}); + +const FortuneDialog = new Lang.Class({ + Name: 'FortuneDialog', + + _init: function(name, command) { + let text; + + try { + let [res, stdout, stderr, status] = GLib.spawn_command_line_sync(command); + text = String.fromCharCode.apply(null, stdout); + } catch(e) { + text = _("Sorry, no wisdom for you today:\n%s").format(e.message); + } + + this._title = new St.Label({ style_class: 'polkit-dialog-headline', + text: _("%s the Oracle says").format(name) }); + this._label = new St.Label({ style_class: 'polkit-dialog-description', + text: text }); + this._label.clutter_text.line_wrap = true; + + this._box = new St.BoxLayout({ vertical: true, + style_class: 'polkit-dialog' // this is just to force a reasonable width + }); + this._box.add(this._title, { align: St.Align.MIDDLE }); + this._box.add(this._label, { expand: true }); + + this._button = new St.Button({ button_mask: St.ButtonMask.ONE, + style_class: 'modal-dialog', + reactive: true }); + this._button.connect('clicked', Lang.bind(this, this.destroy)); + this._button.child = this._box; + + let monitor = Main.layoutManager.primaryMonitor; + + Main.layoutManager.addChrome(this._button); + this._button.set_position(Math.floor(monitor.width / 2 - this._button.width / 2), + Math.floor(monitor.height / 2 - this._button.height / 2)); + + GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, 10, Lang.bind(this, this.destroy)); + }, + + destroy: function() { + this._button.destroy(); + } +}); + +function capitalize(str) { + return str[0].toUpperCase() + str.substring(1, str.length); +} + +const WandaSearchProvider = new Lang.Class({ + Name: 'WandaSearchProvider', + Extends: Search.SearchProvider, + + _init: function() { + this.parent(_("Your favorite Easter Egg")); + }, + + getResultMeta: function(fish) { + return { 'id': fish, + 'name': capitalize(fish), + 'createIcon': function(iconSize) { + // for DND only (maybe could be improved) + // DON'T use St.Icon here, it crashes the shell + // (dnd.js code assumes it can query the actor size + // without parenting it, while StWidget accesses + // StThemeNode in get_preferred_width/height, which + // triggers an assertion failure) + return St.TextureCache.get_default().load_icon_name(null, + 'face-smile', + St.IconType.FULLCOLOR, + iconSize); + } + }; + }, + + getInitialResultSet: function(terms) { + if (terms.join(' ') == MAGIC_FISH_KEY) { + return [ FISH_NAME ]; + } + return []; + }, + + getSubsearchResultSet: function(previousResults, terms) { + return this.getInitialResultSet(terms); + }, + + activateResult: function(fish, params) { + if (this._dialog) + this._dialog.destroy(); + this._dialog = new FortuneDialog(capitalize(fish), FISH_COMMAND); + }, + + createResultActor: function (resultMeta, terms) { + let icon = new WandaIconBin(resultMeta.id, resultMeta.name); + return icon.actor; + } +});