/* -*- 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 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 Notebook() { this._init(); } Notebook.prototype = { _init: function() { this.actor = new Big.Box(); this.tabControls = new Big.Box({ orientation: Big.BoxOrientation.HORIZONTAL, spacing: 4, padding: 2 }); this._selectedIndex = -1; this._tabs = []; }, appendPage: function(name, child) { let labelOuterBox = new Big.Box({ padding: 2 }); let labelBox = new Big.Box({ padding: 2, border_color: MATRIX_GREEN, reactive: true }); labelOuterBox.append(labelBox, Big.BoxPackFlags.NONE); let label = new Clutter.Text({ color: MATRIX_GREEN, font_name: MATRIX_FONT, text: name }); labelBox.connect('button-press-event', Lang.bind(this, function () { this.selectChild(child); return true; })); labelBox.append(label, Big.BoxPackFlags.EXPAND); this._tabs.push([child, labelBox]); child.hide(); this.actor.append(child, Big.BoxPackFlags.EXPAND); this.tabControls.append(labelOuterBox, Big.BoxPackFlags.NONE); if (this._selectedIndex == -1) this.selectIndex(0); }, _unselect: function() { if (this._selectedIndex < 0) return; let [child, labelBox] = this._tabs[this._selectedIndex]; labelBox.padding = 2; labelBox.border = 0; child.hide(); this._selectedIndex = -1; }, selectIndex: function(index) { if (index == this._selectedIndex) return; this._unselect(); if (index < 0) { this.emit('selection', null); return; } let [child, labelBox] = this._tabs[index]; labelBox.padding = 1; labelBox.border = 1; child.show(); this._selectedIndex = index; this.emit('selection', child); }, selectChild: function(child) { if (child == null) this.selectIndex(-1); else { for (let i = 0; i < this._tabs.length; i++) { let [tabChild, labelBox] = this._tabs[i]; if (tabChild == child) { this.selectIndex(i); return; } } } } } Signals.addSignalMethods(Notebook.prototype); 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({ border_color: GREY, border_bottom: 1, height: 8 }); 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(); if (!(actor instanceof Clutter.Actor)) 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, text: "" + parent }); this.actor.append(link, Big.BoxPackFlags.IF_FITS); let parentTarget = parent; link.connect('button-press-event', Lang.bind(this, function () { this._selectByActor(parentTarget); return true; })); } 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(); for (let propName in actor) { let valueStr; try { valueStr = "" + actor[propName]; } catch (e) { valueStr = ''; } 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 Inspector() { this._init(); } Inspector.prototype = { _init: function() { let width = 150; let primary = global.get_primary_monitor(); let eventHandler = new Big.Box({ background_color: LG_BACKGROUND_COLOR, border: 1, border_color: LG_BORDER_COLOR, corner_radius: 4, y: primary.y + Math.floor(primary.height / 2), reactive: true }); eventHandler.connect('notify::allocation', Lang.bind(this, function () { eventHandler.x = primary.x + Math.floor((primary.width - 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); let borderPaintTarget = null; let borderPaintId = null; eventHandler.connect('destroy', Lang.bind(this, function() { if (borderPaintTarget != null) borderPaintTarget.disconnect(borderPaintId); })); eventHandler.connect('button-press-event', Lang.bind(this, function (actor, event) { Clutter.ungrab_pointer(eventHandler); let [stageX, stageY] = event.get_coords(); let target = global.stage.get_actor_at_pos(Clutter.PickMode.ALL, stageX, stageY); this.emit('target', target, stageX, stageY); eventHandler.destroy(); this.emit('closed'); return true; })); eventHandler.connect('motion-event', Lang.bind(this, function (actor, event) { let [stageX, stageY] = event.get_coords(); let target = global.stage.get_actor_at_pos(Clutter.PickMode.ALL, stageX, stageY); displayText.text = ' ' + target; if (borderPaintTarget != null) borderPaintTarget.disconnect(borderPaintId); borderPaintTarget = target; borderPaintId = Shell.add_hook_paint_red_border(target); return true; })); Clutter.grab_pointer(eventHandler); } } Signals.addSignalMethods(Inspector.prototype); function LookingGlass() { this._init(); } LookingGlass.prototype = { _init : function() { this._idleHistorySaveId = 0; let historyPath = global.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 inspector = new Inspector(); inspector.connect('target', Lang.bind(this, function(i, target, stageX, stageY) { this._pushResult('', target); this._hierarchy.setTarget(target); })); inspector.connect('closed', Lang.bind(this, function() { this.actor.show(); global.stage.set_key_focus(this._entry); })); this.actor.hide(); return true; })); let notebook = new Notebook(); this.actor.append(notebook.actor, Big.BoxPackFlags.EXPAND); toolbar.append(notebook.tabControls, Big.BoxPackFlags.END); this._evalBox = new Big.Box({ orientation: Big.BoxOrientation.VERTICAL, spacing: 4 }); notebook.appendPage('Evaluator', this._evalBox); this._resultsArea = new Big.Box({ orientation: Big.BoxOrientation.VERTICAL, spacing: 4 }); this._evalBox.append(this._resultsArea, Big.BoxPackFlags.EXPAND); let entryArea = new Big.Box({ orientation: Big.BoxOrientation.HORIZONTAL }); this._evalBox.append(entryArea, Big.BoxPackFlags.NONE); 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: ''}); /* unmapping the edit box will un-focus it, undo that */ notebook.connect('selection', Lang.bind(this, function (nb, child) { if (child == this._evalBox) global.stage.set_key_focus(this._entry); })); entryArea.append(this._entry, Big.BoxPackFlags.EXPAND); this._hierarchy = new ActorHierarchy(); notebook.appendPage('Hierarchy', this._hierarchy.actor); this._propInspector = new PropertyInspector(); notebook.appendPage('Properties', this._propInspector.actor); this._hierarchy.connect('selection', Lang.bind(this, function (h, actor) { this._pushResult('', actor); notebook.selectIndex(0); })); 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', ' '); // Strip leading and trailing whitespace text = text.replace(/^\s+/g, "").replace(/\s+$/g, ""); if (text == '') return true; this._evaluate(text); this._historyNavIndex = -1; return true; })); this._entry.connect('key-press-event', Lang.bind(this, function(o, e) { let symbol = e.get_key_symbol(); 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').filter(function (e) { return e != ''; }); }, _queueHistorySave: function() { if (this._idleHistorySaveId > 0) return; this._idleHistorySaveId = Mainloop.timeout_add_seconds(5, 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 = ""; } 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 primary = global.get_primary_monitor(); let myWidth = primary.width * 0.7; let myHeight = primary.height * 0.7; let [srcX, srcY] = actor.get_transformed_position(); this.actor.x = srcX + (primary.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; if (!Main.pushModal(this.actor)) return; this.actor.show(); this.actor.lower(Main.chrome.actor); this._open = true; Tweener.removeTweens(this.actor); 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.popModal(this.actor); Tweener.addTween(this.actor, { time: 0.5, transition: "easeOutQuad", y: this._hiddenY, onComplete: Lang.bind(this, function () { this.actor.hide(); }) }); } }; Signals.addSignalMethods(LookingGlass.prototype);