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.
This commit is contained in:
Colin Walters 2009-08-02 03:46:01 -04:00
parent 224538c885
commit 52ae75d4b8
3 changed files with 486 additions and 1 deletions

470
js/ui/lookingGlass.js Normal file
View File

@ -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);

View File

@ -13,6 +13,7 @@ const Chrome = imports.ui.chrome;
const Overlay = imports.ui.overlay; const Overlay = imports.ui.overlay;
const Panel = imports.ui.panel; const Panel = imports.ui.panel;
const RunDialog = imports.ui.runDialog; const RunDialog = imports.ui.runDialog;
const LookingGlass = imports.ui.lookingGlass;
const Sidebar = imports.ui.sidebar; const Sidebar = imports.ui.sidebar;
const Tweener = imports.ui.tweener; const Tweener = imports.ui.tweener;
const WindowManager = imports.ui.windowManager; const WindowManager = imports.ui.windowManager;
@ -25,6 +26,7 @@ let panel = null;
let sidebar = null; let sidebar = null;
let overlay = null; let overlay = null;
let runDialog = null; let runDialog = null;
let lookingGlass = null;
let wm = null; let wm = null;
let recorder = null; let recorder = null;
let inModal = false; let inModal = false;
@ -142,6 +144,14 @@ function endModal() {
inModal = false; inModal = false;
} }
function createLookingGlass() {
if (lookingGlass == null) {
lookingGlass = new LookingGlass.LookingGlass();
lookingGlass.slaveTo(panel.actor);
}
return lookingGlass;
}
function createAppLaunchContext() { function createAppLaunchContext() {
let global = Shell.Global.get(); let global = Shell.Global.get();
let screen = global.screen; let screen = global.screen;

View File

@ -31,7 +31,12 @@ RunDialog.prototype = {
this._isOpen = false; 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(); let global = Shell.Global.get();
global.reexec_self(); global.reexec_self();
}) })