gnome-shell/js/ui/lookingGlass.js
Florian Müllner a42f7c2384 cleanup: Use type-safe comparisons
We have been using type-safe comparisons in new code for quite a while
now, however old code has only been adapted slowly.

Change all the remaining bits to get rid of another legacy style
difference.

Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/2866>
2023-08-09 15:10:38 +00:00

1685 lines
52 KiB
JavaScript

// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
import Clutter from 'gi://Clutter';
import Cogl from 'gi://Cogl';
import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import Graphene from 'gi://Graphene';
import Meta from 'gi://Meta';
import Pango from 'gi://Pango';
import Shell from 'gi://Shell';
import St from 'gi://St';
import * as Signals from '../misc/signals.js';
import System from 'system';
import * as History from '../misc/history.js';
import {ExtensionState} from '../misc/extensionUtils.js';
import * as PopupMenu from './popupMenu.js';
import * as ShellEntry from './shellEntry.js';
import * as Main from './main.js';
import * as JsParse from '../misc/jsParse.js';
const CHEVRON = '>>> ';
/* Imports...feel free to add here as needed */
const commandHeader = `
const {Clutter, Gio, GLib, GObject, Meta, Shell, St} = imports.gi;
const Main = await import('resource:///org/gnome/shell/ui/main.js');
/* 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 AsyncFunction = async function () {}.constructor;
const HISTORY_KEY = 'looking-glass-history';
// Time between tabs for them to count as a double-tab event
const AUTO_COMPLETE_DOUBLE_TAB_DELAY = 500;
const AUTO_COMPLETE_SHOW_COMPLETION_ANIMATION_DURATION = 200;
const AUTO_COMPLETE_GLOBAL_KEYWORDS = _getAutoCompleteGlobalKeywords();
const LG_ANIMATION_TIME = 500;
const CLUTTER_DEBUG_FLAG_CATEGORIES = new Map([
// Paint debugging can easily result in a non-responsive session
['DebugFlag', {argPos: 0, exclude: ['PAINT']}],
['DrawDebugFlag', {argPos: 1, exclude: []}],
// Exluded due to the only current option likely to result in shooting ones
// foot
// ['PickDebugFlag', { argPos: 2, exclude: [] }],
]);
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);
}
class AutoComplete extends Signals.EventEmitter {
constructor(entry) {
super();
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});
}
}
async _handleCompletions(text, time) {
const [completions, attrHead] =
await JsParse.getCompletions(text, commandHeader, AUTO_COMPLETE_GLOBAL_KEYWORDS);
const tabType = (time - this._lastTabTime) < AUTO_COMPLETE_DOUBLE_TAB_DELAY
? 'double' : 'single';
this._processCompletionRequest({
tabType,
completions,
attrHead,
});
this._lastTabTime = time;
}
_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)
this._handleCompletions(text, event.get_time()).catch(logError);
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);
}
}
const 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) {
const 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);
const 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 && o.toString === undefined) {
// eeks, something unprintable. we'll have to guess, probably a module
return typeof o === 'object' && !(o instanceof Object)
? '<module>'
: '<unknown>';
} else {
return `${o}`;
}
}
const 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);
}
});
const 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(${index}) = `});
resultTxt.clutter_text.ellipsize = Pango.EllipsizeMode.END;
box.add(resultTxt);
let objLink = new ObjLink(this._lookingGlass, o);
box.add(objLink);
}
});
const 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: ${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();
}
});
const 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: ${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',
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: `${propName}: `}));
box.add(link);
this._container.add_actor(box);
}
}
}
open(sourceActor) {
if (this._open)
return;
const grab = Main.pushModal(this, {actionMode: Shell.ActionMode.LOOKING_GLASS});
if (grab.get_seat_state() !== Clutter.GrabState.ALL) {
Main.popModal(grab);
return;
}
this._grab = grab;
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;
Main.popModal(this._grab);
this._grab = null;
this._open = false;
this.hide();
this._previousObj = null;
this._obj = null;
}
vfunc_key_press_event(event) {
const symbol = event.get_key_symbol();
if (symbol === Clutter.KEY_Escape) {
this.close();
return Clutter.EVENT_STOP;
}
return super.vfunc_key_press_event(event);
}
_onInsert() {
let obj = this._obj;
this.close();
this._lookingGlass.insertObject(obj);
}
_onBack() {
this.selectObject(this._previousObj, true);
}
});
const RedBorderEffect = GObject.registerClass(
class RedBorderEffect extends Clutter.Effect {
_init() {
super._init();
this._pipeline = null;
}
vfunc_paint_node(node, paintContext) {
let actor = this.get_actor();
const actorNode = new Clutter.ActorNode(actor, -1);
node.add_child(actorNode);
if (!this._pipeline) {
const framebuffer = paintContext.get_framebuffer();
const coglContext = framebuffer.get_context();
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;
const pipelineNode = new Clutter.PipelineNode(this._pipeline);
pipelineNode.set_name('Red Border');
node.add_child(pipelineNode);
const box = new Clutter.ActorBox();
// clockwise order
box.set_origin(0, 0);
box.set_size(alloc.get_width(), width);
pipelineNode.add_rectangle(box);
box.set_origin(alloc.get_width() - width, width);
box.set_size(width, alloc.get_height() - width);
pipelineNode.add_rectangle(box);
box.set_origin(0, alloc.get_height() - width);
box.set_size(alloc.get_width() - width, width);
pipelineNode.add_rectangle(box);
box.set_origin(0, width);
box.set_size(width, alloc.get_height() - width * 2);
pipelineNode.add_rectangle(box);
}
});
const 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);
const 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));
this._grab = global.stage.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() {
if (this._grab) {
this._grab.dismiss();
this._grab = null;
}
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: ${stageX} y: ${stageY}]`;
this._displayText.text = '';
this._displayText.text = `${position} ${this._target}`;
this._lookingGlass.setBorderPaintTarget(this._target);
}
});
const 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++;
const {name} = extension.metadata;
const pos = [...this._extensionsList].findIndex(
dsp => dsp._extension.metadata.name.localeCompare(name) > 0);
this._extensionsList.insert_child_at_index(extensionDisplay, pos);
}
_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');
case ExtensionState.DISABLING:
return _('Disabling');
case ExtensionState.ENABLING:
return _('Enabling');
}
return 'Unknown'; // Not translated, shouldn't appear
}
_createExtensionDisplay(extension) {
let box = new St.BoxLayout({style_class: 'lg-extension', vertical: true});
box._extension = extension;
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);
const state = new St.Label({
style_class: 'lg-extension-state',
text: this._stateToString(extension.state),
});
metaBox.add(state);
const 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) {
const 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);
}
const 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;
}
});
const 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({
icon_name: 'insert-object-symbolic',
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,
});
}
});
const 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);
}
});
const DebugFlag = GObject.registerClass({
GTypeFlags: GObject.TypeFlags.ABSTRACT,
}, class DebugFlag extends St.Button {
_init(label) {
const box = new St.BoxLayout();
const flagLabel = new St.Label({
text: label,
x_expand: true,
x_align: Clutter.ActorAlign.START,
y_align: Clutter.ActorAlign.CENTER,
});
box.add_child(flagLabel);
this._flagSwitch = new PopupMenu.Switch(false);
this._stateHandler = this._flagSwitch.connect('notify::state', () => {
if (this._flagSwitch.state)
this._enable();
else
this._disable();
});
// Update state whenever the switch is mapped, because most debug flags
// don't have a way of notifying us of changes.
this._flagSwitch.connect('notify::mapped', () => {
if (!this._flagSwitch.is_mapped())
return;
const state = this._isEnabled();
if (state === this._flagSwitch.state)
return;
this._flagSwitch.block_signal_handler(this._stateHandler);
this._flagSwitch.state = state;
this._flagSwitch.unblock_signal_handler(this._stateHandler);
});
box.add_child(this._flagSwitch);
super._init({
style_class: 'lg-debug-flag-button',
can_focus: true,
toggleMode: true,
child: box,
label_actor: flagLabel,
y_align: Clutter.ActorAlign.CENTER,
});
this.connect('clicked', () => this._flagSwitch.toggle());
}
_isEnabled() {
throw new Error('Method not implemented');
}
_enable() {
throw new Error('Method not implemented');
}
_disable() {
throw new Error('Method not implemented');
}
});
const ClutterDebugFlag = GObject.registerClass(
class ClutterDebugFlag extends DebugFlag {
_init(categoryName, flagName) {
super._init(flagName);
this._argPos = CLUTTER_DEBUG_FLAG_CATEGORIES.get(categoryName).argPos;
this._enumValue = Clutter[categoryName][flagName];
}
_isEnabled() {
const enabledFlags = Meta.get_clutter_debug_flags();
return !!(enabledFlags[this._argPos] & this._enumValue);
}
_getArgs() {
const args = [0, 0, 0];
args[this._argPos] = this._enumValue;
return args;
}
_enable() {
Meta.add_clutter_debug_flags(...this._getArgs());
}
_disable() {
Meta.remove_clutter_debug_flags(...this._getArgs());
}
});
const MutterPaintDebugFlag = GObject.registerClass(
class MutterPaintDebugFlag extends DebugFlag {
_init(flagName) {
super._init(flagName);
this._enumValue = Meta.DebugPaintFlag[flagName];
}
_isEnabled() {
return !!(Meta.get_debug_paint_flags() & this._enumValue);
}
_enable() {
Meta.add_debug_paint_flag(this._enumValue);
}
_disable() {
Meta.remove_debug_paint_flag(this._enumValue);
}
});
const MutterTopicDebugFlag = GObject.registerClass(
class MutterTopicDebugFlag extends DebugFlag {
_init(flagName) {
super._init(flagName);
this._enumValue = Meta.DebugTopic[flagName];
}
_isEnabled() {
return Meta.is_topic_enabled(this._enumValue);
}
_enable() {
Meta.add_verbose_topic(this._enumValue);
}
_disable() {
Meta.remove_verbose_topic(this._enumValue);
}
});
const UnsafeModeDebugFlag = GObject.registerClass(
class UnsafeModeDebugFlag extends DebugFlag {
_init() {
super._init('unsafe-mode');
}
_isEnabled() {
return global.context.unsafe_mode;
}
_enable() {
global.context.unsafe_mode = true;
}
_disable() {
global.context.unsafe_mode = false;
}
});
const DebugFlags = GObject.registerClass(
class DebugFlags extends St.BoxLayout {
_init() {
super._init({
name: 'lookingGlassDebugFlags',
vertical: true,
x_align: Clutter.ActorAlign.CENTER,
});
// Clutter debug flags
for (const [categoryName, props] of CLUTTER_DEBUG_FLAG_CATEGORIES.entries()) {
this._addHeader(`Clutter${categoryName}`);
for (const flagName of this._getFlagNames(Clutter[categoryName])) {
if (props.exclude.includes(flagName))
continue;
this.add_child(new ClutterDebugFlag(categoryName, flagName));
}
}
// Meta paint flags
this._addHeader('MetaDebugPaintFlag');
for (const flagName of this._getFlagNames(Meta.DebugPaintFlag))
this.add_child(new MutterPaintDebugFlag(flagName));
// Meta debug topics
this._addHeader('MetaDebugTopic');
for (const flagName of this._getFlagNames(Meta.DebugTopic))
this.add_child(new MutterTopicDebugFlag(flagName));
// MetaContext::unsafe-mode
this._addHeader('MetaContext');
this.add_child(new UnsafeModeDebugFlag());
}
_addHeader(title) {
const header = new St.Label({
text: title,
style_class: 'lg-debug-flags-header',
x_align: Clutter.ActorAlign.START,
});
this.add_child(header);
}
*_getFlagNames(enumObject) {
for (const flagName of Object.getOwnPropertyNames(enumObject)) {
if (typeof enumObject[flagName] !== 'number')
continue;
if (enumObject[flagName] <= 0)
continue;
yield flagName;
}
}
});
export const 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);
const inspectButton = new St.Button({
style_class: 'lg-toolbar-button',
icon_name: 'find-location-symbolic',
});
toolbar.add_actor(inspectButton);
inspectButton.connect('clicked', () => {
let inspector = new Inspector(this);
inspector.connect('target', (i, target, stageX, stageY) => {
this._pushResult(`inspect(${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;
});
const gcButton = new St.Button({
style_class: 'lg-toolbar-button',
icon_name: 'user-trash-full-symbolic',
});
toolbar.add_actor(gcButton);
gcButton.connect('clicked', () => {
gcButton.child.icon_name = 'user-trash-symbolic';
System.gc();
this._timeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 500, () => {
gcButton.child.icon_name = 'user-trash-full-symbolic';
this._timeoutId = 0;
return GLib.SOURCE_REMOVE;
});
GLib.Source.set_name_by_id(
this._timeoutId,
'[gnome-shell] gcButton.child.icon_name = \'user-trash-full-symbolic\''
);
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._debugFlags = new DebugFlags();
notebook.appendPage('Flags', this._debugFlags);
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', ' ');
this._evaluate(text).catch(logError);
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();
}
vfunc_captured_event(event) {
if (Main.keyboard.maybeHandleEvent(event))
return Clutter.EVENT_STOP;
return Clutter.EVENT_PROPAGATE;
}
_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: ${size}${unit};
font-family: "${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();
},
});
}
}
async _evaluate(command) {
command = this._history.addItem(command); // trims command
if (!command)
return;
let lines = command.split(';');
lines.push(`return ${lines.pop()}`);
let fullCmd = commandHeader + lines.join(';');
let resultObj;
try {
resultObj = await AsyncFunction(fullCmd)();
} catch (e) {
resultObj = `<exception ${e}>`;
}
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 ${idx}`);
}
}
toggle() {
if (this._open)
this.close();
else
this.open();
}
_queueResize() {
const laters = global.compositor.get_laters();
laters.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(event) {
let symbol = event.get_key_symbol();
if (symbol === Clutter.KEY_Escape) {
this.close();
return Clutter.EVENT_STOP;
}
// Ctrl+PgUp and Ctrl+PgDown switches tabs in the notebook view
if (event.get_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(event);
}
open() {
if (this._open)
return;
let grab = Main.pushModal(this, {actionMode: Shell.ActionMode.LOOKING_GLASS});
if (grab.get_seat_state() !== Clutter.GrabState.ALL) {
Main.popModal(grab);
return;
}
this._grab = grab;
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();
this._entry.grab_key_focus();
}
close() {
if (!this._open)
return;
this._objInspector.hide();
this._open = false;
this.remove_all_transitions();
this.setBorderPaintTarget(null);
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: () => {
Main.popModal(this._grab);
this._grab = null;
this.hide();
},
});
}
get isOpen() {
return this._open;
}
});