From 393c6c68056863cb672c781d1d99d081daefcc71 Mon Sep 17 00:00:00 2001 From: Georges Basile Stavracas Neto Date: Sun, 31 May 2020 17:31:48 -0300 Subject: [PATCH] lookingGlass: Add actor tree inspector Being able to visualize the actor tree is a handy feature to have, specially when debugging the hierarchy. Add a new "Actors" tab to the Looking Glass with the actor tree inspector. The tree is cleared on unmap to not get heavy on the number of actors. https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/1292 --- .../widgets/_looking-glass.scss | 7 + js/ui/lookingGlass.js | 188 ++++++++++++++++++ 2 files changed, 195 insertions(+) diff --git a/data/theme/gnome-shell-sass/widgets/_looking-glass.scss b/data/theme/gnome-shell-sass/widgets/_looking-glass.scss index 9c38e6360..006c2ef92 100644 --- a/data/theme/gnome-shell-sass/widgets/_looking-glass.scss +++ b/data/theme/gnome-shell-sass/widgets/_looking-glass.scss @@ -1,5 +1,7 @@ /* Looking Glass */ +$text_fg_color: #ccc; + // Dialog #LookingGlassDialog { background-color: $osd_bg_color; @@ -52,6 +54,11 @@ &:hover { color: lighten($link_color, 10%); } &:active { color: darken($link_color, 10%); } } + .actor-link { + color: $text_fg_color; + &:hover { color: lighten($text_fg_color, 20%); } + &:active { color: darken($text_fg_color, 20%); } + } } .lg-completions-text { diff --git a/js/ui/lookingGlass.js b/js/ui/lookingGlass.js index a5d762987..5abee0f75 100644 --- a/js/ui/lookingGlass.js +++ b/js/ui/lookingGlass.js @@ -802,6 +802,191 @@ var Extensions = GObject.registerClass({ } }); + +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() { @@ -917,6 +1102,9 @@ class LookingGlass extends St.BoxLayout { 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();