summaryrefslogtreecommitdiffstats
path: root/js/ui/lookingGlass.js
diff options
context:
space:
mode:
Diffstat (limited to 'js/ui/lookingGlass.js')
-rw-r--r--js/ui/lookingGlass.js1373
1 files changed, 1373 insertions, 0 deletions
diff --git a/js/ui/lookingGlass.js b/js/ui/lookingGlass.js
new file mode 100644
index 0000000..5abee0f
--- /dev/null
+++ b/js/ui/lookingGlass.js
@@ -0,0 +1,1373 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+/* exported LookingGlass */
+
+const { Clutter, Cogl, Gio, GLib, GObject,
+ Graphene, Meta, Pango, Shell, St } = imports.gi;
+const Signals = imports.signals;
+const System = imports.system;
+
+const History = imports.misc.history;
+const ExtensionUtils = imports.misc.extensionUtils;
+const ShellEntry = imports.ui.shellEntry;
+const Main = imports.ui.main;
+const JsParse = imports.misc.jsParse;
+
+const { ExtensionState } = ExtensionUtils;
+
+const CHEVRON = '>>> ';
+
+/* Imports...feel free to add here as needed */
+var commandHeader = 'const { Clutter, Gio, GLib, GObject, Meta, Shell, St } = imports.gi; ' +
+ 'const Main = imports.ui.main; ' +
+ /* Utility functions...we should probably be able to use these
+ * in the shell core code too. */
+ 'const stage = global.stage; ' +
+ /* Special lookingGlass functions */
+ 'const inspect = Main.lookingGlass.inspect.bind(Main.lookingGlass); ' +
+ 'const it = Main.lookingGlass.getIt(); ' +
+ 'const r = Main.lookingGlass.getResult.bind(Main.lookingGlass); ';
+
+const HISTORY_KEY = 'looking-glass-history';
+// Time between tabs for them to count as a double-tab event
+var AUTO_COMPLETE_DOUBLE_TAB_DELAY = 500;
+var AUTO_COMPLETE_SHOW_COMPLETION_ANIMATION_DURATION = 200;
+var AUTO_COMPLETE_GLOBAL_KEYWORDS = _getAutoCompleteGlobalKeywords();
+
+const LG_ANIMATION_TIME = 500;
+
+function _getAutoCompleteGlobalKeywords() {
+ const keywords = ['true', 'false', 'null', 'new'];
+ // Don't add the private properties of globalThis (i.e., ones starting with '_')
+ const windowProperties = Object.getOwnPropertyNames(globalThis).filter(
+ a => a.charAt(0) !== '_');
+ const headerProperties = JsParse.getDeclaredConstants(commandHeader);
+
+ return keywords.concat(windowProperties).concat(headerProperties);
+}
+
+var AutoComplete = class AutoComplete {
+ constructor(entry) {
+ this._entry = entry;
+ this._entry.connect('key-press-event', this._entryKeyPressEvent.bind(this));
+ this._lastTabTime = global.get_current_time();
+ }
+
+ _processCompletionRequest(event) {
+ if (event.completions.length == 0)
+ return;
+
+ // Unique match = go ahead and complete; multiple matches + single tab = complete the common starting string;
+ // multiple matches + double tab = emit a suggest event with all possible options
+ if (event.completions.length == 1) {
+ this.additionalCompletionText(event.completions[0], event.attrHead);
+ this.emit('completion', { completion: event.completions[0], type: 'whole-word' });
+ } else if (event.completions.length > 1 && event.tabType === 'single') {
+ let commonPrefix = JsParse.getCommonPrefix(event.completions);
+
+ if (commonPrefix.length > 0) {
+ this.additionalCompletionText(commonPrefix, event.attrHead);
+ this.emit('completion', { completion: commonPrefix, type: 'prefix' });
+ this.emit('suggest', { completions: event.completions });
+ }
+ } else if (event.completions.length > 1 && event.tabType === 'double') {
+ this.emit('suggest', { completions: event.completions });
+ }
+ }
+
+ _entryKeyPressEvent(actor, event) {
+ let cursorPos = this._entry.clutter_text.get_cursor_position();
+ let text = this._entry.get_text();
+ if (cursorPos != -1)
+ text = text.slice(0, cursorPos);
+
+ if (event.get_key_symbol() == Clutter.KEY_Tab) {
+ let [completions, attrHead] = JsParse.getCompletions(text, commandHeader, AUTO_COMPLETE_GLOBAL_KEYWORDS);
+ let currTime = global.get_current_time();
+ if ((currTime - this._lastTabTime) < AUTO_COMPLETE_DOUBLE_TAB_DELAY) {
+ this._processCompletionRequest({ tabType: 'double',
+ completions,
+ attrHead });
+ } else {
+ this._processCompletionRequest({ tabType: 'single',
+ completions,
+ attrHead });
+ }
+ this._lastTabTime = currTime;
+ }
+ return Clutter.EVENT_PROPAGATE;
+ }
+
+ // Insert characters of text not already included in head at cursor position. i.e., if text="abc" and head="a",
+ // the string "bc" will be appended to this._entry
+ additionalCompletionText(text, head) {
+ let additionalCompletionText = text.slice(head.length);
+ let cursorPos = this._entry.clutter_text.get_cursor_position();
+
+ this._entry.clutter_text.insert_text(additionalCompletionText, cursorPos);
+ }
+};
+Signals.addSignalMethods(AutoComplete.prototype);
+
+
+var Notebook = GObject.registerClass({
+ Signals: { 'selection': { param_types: [Clutter.Actor.$gtype] } },
+}, class Notebook extends St.BoxLayout {
+ _init() {
+ super._init({
+ vertical: true,
+ y_expand: true,
+ });
+
+ this.tabControls = new St.BoxLayout({ style_class: 'labels' });
+
+ this._selectedIndex = -1;
+ this._tabs = [];
+ }
+
+ appendPage(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', () => {
+ this.selectChild(child);
+ return true;
+ });
+ labelBox.add_child(label);
+ this.tabControls.add(labelBox);
+
+ let scrollview = new St.ScrollView({ y_expand: true });
+ scrollview.get_hscroll_bar().hide();
+ scrollview.add_actor(child);
+
+ let tabData = { child,
+ labelBox,
+ label,
+ scrollView: scrollview,
+ _scrollToBottom: false };
+ this._tabs.push(tabData);
+ scrollview.hide();
+ this.add_child(scrollview);
+
+ let vAdjust = scrollview.vscroll.adjustment;
+ vAdjust.connect('changed', () => this._onAdjustScopeChanged(tabData));
+ vAdjust.connect('notify::value', () => this._onAdjustValueChanged(tabData));
+
+ if (this._selectedIndex == -1)
+ this.selectIndex(0);
+ }
+
+ _unselect() {
+ 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(index) {
+ if (index == this._selectedIndex)
+ return;
+ if (index < 0) {
+ this._unselect();
+ this.emit('selection', null);
+ return;
+ }
+
+ // Focus the new tab before unmapping the old one
+ let tabData = this._tabs[index];
+ if (!tabData.scrollView.navigate_focus(null, St.DirectionType.TAB_FORWARD, false))
+ this.grab_key_focus();
+
+ this._unselect();
+
+ tabData.labelBox.add_style_pseudo_class('selected');
+ tabData.scrollView.show();
+ this._selectedIndex = index;
+ this.emit('selection', tabData.child);
+ }
+
+ selectChild(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(index) {
+ let tabData = this._tabs[index];
+ tabData._scrollToBottom = true;
+
+ }
+
+ _onAdjustValueChanged(tabData) {
+ let vAdjust = tabData.scrollView.vscroll.adjustment;
+ if (vAdjust.value < (vAdjust.upper - vAdjust.lower - 0.5))
+ tabData._scrolltoBottom = false;
+ }
+
+ _onAdjustScopeChanged(tabData) {
+ if (!tabData._scrollToBottom)
+ return;
+ let vAdjust = tabData.scrollView.vscroll.adjustment;
+ vAdjust.value = vAdjust.upper - vAdjust.page_size;
+ }
+
+ nextTab() {
+ let nextIndex = this._selectedIndex;
+ if (nextIndex < this._tabs.length - 1)
+ ++nextIndex;
+
+ this.selectIndex(nextIndex);
+ }
+
+ prevTab() {
+ let prevIndex = this._selectedIndex;
+ if (prevIndex > 0)
+ --prevIndex;
+
+ this.selectIndex(prevIndex);
+ }
+});
+
+function objectToString(o) {
+ if (typeof o == typeof objectToString) {
+ // special case this since the default is way, way too verbose
+ return '<js function>';
+ } else {
+ if (o === undefined)
+ return 'undefined';
+
+ if (o === null)
+ return 'null';
+
+ return o.toString();
+ }
+}
+
+var ObjLink = GObject.registerClass(
+class ObjLink extends St.Button {
+ _init(lookingGlass, o, title) {
+ let text;
+ if (title)
+ text = title;
+ else
+ text = objectToString(o);
+ text = GLib.markup_escape_text(text, -1);
+
+ super._init({
+ reactive: true,
+ track_hover: true,
+ style_class: 'shell-link',
+ label: text,
+ x_align: Clutter.ActorAlign.START,
+ });
+ this.get_child().single_line_mode = true;
+
+ this._obj = o;
+ this._lookingGlass = lookingGlass;
+ }
+
+ vfunc_clicked() {
+ this._lookingGlass.inspectObject(this._obj, this);
+ }
+});
+
+var Result = GObject.registerClass(
+class Result extends St.BoxLayout {
+ _init(lookingGlass, command, o, index) {
+ super._init({ vertical: true });
+
+ this.index = index;
+ this.o = o;
+
+ this._lookingGlass = lookingGlass;
+
+ let cmdTxt = new St.Label({ text: command });
+ cmdTxt.clutter_text.ellipsize = Pango.EllipsizeMode.END;
+ this.add(cmdTxt);
+ let box = new St.BoxLayout({});
+ this.add(box);
+ let resultTxt = new St.Label({ text: 'r(%d) = '.format(index) });
+ resultTxt.clutter_text.ellipsize = Pango.EllipsizeMode.END;
+ box.add(resultTxt);
+ let objLink = new ObjLink(this._lookingGlass, o);
+ box.add(objLink);
+ }
+});
+
+var WindowList = GObject.registerClass({
+}, class WindowList extends St.BoxLayout {
+ _init(lookingGlass) {
+ super._init({ name: 'Windows', vertical: true, style: 'spacing: 8px' });
+ let tracker = Shell.WindowTracker.get_default();
+ this._updateId = Main.initializeDeferredWork(this, this._updateWindowList.bind(this));
+ global.display.connect('window-created', this._updateWindowList.bind(this));
+ tracker.connect('tracked-windows-changed', this._updateWindowList.bind(this));
+
+ this._lookingGlass = lookingGlass;
+ }
+
+ _updateWindowList() {
+ if (!this._lookingGlass.isOpen)
+ return;
+
+ this.destroy_all_children();
+ 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', this._updateWindowList.bind(this));
+ metaWindow._lookingGlassManaged = true;
+ }
+ let box = new St.BoxLayout({ vertical: true });
+ this.add(box);
+ let windowLink = new ObjLink(this._lookingGlass, metaWindow, metaWindow.title);
+ box.add_child(windowLink);
+ let propsBox = new St.BoxLayout({ vertical: true, style: 'padding-left: 6px;' });
+ box.add(propsBox);
+ propsBox.add(new St.Label({ text: 'wmclass: %s'.format(metaWindow.get_wm_class()) }));
+ let app = tracker.get_window_app(metaWindow);
+ if (app != null && !app.is_window_backed()) {
+ let icon = app.create_icon_texture(22);
+ let propBox = new St.BoxLayout({ style: 'spacing: 6px; ' });
+ propsBox.add(propBox);
+ propBox.add_child(new St.Label({ text: 'app: ' }));
+ let appLink = new ObjLink(this._lookingGlass, app, app.get_id());
+ propBox.add_child(appLink);
+ propBox.add_child(icon);
+ } else {
+ propsBox.add(new St.Label({ text: '<untracked>' }));
+ }
+ }
+ }
+
+ update() {
+ this._updateWindowList();
+ }
+});
+
+var ObjInspector = GObject.registerClass(
+class ObjInspector extends St.ScrollView {
+ _init(lookingGlass) {
+ super._init({
+ pivot_point: new Graphene.Point({ x: 0.5, y: 0.5 }),
+ });
+
+ this._obj = null;
+ this._previousObj = null;
+
+ this._parentList = [];
+
+ this.get_hscroll_bar().hide();
+ this._container = new St.BoxLayout({
+ name: 'LookingGlassPropertyInspector',
+ style_class: 'lg-dialog',
+ vertical: true,
+ x_expand: true,
+ y_expand: true,
+ });
+ this.add_actor(this._container);
+
+ this._lookingGlass = lookingGlass;
+ }
+
+ selectObject(obj, skipPrevious) {
+ if (!skipPrevious)
+ this._previousObj = this._obj;
+ else
+ this._previousObj = null;
+ this._obj = obj;
+
+ this._container.destroy_all_children();
+
+ 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)),
+ x_expand: true,
+ });
+ label.single_line_mode = true;
+ hbox.add_child(label);
+ let button = new St.Button({ label: 'Insert', style_class: 'lg-obj-inspector-button' });
+ button.connect('clicked', this._onInsert.bind(this));
+ hbox.add(button);
+
+ if (this._previousObj != null) {
+ button = new St.Button({ label: 'Back', style_class: 'lg-obj-inspector-button' });
+ button.connect('clicked', this._onBack.bind(this));
+ hbox.add(button);
+ }
+
+ button = new St.Button({ style_class: 'window-close' });
+ button.add_actor(new St.Icon({ icon_name: 'window-close-symbolic' }));
+ button.connect('clicked', this.close.bind(this));
+ hbox.add(button);
+ if (typeof obj == typeof {}) {
+ let properties = [];
+ for (let propName in obj)
+ properties.push(propName);
+ properties.sort();
+
+ for (let i = 0; i < properties.length; i++) {
+ let propName = properties[i];
+ let link;
+ try {
+ let prop = obj[propName];
+ link = new ObjLink(this._lookingGlass, prop);
+ } catch (e) {
+ link = new St.Label({ text: '<error>' });
+ }
+ let box = new St.BoxLayout();
+ box.add(new St.Label({ text: '%s: '.format(propName) }));
+ box.add(link);
+ this._container.add_actor(box);
+ }
+ }
+ }
+
+ open(sourceActor) {
+ if (this._open)
+ return;
+ this._previousObj = null;
+ this._open = true;
+ this.show();
+ if (sourceActor) {
+ this.set_scale(0, 0);
+ this.ease({
+ scale_x: 1,
+ scale_y: 1,
+ mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+ duration: 200,
+ });
+ } else {
+ this.set_scale(1, 1);
+ }
+ }
+
+ close() {
+ if (!this._open)
+ return;
+ this._open = false;
+ this.hide();
+ this._previousObj = null;
+ this._obj = null;
+ }
+
+ _onInsert() {
+ let obj = this._obj;
+ this.close();
+ this._lookingGlass.insertObject(obj);
+ }
+
+ _onBack() {
+ this.selectObject(this._previousObj, true);
+ }
+});
+
+var RedBorderEffect = GObject.registerClass(
+class RedBorderEffect extends Clutter.Effect {
+ _init() {
+ super._init();
+ this._pipeline = null;
+ }
+
+ vfunc_paint(paintContext) {
+ let framebuffer = paintContext.get_framebuffer();
+ let coglContext = framebuffer.get_context();
+ let actor = this.get_actor();
+ actor.continue_paint(paintContext);
+
+ if (!this._pipeline) {
+ let color = new Cogl.Color();
+ color.init_from_4ub(0xff, 0, 0, 0xc4);
+
+ this._pipeline = new Cogl.Pipeline(coglContext);
+ this._pipeline.set_color(color);
+ }
+
+ let alloc = actor.get_allocation_box();
+ let width = 2;
+
+ // clockwise order
+ framebuffer.draw_rectangle(this._pipeline,
+ 0, 0, alloc.get_width(), width);
+ framebuffer.draw_rectangle(this._pipeline,
+ alloc.get_width() - width, width,
+ alloc.get_width(), alloc.get_height());
+ framebuffer.draw_rectangle(this._pipeline,
+ 0, alloc.get_height(),
+ alloc.get_width() - width, alloc.get_height() - width);
+ framebuffer.draw_rectangle(this._pipeline,
+ 0, alloc.get_height() - width,
+ width, width);
+ }
+});
+
+var Inspector = GObject.registerClass({
+ Signals: { 'closed': {},
+ 'target': { param_types: [Clutter.Actor.$gtype, GObject.TYPE_DOUBLE, GObject.TYPE_DOUBLE] } },
+}, class Inspector extends Clutter.Actor {
+ _init(lookingGlass) {
+ super._init({ width: 0, height: 0 });
+
+ Main.uiGroup.add_actor(this);
+
+ let eventHandler = new St.BoxLayout({ name: 'LookingGlassDialog',
+ vertical: false,
+ reactive: true });
+ this._eventHandler = eventHandler;
+ this.add_actor(eventHandler);
+ this._displayText = new St.Label({ x_expand: true });
+ eventHandler.add_child(this._displayText);
+
+ eventHandler.connect('key-press-event', this._onKeyPressEvent.bind(this));
+ eventHandler.connect('button-press-event', this._onButtonPressEvent.bind(this));
+ eventHandler.connect('scroll-event', this._onScrollEvent.bind(this));
+ eventHandler.connect('motion-event', this._onMotionEvent.bind(this));
+
+ let seat = Clutter.get_default_backend().get_default_seat();
+ this._pointerDevice = seat.get_pointer();
+ this._keyboardDevice = seat.get_keyboard();
+
+ this._pointerDevice.grab(eventHandler);
+ this._keyboardDevice.grab(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;
+
+ this._lookingGlass = lookingGlass;
+ }
+
+ vfunc_allocate(box) {
+ this.set_allocation(box);
+
+ if (!this._eventHandler)
+ return;
+
+ let primary = Main.layoutManager.primaryMonitor;
+
+ let [, , 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);
+ }
+
+ _close() {
+ this._pointerDevice.ungrab();
+ this._keyboardDevice.ungrab();
+ this._eventHandler.destroy();
+ this._eventHandler = null;
+ this.emit('closed');
+ }
+
+ _onKeyPressEvent(actor, event) {
+ if (event.get_key_symbol() === Clutter.KEY_Escape)
+ this._close();
+ return Clutter.EVENT_STOP;
+ }
+
+ _onButtonPressEvent(actor, event) {
+ if (this._target) {
+ let [stageX, stageY] = event.get_coords();
+ this.emit('target', this._target, stageX, stageY);
+ }
+ this._close();
+ return Clutter.EVENT_STOP;
+ }
+
+ _onScrollEvent(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 Clutter.EVENT_STOP;
+ }
+
+ _onMotionEvent(actor, event) {
+ this._update(event);
+ return Clutter.EVENT_STOP;
+ }
+
+ _update(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: %d y: %d]'.format(stageX, stageY);
+ this._displayText.text = '';
+ this._displayText.text = '%s %s'.format(position, this._target);
+
+ this._lookingGlass.setBorderPaintTarget(this._target);
+ }
+});
+
+var Extensions = GObject.registerClass({
+}, class Extensions extends St.BoxLayout {
+ _init(lookingGlass) {
+ super._init({ vertical: true, name: 'lookingGlassExtensions' });
+
+ this._lookingGlass = lookingGlass;
+ this._noExtensions = new St.Label({ style_class: 'lg-extensions-none',
+ text: _("No extensions installed") });
+ this._numExtensions = 0;
+ this._extensionsList = new St.BoxLayout({ vertical: true,
+ style_class: 'lg-extensions-list' });
+ this._extensionsList.add(this._noExtensions);
+ this.add(this._extensionsList);
+
+ Main.extensionManager.getUuids().forEach(uuid => {
+ this._loadExtension(null, uuid);
+ });
+
+ Main.extensionManager.connect('extension-loaded',
+ this._loadExtension.bind(this));
+ }
+
+ _loadExtension(o, uuid) {
+ let extension = Main.extensionManager.lookup(uuid);
+ // There can be cases where we create dummy extension metadata
+ // that's not really a proper extension. Don't bother with these.
+ if (!extension.metadata.name)
+ return;
+
+ let extensionDisplay = this._createExtensionDisplay(extension);
+ if (this._numExtensions == 0)
+ this._extensionsList.remove_actor(this._noExtensions);
+
+ this._numExtensions++;
+ this._extensionsList.add(extensionDisplay);
+ }
+
+ _onViewSource(actor) {
+ let extension = actor._extension;
+ let uri = extension.dir.get_uri();
+ Gio.app_info_launch_default_for_uri(uri, global.create_app_launch_context(0, -1));
+ this._lookingGlass.close();
+ }
+
+ _onWebPage(actor) {
+ let extension = actor._extension;
+ Gio.app_info_launch_default_for_uri(extension.metadata.url, global.create_app_launch_context(0, -1));
+ this._lookingGlass.close();
+ }
+
+ _onViewErrors(actor) {
+ let extension = actor._extension;
+ let shouldShow = !actor._isShowing;
+
+ if (shouldShow) {
+ let errors = extension.errors;
+ let errorDisplay = new St.BoxLayout({ vertical: true });
+ if (errors && errors.length) {
+ for (let i = 0; i < errors.length; i++)
+ errorDisplay.add(new St.Label({ text: errors[i] }));
+ } else {
+ /* Translators: argument is an extension UUID. */
+ let message = _("%s has not emitted any errors.").format(extension.uuid);
+ errorDisplay.add(new St.Label({ text: message }));
+ }
+
+ actor._errorDisplay = errorDisplay;
+ actor._parentBox.add(errorDisplay);
+ actor.label = _("Hide Errors");
+ } else {
+ actor._errorDisplay.destroy();
+ actor._errorDisplay = null;
+ actor.label = _("Show Errors");
+ }
+
+ actor._isShowing = shouldShow;
+ }
+
+ _stateToString(extensionState) {
+ switch (extensionState) {
+ case ExtensionState.ENABLED:
+ return _("Enabled");
+ case ExtensionState.DISABLED:
+ case ExtensionState.INITIALIZED:
+ return _("Disabled");
+ case ExtensionState.ERROR:
+ return _("Error");
+ case ExtensionState.OUT_OF_DATE:
+ return _("Out of date");
+ case ExtensionState.DOWNLOADING:
+ return _("Downloading");
+ }
+ return 'Unknown'; // Not translated, shouldn't appear
+ }
+
+ _createExtensionDisplay(extension) {
+ let box = new St.BoxLayout({ style_class: 'lg-extension', vertical: true });
+ let name = new St.Label({
+ style_class: 'lg-extension-name',
+ text: extension.metadata.name,
+ x_expand: true,
+ });
+ box.add_child(name);
+ let description = new St.Label({
+ style_class: 'lg-extension-description',
+ text: extension.metadata.description || 'No description',
+ x_expand: true,
+ });
+ box.add_child(description);
+
+ let metaBox = new St.BoxLayout({ style_class: 'lg-extension-meta' });
+ box.add(metaBox);
+ let state = new St.Label({ style_class: 'lg-extension-state',
+ text: this._stateToString(extension.state) });
+ metaBox.add(state);
+
+ let viewsource = new St.Button({ reactive: true,
+ track_hover: true,
+ style_class: 'shell-link',
+ label: _("View Source") });
+ viewsource._extension = extension;
+ viewsource.connect('clicked', this._onViewSource.bind(this));
+ metaBox.add(viewsource);
+
+ if (extension.metadata.url) {
+ let webpage = new St.Button({ reactive: true,
+ track_hover: true,
+ style_class: 'shell-link',
+ label: _("Web Page") });
+ webpage._extension = extension;
+ webpage.connect('clicked', this._onWebPage.bind(this));
+ metaBox.add(webpage);
+ }
+
+ let viewerrors = new St.Button({ reactive: true,
+ track_hover: true,
+ style_class: 'shell-link',
+ label: _("Show Errors") });
+ viewerrors._extension = extension;
+ viewerrors._parentBox = box;
+ viewerrors._isShowing = false;
+ viewerrors.connect('clicked', this._onViewErrors.bind(this));
+ metaBox.add(viewerrors);
+
+ return box;
+ }
+});
+
+
+var ActorLink = GObject.registerClass({
+ Signals: {
+ 'inspect-actor': {},
+ },
+}, class ActorLink extends St.Button {
+ _init(actor) {
+ this._arrow = new St.Icon({
+ icon_name: 'pan-end-symbolic',
+ icon_size: 8,
+ x_align: Clutter.ActorAlign.CENTER,
+ y_align: Clutter.ActorAlign.CENTER,
+ pivot_point: new Graphene.Point({ x: 0.5, y: 0.5 }),
+ });
+
+ const label = new St.Label({
+ text: actor.toString(),
+ x_align: Clutter.ActorAlign.START,
+ });
+
+ const inspectButton = new St.Button({
+ child: new St.Icon({
+ icon_name: 'insert-object-symbolic',
+ icon_size: 12,
+ y_align: Clutter.ActorAlign.CENTER,
+ }),
+ reactive: true,
+ x_expand: true,
+ x_align: Clutter.ActorAlign.START,
+ y_align: Clutter.ActorAlign.CENTER,
+ });
+ inspectButton.connect('clicked', () => this.emit('inspect-actor'));
+
+ const box = new St.BoxLayout();
+ box.add_child(this._arrow);
+ box.add_child(label);
+ box.add_child(inspectButton);
+
+ super._init({
+ reactive: true,
+ track_hover: true,
+ toggle_mode: true,
+ style_class: 'actor-link',
+ child: box,
+ x_align: Clutter.ActorAlign.START,
+ });
+
+ this._actor = actor;
+ }
+
+ vfunc_clicked() {
+ this._arrow.ease({
+ rotation_angle_z: this.checked ? 90 : 0,
+ duration: 250,
+ });
+ }
+});
+
+var ActorTreeViewer = GObject.registerClass(
+class ActorTreeViewer extends St.BoxLayout {
+ _init(lookingGlass) {
+ super._init();
+
+ this._lookingGlass = lookingGlass;
+ this._actorData = new Map();
+ }
+
+ _showActorChildren(actor) {
+ const data = this._actorData.get(actor);
+ if (!data || data.visible)
+ return;
+
+ data.visible = true;
+ data.actorAddedId = actor.connect('actor-added', (container, child) => {
+ this._addActor(data.children, child);
+ });
+ data.actorRemovedId = actor.connect('actor-removed', (container, child) => {
+ this._removeActor(child);
+ });
+
+ for (let child of actor)
+ this._addActor(data.children, child);
+ }
+
+ _hideActorChildren(actor) {
+ const data = this._actorData.get(actor);
+ if (!data || !data.visible)
+ return;
+
+ for (let child of actor)
+ this._removeActor(child);
+
+ data.visible = false;
+ if (data.actorAddedId > 0) {
+ actor.disconnect(data.actorAddedId);
+ data.actorAddedId = 0;
+ }
+ if (data.actorRemovedId > 0) {
+ actor.disconnect(data.actorRemovedId);
+ data.actorRemovedId = 0;
+ }
+ data.children.remove_all_children();
+ }
+
+ _addActor(container, actor) {
+ if (this._actorData.has(actor))
+ return;
+
+ if (actor === this._lookingGlass)
+ return;
+
+ const button = new ActorLink(actor);
+ button.connect('notify::checked', () => {
+ this._lookingGlass.setBorderPaintTarget(actor);
+ if (button.checked)
+ this._showActorChildren(actor);
+ else
+ this._hideActorChildren(actor);
+ });
+ button.connect('inspect-actor', () => {
+ this._lookingGlass.inspectObject(actor, button);
+ });
+
+ const mainContainer = new St.BoxLayout({ vertical: true });
+ const childrenContainer = new St.BoxLayout({
+ vertical: true,
+ style: 'padding: 0 0 0 18px',
+ });
+
+ mainContainer.add_child(button);
+ mainContainer.add_child(childrenContainer);
+
+ this._actorData.set(actor, {
+ button,
+ container: mainContainer,
+ children: childrenContainer,
+ visible: false,
+ actorAddedId: 0,
+ actorRemovedId: 0,
+ actorDestroyedId: actor.connect('destroy', () => this._removeActor(actor)),
+ });
+
+ let belowChild = null;
+ const nextSibling = actor.get_next_sibling();
+ if (nextSibling && this._actorData.has(nextSibling))
+ belowChild = this._actorData.get(nextSibling).container;
+
+ container.insert_child_above(mainContainer, belowChild);
+ }
+
+ _removeActor(actor) {
+ const data = this._actorData.get(actor);
+ if (!data)
+ return;
+
+ for (let child of actor)
+ this._removeActor(child);
+
+ if (data.actorAddedId > 0) {
+ actor.disconnect(data.actorAddedId);
+ data.actorAddedId = 0;
+ }
+ if (data.actorRemovedId > 0) {
+ actor.disconnect(data.actorRemovedId);
+ data.actorRemovedId = 0;
+ }
+ if (data.actorDestroyedId > 0) {
+ actor.disconnect(data.actorDestroyedId);
+ data.actorDestroyedId = 0;
+ }
+ data.container.destroy();
+ this._actorData.delete(actor);
+ }
+
+ vfunc_map() {
+ super.vfunc_map();
+ this._addActor(this, global.stage);
+ }
+
+ vfunc_unmap() {
+ super.vfunc_unmap();
+ this._removeActor(global.stage);
+ }
+});
+
+var LookingGlass = GObject.registerClass(
+class LookingGlass extends St.BoxLayout {
+ _init() {
+ super._init({
+ name: 'LookingGlassDialog',
+ style_class: 'lg-dialog',
+ vertical: true,
+ visible: false,
+ reactive: true,
+ });
+
+ this._borderPaintTarget = null;
+ this._redBorderEffect = new RedBorderEffect();
+
+ this._open = false;
+
+ this._it = null;
+ this._offset = 0;
+
+ // Sort of magic, but...eh.
+ this._maxItems = 150;
+
+ this._interfaceSettings = new Gio.Settings({ schema_id: 'org.gnome.desktop.interface' });
+ this._interfaceSettings.connect('changed::monospace-font-name',
+ this._updateFont.bind(this));
+ this._updateFont();
+
+ // We want it to appear to slide out from underneath the panel
+ Main.uiGroup.add_actor(this);
+ Main.uiGroup.set_child_below_sibling(this,
+ Main.layoutManager.panelBox);
+ Main.layoutManager.panelBox.connect('notify::allocation',
+ this._queueResize.bind(this));
+ Main.layoutManager.keyboardBox.connect('notify::allocation',
+ this._queueResize.bind(this));
+
+ this._objInspector = new ObjInspector(this);
+ Main.uiGroup.add_actor(this._objInspector);
+ this._objInspector.hide();
+
+ let toolbar = new St.BoxLayout({ name: 'Toolbar' });
+ this.add_actor(toolbar);
+ let inspectIcon = new St.Icon({ icon_name: 'gtk-color-picker',
+ icon_size: 24 });
+ toolbar.add_actor(inspectIcon);
+ inspectIcon.reactive = true;
+ inspectIcon.connect('button-press-event', () => {
+ let inspector = new Inspector(this);
+ inspector.connect('target', (i, target, stageX, stageY) => {
+ this._pushResult('inspect(%d, %d)'.format(Math.round(stageX), Math.round(stageY)), target);
+ });
+ inspector.connect('closed', () => {
+ this.show();
+ global.stage.set_key_focus(this._entry);
+ });
+ this.hide();
+ return Clutter.EVENT_STOP;
+ });
+
+ let gcIcon = new St.Icon({ icon_name: 'user-trash-full',
+ icon_size: 24 });
+ toolbar.add_actor(gcIcon);
+ gcIcon.reactive = true;
+ gcIcon.connect('button-press-event', () => {
+ gcIcon.icon_name = 'user-trash';
+ System.gc();
+ this._timeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 500, () => {
+ gcIcon.icon_name = 'user-trash-full';
+ this._timeoutId = 0;
+ return GLib.SOURCE_REMOVE;
+ });
+ GLib.Source.set_name_by_id(this._timeoutId, '[gnome-shell] gcIcon.icon_name = \'user-trash-full\'');
+ return Clutter.EVENT_PROPAGATE;
+ });
+
+ let notebook = new Notebook();
+ this._notebook = notebook;
+ this.add_child(notebook);
+
+ let emptyBox = new St.Bin({ x_expand: true });
+ toolbar.add_child(emptyBox);
+ 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,
+ y_expand: true,
+ });
+ this._evalBox.add_child(this._resultsArea);
+
+ this._entryArea = new St.BoxLayout({
+ name: 'EntryArea',
+ y_align: Clutter.ActorAlign.END,
+ });
+ this._evalBox.add_actor(this._entryArea);
+
+ let label = new St.Label({ text: CHEVRON });
+ this._entryArea.add(label);
+
+ this._entry = new St.Entry({
+ can_focus: true,
+ x_expand: true,
+ });
+ ShellEntry.addContextMenu(this._entry);
+ this._entryArea.add_child(this._entry);
+
+ this._windowList = new WindowList(this);
+ notebook.appendPage('Windows', this._windowList);
+
+ this._extensions = new Extensions(this);
+ notebook.appendPage('Extensions', this._extensions);
+
+ this._actorTreeViewer = new ActorTreeViewer(this);
+ notebook.appendPage('Actors', this._actorTreeViewer);
+
+ this._entry.clutter_text.connect('activate', (o, _e) => {
+ // Hide any completions we are currently showing
+ this._hideCompletions();
+
+ let text = o.get_text();
+ // Ensure we don't get newlines in the command; the history file is
+ // newline-separated.
+ text = text.replace('\n', ' ');
+ // Strip leading and trailing whitespace
+ text = text.replace(/^\s+/g, '').replace(/\s+$/g, '');
+ if (text == '')
+ return true;
+ this._evaluate(text);
+ return true;
+ });
+
+ this._history = new History.HistoryManager({ gsettingsKey: HISTORY_KEY,
+ entry: this._entry.clutter_text });
+
+ this._autoComplete = new AutoComplete(this._entry);
+ this._autoComplete.connect('suggest', (a, e) => {
+ this._showCompletions(e.completions);
+ });
+ // If a completion is completed unambiguously, the currently-displayed completion
+ // suggestions become irrelevant.
+ this._autoComplete.connect('completion', (a, e) => {
+ if (e.type == 'whole-word')
+ this._hideCompletions();
+ });
+
+ this._resize();
+ }
+
+ _updateFont() {
+ let fontName = this._interfaceSettings.get_string('monospace-font-name');
+ let fontDesc = Pango.FontDescription.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.
+ let size = fontDesc.get_size() / 1024.;
+ let unit = fontDesc.get_size_is_absolute() ? 'px' : 'pt';
+ this.style = 'font-size: %d%s; font-family: "%s";'.format(
+ size, unit, fontDesc.get_family());
+ }
+
+ setBorderPaintTarget(obj) {
+ if (this._borderPaintTarget != null)
+ this._borderPaintTarget.remove_effect(this._redBorderEffect);
+ this._borderPaintTarget = obj;
+ if (this._borderPaintTarget != null)
+ this._borderPaintTarget.add_effect(this._redBorderEffect);
+ }
+
+ _pushResult(command, obj) {
+ let index = this._resultsArea.get_n_children() + this._offset;
+ let result = new Result(this, CHEVRON + command, obj, index);
+ this._resultsArea.add(result);
+ if (obj instanceof Clutter.Actor)
+ this.setBorderPaintTarget(obj);
+
+ if (this._resultsArea.get_n_children() > this._maxItems) {
+ this._resultsArea.get_first_child().destroy();
+ this._offset++;
+ }
+ this._it = obj;
+
+ // Scroll to bottom
+ this._notebook.scrollToBottom(0);
+ }
+
+ _showCompletions(completions) {
+ if (!this._completionActor) {
+ this._completionActor = new St.Label({ name: 'LookingGlassAutoCompletionText', style_class: 'lg-completions-text' });
+ this._completionActor.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
+ this._completionActor.clutter_text.line_wrap = true;
+ this._evalBox.insert_child_below(this._completionActor, this._entryArea);
+ }
+
+ this._completionActor.set_text(completions.join(', '));
+
+ // Setting the height to -1 allows us to get its actual preferred height rather than
+ // whatever was last set when animating
+ this._completionActor.set_height(-1);
+ let [, naturalHeight] = this._completionActor.get_preferred_height(this._resultsArea.get_width());
+
+ // Don't reanimate if we are already visible
+ if (this._completionActor.visible) {
+ this._completionActor.height = naturalHeight;
+ } else {
+ let settings = St.Settings.get();
+ let duration = AUTO_COMPLETE_SHOW_COMPLETION_ANIMATION_DURATION / settings.slow_down_factor;
+ this._completionActor.show();
+ this._completionActor.remove_all_transitions();
+ this._completionActor.ease({
+ height: naturalHeight,
+ opacity: 255,
+ duration,
+ mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+ });
+ }
+ }
+
+ _hideCompletions() {
+ if (this._completionActor) {
+ let settings = St.Settings.get();
+ let duration = AUTO_COMPLETE_SHOW_COMPLETION_ANIMATION_DURATION / settings.slow_down_factor;
+ this._completionActor.remove_all_transitions();
+ this._completionActor.ease({
+ height: 0,
+ opacity: 0,
+ duration,
+ mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+ onComplete: () => {
+ this._completionActor.hide();
+ },
+ });
+ }
+ }
+
+ _evaluate(command) {
+ this._history.addItem(command);
+
+ let lines = command.split(';');
+ lines.push('return %s'.format(lines.pop()));
+
+ let fullCmd = commandHeader + lines.join(';');
+
+ let resultObj;
+ try {
+ resultObj = Function(fullCmd)();
+ } catch (e) {
+ resultObj = '<exception %s>'.format(e.toString());
+ }
+
+ this._pushResult(command, resultObj);
+ this._entry.text = '';
+ }
+
+ inspect(x, y) {
+ return global.stage.get_actor_at_pos(Clutter.PickMode.REACTIVE, x, y);
+ }
+
+ getIt() {
+ return this._it;
+ }
+
+ getResult(idx) {
+ try {
+ return this._resultsArea.get_child_at_index(idx - this._offset).o;
+ } catch (e) {
+ throw new Error('Unknown result at index %d'.format(idx));
+ }
+ }
+
+ toggle() {
+ if (this._open)
+ this.close();
+ else
+ this.open();
+ }
+
+ _queueResize() {
+ Meta.later_add(Meta.LaterType.BEFORE_REDRAW, () => {
+ this._resize();
+ return GLib.SOURCE_REMOVE;
+ });
+ }
+
+ _resize() {
+ let primary = Main.layoutManager.primaryMonitor;
+ let myWidth = primary.width * 0.7;
+ let availableHeight = primary.height - Main.layoutManager.keyboardBox.height;
+ let myHeight = Math.min(primary.height * 0.7, availableHeight * 0.9);
+ this.x = primary.x + (primary.width - myWidth) / 2;
+ this._hiddenY = primary.y + Main.layoutManager.panelBox.height - myHeight;
+ this._targetY = this._hiddenY + myHeight;
+ this.y = this._hiddenY;
+ this.width = myWidth;
+ this.height = myHeight;
+ this._objInspector.set_size(Math.floor(myWidth * 0.8), Math.floor(myHeight * 0.8));
+ this._objInspector.set_position(this.x + Math.floor(myWidth * 0.1),
+ this._targetY + Math.floor(myHeight * 0.1));
+ }
+
+ insertObject(obj) {
+ this._pushResult('<insert>', obj);
+ }
+
+ inspectObject(obj, sourceActor) {
+ this._objInspector.open(sourceActor);
+ this._objInspector.selectObject(obj);
+ }
+
+ // Handle key events which are relevant for all tabs of the LookingGlass
+ vfunc_key_press_event(keyPressEvent) {
+ let symbol = keyPressEvent.keyval;
+ if (symbol == Clutter.KEY_Escape) {
+ if (this._objInspector.visible)
+ this._objInspector.close();
+ else
+ this.close();
+ return Clutter.EVENT_STOP;
+ }
+ // Ctrl+PgUp and Ctrl+PgDown switches tabs in the notebook view
+ if (keyPressEvent.modifier_state & Clutter.ModifierType.CONTROL_MASK) {
+ if (symbol == Clutter.KEY_Page_Up)
+ this._notebook.prevTab();
+ else if (symbol == Clutter.KEY_Page_Down)
+ this._notebook.nextTab();
+ }
+ return super.vfunc_key_press_event(keyPressEvent);
+ }
+
+ open() {
+ if (this._open)
+ return;
+
+ if (!Main.pushModal(this._entry, { actionMode: Shell.ActionMode.LOOKING_GLASS }))
+ return;
+
+ this._notebook.selectIndex(0);
+ this.show();
+ this._open = true;
+ this._history.lastItem();
+
+ this.remove_all_transitions();
+
+ // We inverse compensate for the slow-down so you can change the factor
+ // through LookingGlass without long waits.
+ let duration = LG_ANIMATION_TIME / St.Settings.get().slow_down_factor;
+ this.ease({
+ y: this._targetY,
+ duration,
+ mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+ });
+
+ this._windowList.update();
+ }
+
+ close() {
+ if (!this._open)
+ return;
+
+ this._objInspector.hide();
+
+ this._open = false;
+ this.remove_all_transitions();
+
+ this.setBorderPaintTarget(null);
+
+ Main.popModal(this._entry);
+
+ let settings = St.Settings.get();
+ let duration = Math.min(LG_ANIMATION_TIME / settings.slow_down_factor,
+ LG_ANIMATION_TIME);
+ this.ease({
+ y: this._hiddenY,
+ duration,
+ mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+ onComplete: () => this.hide(),
+ });
+ }
+
+ get isOpen() {
+ return this._open;
+ }
+});