2fa6f7ba7e
MutterWindow and MutterPlugin have been renamed to MetaWindowActor and MetaPlugin, mutter_plugin_list_windows() to meta_plugin_list_window_actors(). Adapt to those changes. https://bugzilla.gnome.org/show_bug.cgi?id=632500
980 lines
35 KiB
JavaScript
980 lines
35 KiB
JavaScript
/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */
|
|
|
|
const Clutter = imports.gi.Clutter;
|
|
const GConf = imports.gi.GConf;
|
|
const GLib = imports.gi.GLib;
|
|
const Gio = imports.gi.Gio;
|
|
const Pango = imports.gi.Pango;
|
|
const St = imports.gi.St;
|
|
const Shell = imports.gi.Shell;
|
|
const Signals = imports.signals;
|
|
const Lang = imports.lang;
|
|
const Mainloop = imports.mainloop;
|
|
const Gettext = imports.gettext.domain('gnome-shell');
|
|
const _ = Gettext.gettext;
|
|
|
|
const ExtensionSystem = imports.ui.extensionSystem;
|
|
const Link = imports.ui.link;
|
|
const Tweener = imports.ui.tweener;
|
|
const Main = imports.ui.main;
|
|
|
|
/* 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 St.BoxLayout({ vertical: true });
|
|
|
|
this.tabControls = new St.BoxLayout({ style_class: 'labels' });
|
|
|
|
this._selectedIndex = -1;
|
|
this._tabs = [];
|
|
},
|
|
|
|
appendPage: function(name, child) {
|
|
let labelBox = new St.BoxLayout({ style_class: 'notebook-tab',
|
|
reactive: true,
|
|
track_hover: true });
|
|
let label = new St.Button({ label: name });
|
|
label.connect('clicked', Lang.bind(this, function () {
|
|
this.selectChild(child);
|
|
return true;
|
|
}));
|
|
labelBox.add(label, { expand: true });
|
|
this.tabControls.add(labelBox);
|
|
|
|
let scrollview = new St.ScrollView({ x_fill: true, y_fill: true });
|
|
scrollview.get_hscroll_bar().hide();
|
|
scrollview.add_actor(child);
|
|
|
|
let tabData = { child: child,
|
|
labelBox: labelBox,
|
|
label: label,
|
|
scrollView: scrollview,
|
|
_scrollToBottom: false };
|
|
this._tabs.push(tabData);
|
|
scrollview.hide();
|
|
this.actor.add(scrollview, { expand: true });
|
|
|
|
let vAdjust = scrollview.vscroll.adjustment;
|
|
vAdjust.connect('changed', Lang.bind(this, function () { this._onAdjustScopeChanged(tabData); }));
|
|
vAdjust.connect('notify::value', Lang.bind(this, function() { this._onAdjustValueChanged(tabData); }));
|
|
|
|
if (this._selectedIndex == -1)
|
|
this.selectIndex(0);
|
|
},
|
|
|
|
_unselect: function() {
|
|
if (this._selectedIndex < 0)
|
|
return;
|
|
let tabData = this._tabs[this._selectedIndex];
|
|
tabData.labelBox.remove_style_pseudo_class('selected');
|
|
tabData.scrollView.hide();
|
|
this._selectedIndex = -1;
|
|
},
|
|
|
|
selectIndex: function(index) {
|
|
if (index == this._selectedIndex)
|
|
return;
|
|
this._unselect();
|
|
if (index < 0) {
|
|
this.emit('selection', null);
|
|
return;
|
|
}
|
|
let tabData = this._tabs[index];
|
|
tabData.labelBox.add_style_pseudo_class('selected');
|
|
tabData.scrollView.show();
|
|
this._selectedIndex = index;
|
|
this.emit('selection', tabData.child);
|
|
},
|
|
|
|
selectChild: function(child) {
|
|
if (child == null)
|
|
this.selectIndex(-1);
|
|
else {
|
|
for (let i = 0; i < this._tabs.length; i++) {
|
|
let tabData = this._tabs[i];
|
|
if (tabData.child == child) {
|
|
this.selectIndex(i);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
scrollToBottom: function(index) {
|
|
let tabData = this._tabs[index];
|
|
tabData._scrollToBottom = true;
|
|
|
|
},
|
|
|
|
_onAdjustValueChanged: function (tabData) {
|
|
let vAdjust = tabData.scrollView.vscroll.adjustment;
|
|
if (vAdjust.value < (vAdjust.upper - vAdjust.lower - 0.5))
|
|
tabData._scrolltoBottom = false;
|
|
},
|
|
|
|
_onAdjustScopeChanged: function (tabData) {
|
|
if (!tabData._scrollToBottom)
|
|
return;
|
|
let vAdjust = tabData.scrollView.vscroll.adjustment;
|
|
vAdjust.value = vAdjust.upper - vAdjust.page_size;
|
|
}
|
|
};
|
|
Signals.addSignalMethods(Notebook.prototype);
|
|
|
|
function objectToString(o) {
|
|
if (typeof(o) == typeof(objectToString)) {
|
|
// special case this since the default is way, way too verbose
|
|
return "<js function>";
|
|
} else {
|
|
return "" + o;
|
|
}
|
|
}
|
|
|
|
function ObjLink(o, title) {
|
|
this._init(o, title);
|
|
}
|
|
|
|
ObjLink.prototype = {
|
|
__proto__: Link.Link,
|
|
|
|
_init: function(o, title) {
|
|
let text;
|
|
if (title)
|
|
text = title;
|
|
else
|
|
text = objectToString(o);
|
|
text = GLib.markup_escape_text(text, -1);
|
|
this._obj = o;
|
|
Link.Link.prototype._init.call(this, { label: text });
|
|
this.actor.get_child().single_line_mode = true;
|
|
this.actor.connect('clicked', Lang.bind(this, this._onClicked));
|
|
},
|
|
|
|
_onClicked: function (link) {
|
|
Main.lookingGlass.inspectObject(this._obj, this.actor);
|
|
}
|
|
};
|
|
|
|
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 St.BoxLayout({ vertical: true });
|
|
|
|
let cmdTxt = new St.Label({ text: command });
|
|
cmdTxt.clutter_text.ellipsize = Pango.EllipsizeMode.END;
|
|
this.actor.add(cmdTxt);
|
|
let box = new St.BoxLayout({});
|
|
this.actor.add(box);
|
|
let resultTxt = new St.Label({ text: 'r(' + index + ') = ' });
|
|
resultTxt.clutter_text.ellipsize = Pango.EllipsizeMode.END;
|
|
box.add(resultTxt);
|
|
let objLink = new ObjLink(o);
|
|
box.add(objLink.actor);
|
|
let line = new Clutter.Rectangle({ name: 'Separator' });
|
|
let padBin = new St.Bin({ name: 'Separator', x_fill: true, y_fill: true });
|
|
padBin.add_actor(line);
|
|
this.actor.add(padBin);
|
|
}
|
|
};
|
|
|
|
function WindowList() {
|
|
this._init();
|
|
}
|
|
|
|
WindowList.prototype = {
|
|
_init : function () {
|
|
this.actor = new St.BoxLayout({ name: 'Windows', vertical: true, style: 'spacing: 8px' });
|
|
let display = global.screen.get_display();
|
|
let tracker = Shell.WindowTracker.get_default();
|
|
this._updateId = Main.initializeDeferredWork(this.actor, Lang.bind(this, this._updateWindowList));
|
|
display.connect('window-created', Lang.bind(this, this._updateWindowList));
|
|
tracker.connect('tracked-windows-changed', Lang.bind(this, this._updateWindowList));
|
|
},
|
|
|
|
_updateWindowList: function() {
|
|
this.actor.get_children().forEach(function (actor) { actor.destroy(); });
|
|
let windows = global.get_window_actors();
|
|
let tracker = Shell.WindowTracker.get_default();
|
|
for (let i = 0; i < windows.length; i++) {
|
|
let metaWindow = windows[i].metaWindow;
|
|
// Avoid multiple connections
|
|
if (!metaWindow._lookingGlassManaged) {
|
|
metaWindow.connect('unmanaged', Lang.bind(this, this._updateWindowList));
|
|
metaWindow._lookingGlassManaged = true;
|
|
}
|
|
let box = new St.BoxLayout({ vertical: true });
|
|
this.actor.add(box);
|
|
let windowLink = new ObjLink(metaWindow, metaWindow.title);
|
|
box.add(windowLink.actor, { x_align: St.Align.START, x_fill: false });
|
|
let propsBox = new St.BoxLayout({ vertical: true, style: 'padding-left: 6px;' });
|
|
box.add(propsBox);
|
|
propsBox.add(new St.Label({ text: 'wmclass: ' + metaWindow.get_wm_class() }));
|
|
let app = tracker.get_window_app(metaWindow);
|
|
if (app != null && !app.is_transient()) {
|
|
let icon = app.create_icon_texture(22);
|
|
let propBox = new St.BoxLayout({ style: 'spacing: 6px; ' });
|
|
propsBox.add(propBox);
|
|
propBox.add(new St.Label({ text: 'app: ' }), { y_fill: false });
|
|
let appLink = new ObjLink(app, app.get_id());
|
|
propBox.add(appLink.actor, { y_fill: false });
|
|
propBox.add(icon, { y_fill: false });
|
|
} else {
|
|
propsBox.add(new St.Label({ text: '<untracked>' }));
|
|
}
|
|
}
|
|
}
|
|
};
|
|
Signals.addSignalMethods(WindowList.prototype);
|
|
|
|
function ObjInspector() {
|
|
this._init();
|
|
}
|
|
|
|
ObjInspector.prototype = {
|
|
_init : function () {
|
|
this._obj = null;
|
|
this._previousObj = null;
|
|
|
|
this._parentList = [];
|
|
|
|
this.actor = new St.ScrollView({ x_fill: true, y_fill: true });
|
|
this.actor.get_hscroll_bar().hide();
|
|
this._container = new St.BoxLayout({ name: 'LookingGlassPropertyInspector',
|
|
style_class: 'lg-dialog',
|
|
vertical: true });
|
|
this.actor.add_actor(this._container);
|
|
},
|
|
|
|
selectObject: function(obj, skipPrevious) {
|
|
if (!skipPrevious)
|
|
this._previousObj = this._obj;
|
|
else
|
|
this._previousObj = null;
|
|
this._obj = obj;
|
|
|
|
this._container.get_children().forEach(function (child) { child.destroy(); });
|
|
|
|
let hbox = new St.BoxLayout({ style_class: 'lg-obj-inspector-title' });
|
|
this._container.add_actor(hbox);
|
|
let label = new St.Label({ text: 'Inspecting: %s: %s'.format(typeof(obj),
|
|
objectToString(obj)) });
|
|
label.single_line_mode = true;
|
|
hbox.add(label, { expand: true, y_fill: false });
|
|
let button = new St.Button({ label: 'Insert', style_class: 'lg-obj-inspector-button' });
|
|
button.connect('clicked', Lang.bind(this, this._onInsert));
|
|
hbox.add(button);
|
|
|
|
if (this._previousObj != null) {
|
|
button = new St.Button({ label: 'Back', style_class: 'lg-obj-inspector-button' });
|
|
button.connect('clicked', Lang.bind(this, this._onBack));
|
|
hbox.add(button);
|
|
}
|
|
|
|
button = new St.Button({ style_class: 'window-close' });
|
|
button.connect('clicked', Lang.bind(this, this.close));
|
|
hbox.add(button);
|
|
if (typeof(obj) == typeof({})) {
|
|
for (let propName in obj) {
|
|
let valueStr;
|
|
let link;
|
|
try {
|
|
let prop = obj[propName];
|
|
link = new ObjLink(prop).actor;
|
|
} catch (e) {
|
|
link = new St.Label({ text: '<error>' });
|
|
}
|
|
let hbox = new St.BoxLayout();
|
|
let propText = propName + ": " + valueStr;
|
|
hbox.add(new St.Label({ text: propName + ': ' }));
|
|
hbox.add(link);
|
|
this._container.add_actor(hbox);
|
|
}
|
|
}
|
|
},
|
|
|
|
open: function(sourceActor) {
|
|
if (this._open)
|
|
return;
|
|
this._previousObj = null;
|
|
this._open = true;
|
|
this.actor.show();
|
|
if (sourceActor) {
|
|
this.actor.set_scale(0, 0);
|
|
let [sourceX, sourceY] = sourceActor.get_transformed_position();
|
|
let [sourceWidth, sourceHeight] = sourceActor.get_transformed_size();
|
|
this.actor.move_anchor_point(Math.floor(sourceX + sourceWidth / 2),
|
|
Math.floor(sourceY + sourceHeight / 2));
|
|
Tweener.addTween(this.actor, { scale_x: 1, scale_y: 1,
|
|
transition: "easeOutQuad",
|
|
time: 0.2 });
|
|
} else {
|
|
this.actor.set_scale(1, 1);
|
|
}
|
|
},
|
|
|
|
close: function() {
|
|
if (!this._open)
|
|
return;
|
|
this._open = false;
|
|
this.actor.hide();
|
|
this._previousObj = null;
|
|
this._obj = null;
|
|
},
|
|
|
|
_onInsert: function() {
|
|
let obj = this._obj;
|
|
this.close();
|
|
Main.lookingGlass.insertObject(obj);
|
|
},
|
|
|
|
_onBack: function() {
|
|
this.selectObject(this._previousObj, true);
|
|
}
|
|
};
|
|
|
|
function Inspector() {
|
|
this._init();
|
|
}
|
|
|
|
Inspector.prototype = {
|
|
_init: function() {
|
|
let container = new Shell.GenericContainer({ width: 0,
|
|
height: 0 });
|
|
container.connect('allocate', Lang.bind(this, this._allocate));
|
|
Main.uiGroup.add_actor(container);
|
|
|
|
let eventHandler = new St.BoxLayout({ name: 'LookingGlassDialog',
|
|
vertical: false,
|
|
reactive: true });
|
|
this._eventHandler = eventHandler;
|
|
container.add_actor(eventHandler);
|
|
this._displayText = new St.Label();
|
|
eventHandler.add(this._displayText, { expand: true });
|
|
|
|
this._borderPaintTarget = null;
|
|
this._borderPaintId = null;
|
|
eventHandler.connect('destroy', Lang.bind(this, this._onDestroy));
|
|
eventHandler.connect('key-press-event', Lang.bind(this, this._onKeyPressEvent));
|
|
eventHandler.connect('button-press-event', Lang.bind(this, this._onButtonPressEvent));
|
|
eventHandler.connect('scroll-event', Lang.bind(this, this._onScrollEvent));
|
|
eventHandler.connect('motion-event', Lang.bind(this, this._onMotionEvent));
|
|
Clutter.grab_pointer(eventHandler);
|
|
Clutter.grab_keyboard(eventHandler);
|
|
|
|
// this._target is the actor currently shown by the inspector.
|
|
// this._pointerTarget is the actor directly under the pointer.
|
|
// Normally these are the same, but if you use the scroll wheel
|
|
// to drill down, they'll diverge until you either scroll back
|
|
// out, or move the pointer outside of _pointerTarget.
|
|
this._target = null;
|
|
this._pointerTarget = null;
|
|
},
|
|
|
|
_allocate: function(actor, box, flags) {
|
|
let primary = global.get_primary_monitor();
|
|
|
|
let [minWidth, minHeight, natWidth, natHeight] =
|
|
this._eventHandler.get_preferred_size();
|
|
|
|
let childBox = new Clutter.ActorBox();
|
|
childBox.x1 = primary.x + Math.floor((primary.width - natWidth) / 2);
|
|
childBox.x2 = childBox.x1 + natWidth;
|
|
childBox.y1 = primary.y + Math.floor((primary.height - natHeight) / 2);
|
|
childBox.y2 = childBox.y1 + natHeight;
|
|
this._eventHandler.allocate(childBox, flags);
|
|
},
|
|
|
|
_close: function() {
|
|
Clutter.ungrab_pointer(this._eventHandler);
|
|
Clutter.ungrab_keyboard(this._eventHandler);
|
|
this._eventHandler.destroy();
|
|
this.emit('closed');
|
|
},
|
|
|
|
_onDestroy: function() {
|
|
if (this._borderPaintTarget != null)
|
|
this._borderPaintTarget.disconnect(this._borderPaintId);
|
|
},
|
|
|
|
_onKeyPressEvent: function (actor, event) {
|
|
if (event.get_key_symbol() == Clutter.Escape)
|
|
this._close();
|
|
return true;
|
|
},
|
|
|
|
_onButtonPressEvent: function (actor, event) {
|
|
if (this._target) {
|
|
let [stageX, stageY] = event.get_coords();
|
|
this.emit('target', this._target, stageX, stageY);
|
|
}
|
|
this._close();
|
|
return true;
|
|
},
|
|
|
|
_onScrollEvent: function (actor, event) {
|
|
switch (event.get_scroll_direction()) {
|
|
case Clutter.ScrollDirection.UP:
|
|
// select parent
|
|
let parent = this._target.get_parent();
|
|
if (parent != null) {
|
|
this._target = parent;
|
|
this._update(event);
|
|
}
|
|
break;
|
|
|
|
case Clutter.ScrollDirection.DOWN:
|
|
// select child
|
|
if (this._target != this._pointerTarget) {
|
|
let child = this._pointerTarget;
|
|
while (child) {
|
|
let parent = child.get_parent();
|
|
if (parent == this._target)
|
|
break;
|
|
child = parent;
|
|
}
|
|
if (child) {
|
|
this._target = child;
|
|
this._update(event);
|
|
}
|
|
}
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
return true;
|
|
},
|
|
|
|
_onMotionEvent: function (actor, event) {
|
|
this._update(event);
|
|
return true;
|
|
},
|
|
|
|
_update: function(event) {
|
|
let [stageX, stageY] = event.get_coords();
|
|
let target = global.stage.get_actor_at_pos(Clutter.PickMode.ALL,
|
|
stageX,
|
|
stageY);
|
|
|
|
if (target != this._pointerTarget)
|
|
this._target = target;
|
|
this._pointerTarget = target;
|
|
|
|
let position = '[inspect x: ' + stageX + ' y: ' + stageY + ']';
|
|
this._displayText.text = '';
|
|
this._displayText.text = position + ' ' + this._target;
|
|
if (this._borderPaintTarget != null)
|
|
this._borderPaintTarget.disconnect(this._borderPaintId);
|
|
this._borderPaintTarget = this._target;
|
|
this._borderPaintId = Shell.add_hook_paint_red_border(this._target);
|
|
}
|
|
};
|
|
|
|
Signals.addSignalMethods(Inspector.prototype);
|
|
|
|
function ErrorLog() {
|
|
this._init();
|
|
}
|
|
|
|
ErrorLog.prototype = {
|
|
_init: function() {
|
|
this.actor = new St.BoxLayout();
|
|
this.text = new St.Label();
|
|
this.actor.add(this.text);
|
|
// We need to override StLabel's default ellipsization when
|
|
// using line_wrap; otherwise ClutterText's layout is going
|
|
// to constrain both the width and height, which prevents
|
|
// scrolling.
|
|
this.text.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
|
|
this.text.clutter_text.line_wrap = true;
|
|
this.actor.connect('notify::mapped', Lang.bind(this, this._renderText));
|
|
},
|
|
|
|
_formatTime: function(d){
|
|
function pad(n) { return n < 10 ? '0' + n : n; }
|
|
return d.getUTCFullYear()+'-'
|
|
+ pad(d.getUTCMonth()+1)+'-'
|
|
+ pad(d.getUTCDate())+'T'
|
|
+ pad(d.getUTCHours())+':'
|
|
+ pad(d.getUTCMinutes())+':'
|
|
+ pad(d.getUTCSeconds())+'Z';
|
|
},
|
|
|
|
_renderText: function() {
|
|
if (!this.actor.mapped)
|
|
return;
|
|
let text = this.text.text;
|
|
let stack = Main._getAndClearErrorStack();
|
|
for (let i = 0; i < stack.length; i++) {
|
|
let logItem = stack[i];
|
|
text += logItem.category + ' t=' + this._formatTime(new Date(logItem.timestamp)) + ' ' + logItem.message + '\n';
|
|
}
|
|
this.text.text = text;
|
|
}
|
|
};
|
|
|
|
function Extensions() {
|
|
this._init();
|
|
}
|
|
|
|
Extensions.prototype = {
|
|
_init: function() {
|
|
this.actor = new St.BoxLayout({ vertical: true,
|
|
name: 'lookingGlassExtensions' });
|
|
this._noExtensions = new St.Label({ style_class: 'lg-extensions-none',
|
|
text: _("No extensions installed") });
|
|
this._extensionsList = new St.BoxLayout({ vertical: true,
|
|
style_class: 'lg-extensions-list' });
|
|
this.actor.add(this._extensionsList);
|
|
this._loadExtensionList();
|
|
},
|
|
|
|
_loadExtensionList: function() {
|
|
let extensions = ExtensionSystem.extensionMeta;
|
|
let totalExtensions = 0;
|
|
for (let uuid in extensions) {
|
|
let extensionDisplay = this._createExtensionDisplay(extensions[uuid]);
|
|
this._extensionsList.add(extensionDisplay);
|
|
totalExtensions++;
|
|
}
|
|
if (totalExtensions == 0) {
|
|
this._extensionsList.add(this._noExtensions);
|
|
}
|
|
},
|
|
|
|
_onViewSource: function (actor) {
|
|
let meta = actor._extensionMeta;
|
|
let file = Gio.file_new_for_path(meta.path);
|
|
let uri = file.get_uri();
|
|
Gio.app_info_launch_default_for_uri(uri, global.create_app_launch_context());
|
|
Main.lookingGlass.close();
|
|
},
|
|
|
|
_onWebPage: function (actor) {
|
|
let meta = actor._extensionMeta;
|
|
Gio.app_info_launch_default_for_uri(meta.url, global.create_app_launch_context());
|
|
Main.lookingGlass.close();
|
|
},
|
|
|
|
_stateToString: function(extensionState) {
|
|
switch (extensionState) {
|
|
case ExtensionSystem.ExtensionState.ENABLED:
|
|
return _("Enabled");
|
|
case ExtensionSystem.ExtensionState.DISABLED:
|
|
return _("Disabled");
|
|
case ExtensionSystem.ExtensionState.ERROR:
|
|
return _("Error");
|
|
case ExtensionSystem.ExtensionState.OUT_OF_DATE:
|
|
return _("Out of date");
|
|
}
|
|
return 'Unknown'; // Not translated, shouldn't appear
|
|
},
|
|
|
|
_createExtensionDisplay: function(meta) {
|
|
let box = new St.BoxLayout({ style_class: 'lg-extension', vertical: true });
|
|
let name = new St.Label({ style_class: 'lg-extension-name',
|
|
text: meta.name });
|
|
box.add(name, { expand: true });
|
|
let description = new St.Label({ style_class: 'lg-extension-description',
|
|
text: meta.description });
|
|
box.add(description, { expand: true });
|
|
|
|
let metaBox = new St.BoxLayout();
|
|
box.add(metaBox);
|
|
let stateString = this._stateToString(meta.state);
|
|
let state = new St.Label({ style_class: 'lg-extension-state',
|
|
text: this._stateToString(meta.state) });
|
|
|
|
let actionsContainer = new St.Bin({ x_align: St.Align.END });
|
|
metaBox.add(actionsContainer);
|
|
let actionsBox = new St.BoxLayout({ style_class: 'lg-extension-actions' });
|
|
actionsContainer.set_child(actionsBox);
|
|
|
|
let viewsource = new Link.Link({ label: _("View Source") });
|
|
viewsource.actor._extensionMeta = meta;
|
|
viewsource.actor.connect('clicked', Lang.bind(this, this._onViewSource));
|
|
actionsBox.add(viewsource.actor);
|
|
|
|
if (meta.url) {
|
|
let webpage = new Link.Link({ label: _("Web Page") });
|
|
webpage.actor._extensionMeta = meta;
|
|
webpage.actor.connect('clicked', Lang.bind(this, this._onWebPage));
|
|
actionsBox.add(webpage.actor);
|
|
}
|
|
|
|
return box;
|
|
}
|
|
};
|
|
|
|
function LookingGlass() {
|
|
this._init();
|
|
}
|
|
|
|
LookingGlass.prototype = {
|
|
_init : function() {
|
|
this._idleHistorySaveId = 0;
|
|
let historyPath = global.userdatadir + '/lookingglass-history.txt';
|
|
this._historyFile = Gio.file_new_for_path(historyPath);
|
|
this._savedText = null;
|
|
this._historyNavIndex = -1;
|
|
this._history = [];
|
|
this._borderPaintTarget = null;
|
|
this._borderPaintId = 0;
|
|
this._borderDestroyId = 0;
|
|
|
|
this._readHistory();
|
|
|
|
this._open = false;
|
|
|
|
this._offset = 0;
|
|
this._results = [];
|
|
|
|
// Sort of magic, but...eh.
|
|
this._maxItems = 150;
|
|
|
|
this.actor = new St.BoxLayout({ name: 'LookingGlassDialog',
|
|
style_class: 'lg-dialog',
|
|
vertical: true,
|
|
visible: false });
|
|
|
|
let gconf = GConf.Client.get_default();
|
|
gconf.add_dir('/desktop/gnome/interface', GConf.ClientPreloadType.PRELOAD_NONE);
|
|
gconf.notify_add('/desktop/gnome/interface/monospace_font_name',
|
|
Lang.bind(this, this._updateFont));
|
|
this._updateFont();
|
|
|
|
Main.uiGroup.add_actor(this.actor);
|
|
|
|
this._objInspector = new ObjInspector();
|
|
Main.uiGroup.add_actor(this._objInspector.actor);
|
|
this._objInspector.actor.hide();
|
|
|
|
let toolbar = new St.BoxLayout({ name: 'Toolbar' });
|
|
this.actor.add_actor(toolbar);
|
|
let inspectIcon = St.TextureCache.get_default().load_icon_name('gtk-color-picker',
|
|
St.IconType.SYMBOLIC,
|
|
24);
|
|
toolbar.add_actor(inspectIcon);
|
|
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('<inspect x:' + stageX + ' y:' + stageY + '>',
|
|
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._notebook = notebook;
|
|
this.actor.add(notebook.actor, { expand: true });
|
|
|
|
let emptyBox = new St.Bin();
|
|
toolbar.add(emptyBox, { expand: true });
|
|
toolbar.add_actor(notebook.tabControls);
|
|
|
|
this._evalBox = new St.BoxLayout({ name: 'EvalBox', vertical: true });
|
|
notebook.appendPage('Evaluator', this._evalBox);
|
|
|
|
this._resultsArea = new St.BoxLayout({ name: 'ResultsArea', vertical: true });
|
|
this._evalBox.add(this._resultsArea, { expand: true });
|
|
|
|
let entryArea = new St.BoxLayout({ name: 'EntryArea' });
|
|
this._evalBox.add_actor(entryArea);
|
|
|
|
let label = new St.Label({ text: 'js>>> ' });
|
|
entryArea.add(label);
|
|
|
|
this._entry = new St.Entry();
|
|
/* 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.add(this._entry, { expand: true });
|
|
|
|
this._windowList = new WindowList();
|
|
this._windowList.connect('selected', Lang.bind(this, function(list, window) {
|
|
notebook.selectIndex(0);
|
|
this._pushResult('<window selection>', window);
|
|
}));
|
|
notebook.appendPage('Windows', this._windowList.actor);
|
|
|
|
this._errorLog = new ErrorLog();
|
|
notebook.appendPage('Errors', this._errorLog.actor);
|
|
|
|
this._extensions = new Extensions();
|
|
notebook.appendPage('Extensions', this._extensions.actor);
|
|
|
|
this._entry.clutter_text.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.clutter_text.connect('key-press-event', Lang.bind(this, function(o, e) {
|
|
let symbol = e.get_key_symbol();
|
|
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;
|
|
}
|
|
}));
|
|
},
|
|
|
|
_updateFont: function() {
|
|
let gconf = GConf.Client.get_default();
|
|
let fontName = gconf.get_string('/desktop/gnome/interface/monospace_font_name');
|
|
// This is mishandled by the scanner - should by Pango.FontDescription_from_string(fontName);
|
|
// https://bugzilla.gnome.org/show_bug.cgi?id=595889
|
|
let fontDesc = Pango.font_description_from_string(fontName);
|
|
// We ignore everything but size and style; you'd be crazy to set your system-wide
|
|
// monospace font to be bold/oblique/etc. Could easily be added here.
|
|
this.actor.style =
|
|
'font-size: ' + fontDesc.get_size() / 1024. + (fontDesc.get_size_is_absolute() ? 'px' : 'pt') + ';'
|
|
+ 'font-family: "' + fontDesc.get_family() + '";';
|
|
},
|
|
|
|
_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.add(result.actor);
|
|
if (this._borderPaintTarget != null) {
|
|
this._borderPaintTarget.disconnect(this._borderPaintId);
|
|
this._borderPaintTarget = null;
|
|
}
|
|
if (obj instanceof Clutter.Actor) {
|
|
this._borderPaintTarget = obj;
|
|
this._borderPaintId = Shell.add_hook_paint_red_border(obj);
|
|
this._borderDestroyId = obj.connect('destroy', Lang.bind(this, function () {
|
|
this._borderDestroyId = 0;
|
|
this._borderPaintTarget = null;
|
|
}));
|
|
}
|
|
let children = this._resultsArea.get_children();
|
|
if (children.length > this._maxItems) {
|
|
this._results.shift();
|
|
children[0].destroy();
|
|
this._offset++;
|
|
}
|
|
this._it = obj;
|
|
|
|
// Scroll to bottom
|
|
this._notebook.scrollToBottom(0);
|
|
},
|
|
|
|
_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._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;
|
|
this._objInspector.actor.set_size(Math.floor(myWidth * 0.8), Math.floor(myHeight * 0.8));
|
|
this._objInspector.actor.set_position(this.actor.x + Math.floor(myWidth * 0.1),
|
|
this._targetY + Math.floor(myHeight * 0.1));
|
|
},
|
|
|
|
slaveTo: function(actor) {
|
|
this._slaveTo = actor;
|
|
actor.connect('notify::allocation', Lang.bind(this, function () {
|
|
this._resizeTo(actor);
|
|
}));
|
|
this._resizeTo(actor);
|
|
},
|
|
|
|
insertObject: function(obj) {
|
|
this._pushResult('<insert>', obj);
|
|
},
|
|
|
|
inspectObject: function(obj, sourceActor) {
|
|
this._objInspector.open(sourceActor);
|
|
this._objInspector.selectObject(obj);
|
|
},
|
|
|
|
// Handle key events which are relevant for all tabs of the LookingGlass
|
|
_globalKeyPressEvent : function(actor, event) {
|
|
let symbol = event.get_key_symbol();
|
|
if (symbol == Clutter.Escape) {
|
|
if (this._objInspector.actor.visible) {
|
|
this._objInspector.close();
|
|
} else {
|
|
this.close();
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
},
|
|
|
|
open : function() {
|
|
if (this._open)
|
|
return;
|
|
|
|
if (!Main.pushModal(this.actor))
|
|
return;
|
|
|
|
this._keyPressEventId = global.stage.connect('key-press-event',
|
|
Lang.bind(this, this._globalKeyPressEvent));
|
|
|
|
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;
|
|
|
|
if (this._keyPressEventId)
|
|
global.stage.disconnect(this._keyPressEventId);
|
|
|
|
this._objInspector.actor.hide();
|
|
|
|
this._historyNavIndex = -1;
|
|
this._open = false;
|
|
Tweener.removeTweens(this.actor);
|
|
|
|
if (this._borderPaintTarget != null) {
|
|
this._borderPaintTarget.disconnect(this._borderPaintId);
|
|
this._borderPaintTarget.disconnect(this._borderDestroyId);
|
|
this._borderPaintTarget = null;
|
|
}
|
|
|
|
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);
|