[gnome-shell] LookingGlass - JavaScript prompt and interactive Clutter controller



commit 52ae75d4b8d3d5c1129f2d9db862df197f634ab5
Author: Colin Walters <walters verbum org>
Date:   Sun Aug 2 03:46:01 2009 -0400

    LookingGlass - JavaScript prompt and interactive Clutter controller
    
    Add a dropdown pane triggered by Alt-F2, "lg" which supports interactive
    JavaScript evaluation, saving/restoring a history file, as well as
    an inspector element to pick by using the mouse.

 js/ui/lookingGlass.js |  470 +++++++++++++++++++++++++++++++++++++++++++++++++
 js/ui/main.js         |   10 +
 js/ui/runDialog.js    |    7 +-
 3 files changed, 486 insertions(+), 1 deletions(-)
---
diff --git a/js/ui/lookingGlass.js b/js/ui/lookingGlass.js
new file mode 100644
index 0000000..cba5920
--- /dev/null
+++ b/js/ui/lookingGlass.js
@@ -0,0 +1,470 @@
+/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */
+
+const Big = imports.gi.Big;
+const Clutter = imports.gi.Clutter;
+const Gio = imports.gi.Gio;
+const Pango = imports.gi.Pango;
+const Shell = imports.gi.Shell;
+const Signals = imports.signals;
+const Lang = imports.lang;
+const Mainloop = imports.mainloop;
+
+const Tweener = imports.ui.tweener;
+const Main = imports.ui.main;
+
+const LG_BORDER_COLOR = new Clutter.Color();
+LG_BORDER_COLOR.from_pixel(0x0000aca0);
+const LG_BACKGROUND_COLOR = new Clutter.Color();
+LG_BACKGROUND_COLOR.from_pixel(0x000000d5);
+const GREY = new Clutter.Color();
+GREY.from_pixel(0xAFAFAFFF);
+const MATRIX_GREEN = new Clutter.Color();
+MATRIX_GREEN.from_pixel(0x88ff66ff);
+// FIXME pull from GConf
+const MATRIX_FONT = 'Monospace 10';
+
+                    /* Imports...feel free to add here as needed */
+var commandHeader = "const Clutter = imports.gi.Clutter; " +
+                    "const GLib = imports.gi.GLib; " +
+                    "const Gtk = imports.gi.Gtk; " +
+                    "const Mainloop = imports.mainloop; " +
+                    "const Meta = imports.gi.Meta; " +
+                    "const Shell = imports.gi.Shell; " +
+                    "const Main = imports.ui.main; " +
+                    "const Lang = imports.lang; " +
+                    "const Tweener = imports.ui.tweener; " +
+                    /* Utility functions...we should probably be able to use these
+                     * in the shell core code too. */
+                    "const global = Shell.Global.get(); " +
+                    "const stage = global.stage; " +
+                    "const color = function(pixel) { let c= new Clutter.Color(); c.from_pixel(pixel); return c; }; " +
+                    /* Special lookingGlass functions */
+                    "const it = Main.lookingGlass.getIt(); " +
+                    "const r = Lang.bind(Main.lookingGlass, Main.lookingGlass.getResult); ";
+
+function Result(command, o, index) {
+    this._init(command, o, index);
+}
+
+Result.prototype = {
+    _init : function(command, o, index) {
+        this.index = index;
+        this.o = o;
+
+        this.actor = new Big.Box();
+
+        let cmdTxt = new Clutter.Text({ color: MATRIX_GREEN,
+                                        font_name: MATRIX_FONT,
+                                        ellipsize: Pango.EllipsizeMode.END,
+                                        text: command });
+        this.actor.append(cmdTxt, Big.BoxPackFlags.NONE);
+        let resultTxt = new Clutter.Text({ color: MATRIX_GREEN,
+                                           font_name: MATRIX_FONT,
+                                           ellipsize: Pango.EllipsizeMode.END,
+                                           text: "r(" + index + ") = " + o });
+        this.actor.append(resultTxt, Big.BoxPackFlags.NONE);
+        let line = new Big.Box({ opacity: 0, border_color: GREY,
+                                border_bottom: 1,
+                                height: 4 });
+        this.actor.append(line, Big.BoxPackFlags.NONE);
+    }
+}
+
+function ActorHierarchy() {
+    this._init();
+}
+
+ActorHierarchy.prototype = {
+    _init : function () {
+        this._previousTarget = null;
+        this._target = null;
+
+        this._parentList = [];
+
+        this.actor = new Big.Box({ spacing: 4,
+                                   border: 1,
+                                   padding: 4,
+                                   border_color: GREY });
+    },
+
+    setTarget: function(actor) {
+        this._previousTarget = this._target;
+        this.target = actor;
+
+        this.actor.remove_all();
+
+        /* FIXME - need scrolling here */
+        return;
+
+        if (this.target == null)
+            return;
+
+        this._parentList = [];
+        let parent = actor;
+        while ((parent = parent.get_parent()) != null) {
+            this._parentList.push(parent);
+
+            let link = new Clutter.Text({ color: MATRIX_GREEN,
+                                          font_name: MATRIX_FONT,
+                                          reactive: true });
+            this.actor.append(link, Big.BoxPackFlags.IF_FITS);
+            let parentTarget = parent;
+            link.connect('button-press-event', Lang.bind(this, function () {
+                this._selectByActor(parentTarget);
+            }));
+        }
+        this.emit('selection', actor);
+    },
+
+    _selectByActor: function(actor) {
+        let idx = this._parentList.indexOf(actor);
+        let children = this.actor.get_children();
+        let link = children[idx];
+        this.emit('selection', actor);
+    }
+}
+Signals.addSignalMethods(ActorHierarchy.prototype);
+
+function PropertyInspector() {
+    this._init();
+}
+
+PropertyInspector.prototype = {
+    _init : function () {
+        this._target = null;
+
+        this._parentList = [];
+
+        this.actor = new Big.Box({ spacing: 4,
+                                   border: 1,
+                                   padding: 4,
+                                   border_color: GREY });
+    },
+
+    setTarget: function(actor) {
+        this.target = actor;
+
+        this.actor.remove_all();
+
+        /* FIXME - need scrolling here */
+        return;
+
+        for (let propName in actor) {
+            let valueStr;
+            try {
+                valueStr = "" + actor[propName];
+            } catch (e) {
+                valueStr = '<error>';
+            }
+            let propText = propName + ": " + valueStr;
+            let propDisplay = new Clutter.Text({ color: MATRIX_GREEN,
+                                                 font_name: MATRIX_FONT,
+                                                 reactive: true,
+                                                 text: propText });
+            this.actor.append(propDisplay, Big.BoxPackFlags.IF_FITS);
+        }
+    }
+}
+
+function LookingGlass() {
+    this._init();
+}
+
+LookingGlass.prototype = {
+    _init : function() {
+        let global = Shell.Global.get();
+
+        this._idleHistorySaveId = 0;
+        let historyPath = Shell.Global.get().configdir + "/lookingglass-history.txt";
+        this._historyFile = Gio.file_new_for_path(historyPath);
+        this._savedText = null;
+        this._historyNavIndex = -1;
+        this._history = [];
+        this._readHistory();
+
+        this._open = false;
+
+        this._offset = 0;
+        this._results = [];
+
+        // TODO replace with scrolling or something better
+        this._maxItems = 10;
+
+        this.actor = new Big.Box({ background_color: LG_BACKGROUND_COLOR,
+                                   border: 1,
+                                   border_color: LG_BORDER_COLOR,
+                                   corner_radius: 4,
+                                   padding_top: 8,
+                                   padding_left: 4,
+                                   padding_right: 4,
+                                   padding_bottom: 4,
+                                   spacing: 4,
+                                   visible: false
+                                });
+        global.stage.add_actor(this.actor);
+
+        let toolbar = new Big.Box({ orientation: Big.BoxOrientation.HORIZONTAL,
+                                    border: 1, border_color: GREY,
+                                    corner_radius: 4 });
+        this.actor.append(toolbar, Big.BoxPackFlags.NONE);
+        let inspectIcon = Shell.TextureCache.get_default().load_gicon(new Gio.ThemedIcon({ name: 'gtk-color-picker' }),
+                                                                      24);
+        toolbar.append(inspectIcon, Big.BoxPackFlags.NONE);
+        inspectIcon.reactive = true;
+        inspectIcon.connect('button-press-event', Lang.bind(this, function () {
+            let global = Shell.Global.get();
+            let width = 150;
+            let eventHandler = new Big.Box({ background_color: LG_BACKGROUND_COLOR,
+                                             border: 1,
+                                             border_color: LG_BORDER_COLOR,
+                                             corner_radius: 4,
+                                             y: global.stage.height/2,
+                                             reactive: true
+                                          });
+            eventHandler.connect('notify::allocation', Lang.bind(this, function () {
+                eventHandler.x = Math.floor((global.stage.width)/2 - (eventHandler.width)/2);
+            }));
+            global.stage.add_actor(eventHandler);
+            let displayText = new Clutter.Text({ color: MATRIX_GREEN,
+                                                 font_name: MATRIX_FONT, text: '' });
+            eventHandler.append(displayText, Big.BoxPackFlags.EXPAND);
+            eventHandler.connect('button-press-event', Lang.bind(this, function (actor, event) {
+                let global = Shell.Global.get();
+                Clutter.ungrab_pointer(eventHandler);
+
+                let [stageX, stageY] = event.get_coords();
+                let target = global.stage.get_actor_at_pos(Clutter.PickMode.ALL,
+                                                           stageX,
+                                                           stageY);
+                this._pushResult('<inspect x:' + stageX + ' y:' + stageY + '>',
+                                 target);
+                this._hierarchy.setTarget(target);
+                eventHandler.destroy();
+                this.actor.show();
+                let global = Shell.Global.get();
+                global.stage.set_key_focus(this._entry);
+                return true;
+            }));
+            eventHandler.connect('motion-event', Lang.bind(this, function (actor, event) {
+                let global = Shell.Global.get();
+                let [stageX, stageY] = event.get_coords();
+                let target = global.stage.get_actor_at_pos(Clutter.PickMode.ALL,
+                                                           stageX,
+                                                           stageY);
+                displayText.text = '<inspect x: ' + stageX + ' y: ' + stageY + '> ' + target;
+                return true;
+            }));
+            Clutter.grab_pointer(eventHandler);
+            this.actor.hide();
+            return true;
+        }));
+
+        this._mainContent = new Big.Box({ orientation: Big.BoxOrientation.HORIZONTAL,
+                                          spacing: 4 });
+        this.actor.append(this._mainContent, Big.BoxPackFlags.EXPAND);
+
+        this._resultsArea = new Big.Box({ orientation: Big.BoxOrientation.VERTICAL,
+                                        spacing: 0 });
+        this._mainContent.append(this._resultsArea, Big.BoxPackFlags.EXPAND);
+
+        let entryArea = new Big.Box({ orientation: Big.BoxOrientation.HORIZONTAL });
+
+        let label = new Clutter.Text({ color: MATRIX_GREEN,
+                                       font_name: MATRIX_FONT,
+                                       text: 'js>>> ' });
+        entryArea.append(label, Big.BoxPackFlags.NONE);
+
+        this._entry = new Clutter.Text({ color: MATRIX_GREEN,
+                                         font_name: MATRIX_FONT,
+                                         editable: true,
+                                         activatable: true,
+                                         singleLineMode: true,
+                                         text: ''});
+        entryArea.append(this._entry, Big.BoxPackFlags.EXPAND);
+        this.actor.append(entryArea, Big.BoxPackFlags.NONE);
+
+        let inspectionBox = new Big.Box({ spacing: 4 });
+        this._mainContent.append(inspectionBox, Big.BoxPackFlags.NONE);
+
+        this._hierarchy = new ActorHierarchy();
+        inspectionBox.append(this._hierarchy.actor, Big.BoxPackFlags.EXPAND);
+        this._propInspector = new PropertyInspector();
+        inspectionBox.append(this._propInspector.actor, Big.BoxPackFlags.EXPAND);
+        this._hierarchy.connect('selected', Lang.bind(this, function (h, actor) {
+            this._propInspector.setTarget(actor);
+        }));
+
+        this._entry.connect('activate', Lang.bind(this, function (o, e) {
+            let text = o.get_text();
+            // Ensure we don't get newlines in the command; the history file is
+            // newline-separated.
+            text.replace('\n', ' ');
+            this._evaluate(text);
+            this._historyNavIndex = -1;
+            return true;
+        }));
+        this._entry.connect('key-press-event', Lang.bind(this, function(o, e) {
+            let symbol = Shell.get_event_key_symbol(e);
+            if (symbol == Clutter.Escape) {
+                this.close();
+                return true;
+            } else if (symbol == Clutter.Up) {
+                if (this._historyNavIndex >= this._history.length - 1)
+                    return true;
+                this._historyNavIndex++;
+                if (this._historyNavIndex == 0)
+                    this._savedText = this._entry.text;
+                this._entry.text = this._history[this._history.length - this._historyNavIndex - 1];
+                return true;
+            } else if (symbol == Clutter.Down) {
+                if (this._historyNavIndex <= 0)
+                    return true;
+                this._historyNavIndex--;
+                if (this._historyNavIndex < 0)
+                    this._entry.text = this._savedText;
+                else
+                    this._entry.text = this._history[this._history.length - this._historyNavIndex - 1];
+                return true;
+            } else {
+                this._historyNavIndex = -1;
+                this._savedText = null;
+                return false;
+            }
+        }));
+    },
+
+    _readHistory: function () {
+        if (!this._historyFile.query_exists(null))
+            return;
+        let [result, contents, length, etag] = this._historyFile.load_contents(null);
+        this._history = contents.split('\n');
+    },
+
+    _queueHistorySave: function() {
+        if (this._idleHistorySaveId > 0)
+            return;
+        this._idleHistorySaveId = Mainloop.timeout_add_seconds(30, Lang.bind(this, this._doSaveHistory));
+    },
+
+    _doSaveHistory: function () {
+        this._idleHistorySaveId = false;
+        let output = this._historyFile.replace(null, true, Gio.FileCreateFlags.NONE, null);
+        let dataOut = new Gio.DataOutputStream({ base_stream: output });
+        dataOut.put_string(this._history.join('\n'), null);
+        dataOut.put_string('\n', null);
+        dataOut.close(null);
+        return false;
+    },
+
+    _pushResult: function(command, obj) {
+        let index = this._results.length + this._offset;
+        let result = new Result('>>> ' + command, obj, index);
+        this._results.push(result);
+        this._resultsArea.append(result.actor, Big.BoxPackFlags.NONE);
+        this._propInspector.setTarget(obj);
+        let children = this._resultsArea.get_children();
+        if (children.length > this._maxItems) {
+            this._results.shift();
+            children[0].destroy();
+            this._offset++;
+        }
+        this._it = obj;
+    },
+
+    _evaluate : function(command) {
+        this._history.push(command);
+        this._queueHistorySave();
+
+        let fullCmd = commandHeader + command;
+
+        let resultObj;
+        try {
+            resultObj = eval(fullCmd);
+        } catch (e) {
+            resultObj = "<exception " + e + ">";
+        }
+
+        this._pushResult(command, resultObj);
+        this._hierarchy.setTarget(null);
+        this._entry.text = '';
+    },
+
+    getIt: function () {
+        return this._it;
+    },
+
+    getResult: function(idx) {
+        return this._results[idx - this._offset].o;
+    },
+
+    toggle: function() {
+        if (this._open)
+            this.close();
+        else
+            this.open();
+    },
+
+    _resizeTo: function(actor) {
+        let stage = Shell.Global.get().stage;
+        let stageWidth = stage.width;
+        let myWidth = stage.width * 0.7;
+        let myHeight = stage.height * 0.7;
+        let [srcX, srcY] = actor.get_transformed_position();
+        this.actor.x = srcX + (stage.width-myWidth)/2;
+        this._hiddenY = srcY + actor.height - myHeight - 4; // -4 to hide the top corners
+        this._targetY = this._hiddenY + myHeight;
+        this.actor.y = this._hiddenY;
+        this.actor.width = myWidth;
+        this.actor.height = myHeight;
+    },
+
+    slaveTo: function(actor) {
+        this._slaveTo = actor;
+        actor.connect('notify::allocation', Lang.bind(this, function () {
+            this._resizeTo(actor);
+        }));
+        this._resizeTo(actor);
+    },
+
+    open : function() {
+        if (this._open)
+            return;
+
+        this.actor.show();
+        this.actor.lower(Main.chrome.actor);
+        this._open = true;
+
+        Tweener.removeTweens(this.actor);
+
+        if (!Main.startModal())
+            return;
+
+        let global = Shell.Global.get();
+        global.stage.set_key_focus(this._entry);
+
+        Tweener.addTween(this.actor, { time: 0.5,
+                                       transition: "easeOutQuad",
+                                       y: this._targetY
+                                     });
+    },
+
+    close : function() {
+        if (!this._open)
+            return;
+
+        this._historyNavIndex = -1;
+        this._open = false;
+        Tweener.removeTweens(this.actor);
+
+        Main.endModal();
+
+        Tweener.addTween(this.actor, { time: 0.5,
+                                       transition: "easeOutQuad",
+                                       y: this._hiddenY,
+                                       onComplete: Lang.bind(this, function () {
+                                           this.actor.hide();
+                                       })
+                                     });
+    }
+};
+Signals.addSignalMethods(LookingGlass.prototype);
diff --git a/js/ui/main.js b/js/ui/main.js
index f87f289..d22d523 100644
--- a/js/ui/main.js
+++ b/js/ui/main.js
@@ -13,6 +13,7 @@ const Chrome = imports.ui.chrome;
 const Overlay = imports.ui.overlay;
 const Panel = imports.ui.panel;
 const RunDialog = imports.ui.runDialog;
