8d1aa7bed8
Several extensions use this class to let the user pick application windows. Exporting this class will make it easier for these extensions to be ported to GNOME 45. Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/2898>
1685 lines
52 KiB
JavaScript
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);
|
|
}
|
|
});
|
|
|
|
export 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;
|
|
}
|
|
});
|