+const LookingGlass = imports.ui.lookingGlass;
 const Sidebar = imports.ui.sidebar;
 const Tweener = imports.ui.tweener;
 const WindowManager = imports.ui.windowManager;
@@ -25,6 +26,7 @@ let panel = null;
 let sidebar = null;
 let overlay = null;
 let runDialog = null;
+let lookingGlass = null;
 let wm = null;
 let recorder = null;
 let inModal = false;
@@ -142,6 +144,14 @@ function endModal() {
     inModal = false;
 }
 
+function createLookingGlass() {
+    if (lookingGlass == null) {
+        lookingGlass = new LookingGlass.LookingGlass();
+        lookingGlass.slaveTo(panel.actor);
+    }
+    return lookingGlass;
+}
+
 function createAppLaunchContext() {
     let global = Shell.Global.get();
     let screen = global.screen;
diff --git a/js/ui/runDialog.js b/js/ui/runDialog.js
index 2334b5b..9e84782 100644
--- a/js/ui/runDialog.js
+++ b/js/ui/runDialog.js
@@ -31,7 +31,12 @@ RunDialog.prototype = {
 
         this._isOpen = false;
 
-        this._internalCommands = { 'restart': Lang.bind(this, function() {
+        this._internalCommands = { 'lg':
+                                   Lang.bind(this, function() {
+                                       Mainloop.idle_add(function() { Main.createLookingGlass().open(); });
+                                   }),
+                                   
+                                   'restart': Lang.bind(this, function() {
                                        let global = Shell.Global.get();
                                        global.reexec_self();
                                    })



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]