Skip to content
Matthias Seiffert edited this page Sep 23, 2023 · 1 revision

This page was previously available as `notes.org` in the repository but was removed there. Some information is probably outdated and this page is only kept for reference.

Mutter state change signal summary (WIP)

stuck -> unstuck: stuck property change remove from all workspaces add on target workspace

unstuck -> stuck: stuck property change [not-verified] remove from current workspace add on all workspaces

window moved from workspace A -> workspace B: remove from A add on B

window A close: next window receive focus remove A from workspace window-left-monitor (actor is null)

window is created: window-added (actor is null) window-entered-monitor (actor is null) window-created window-focus

window monitor changed: (ws-only-primary) [order not-verified] window-left-monitor [not-verified] window-entered-monitor unstuck -> stuck

window monitor changed: (ws-spans-monitor) window-left-monitor [not-verified] window-entered-monitor

Monitor changes (ws-only-primary) primary -> secondary <=> unstuck -> stuck seconday -> primary <=> stuck -> unstuck

window_added always follows a window_removed except when a window is closed.

window monitor membership determined by majority area (ish)

Mutter signal order

When window A is closed

  1. The next window, B, receives ‘focus’ (but the actor of A seems to be gone?)
  2. Workspace receives ‘window-removed’. (‘A’ seems to have been stripped of signal handlers)
  3. on screen ‘window-left-monitor’, actor isn’t available

When window A is created

  1. on workspace “window-added” is run, actor isn’t available
  2. on screen “window-entered-monitor”, actor isn’t available
  3. on display “window-created” is run, actor is available
  4. focus is run if the new window should be focused

Toggle “Always on visible workspace” (scratch windows)

  • window-removed on workspace of window
  • window-added on all workspaces

Keybinding system

`Main.wm.addKeybinding` is used to register a named keybindable action and it’s handler. An numeric id is returned. (this is a thin wrapper around `MetaDisplay.add_keybinding`)

The action should have an entry in the schema underlying the `GSettings` object supplied to `addKeybinding`. This is where the actual keybinding is specified. Multiple bindings can be specified.

<key type="as" name="toggle-scratch-layer">
  <default><![CDATA[['e']]]></default>
  <summary>Toggles the floating scratch layer</summary>
</key>

To change a keybinding simply change this value in the gsetting: (mutter will pick up the change automatically.

mySettings.set_strv("toggle-scratch-layer", ["<Super>s"]);

Action names are global. (note that the mutter documentation mostly refers to actions as keybindings)

`Meta.keybindings_set_custom_handler` is used to change a action handler. Despite what the documentation suggests this works for non-builtin actions too.

If the action is a mutter built-in (one of `Meta.KeyBindingAction.*`, setting the custom handler to `null` restores the default handler.

Action handlers fire on key-down.

Mutter itself does not support key-release sensitive bindings, but it’s possible to create a Clutter actor in response to a key-down binding, which temporarily take over the keyboard. Clutter can listen for key-up/key-release events.

`MetaDisplay.get_keybinding_action` looks up the action id bound to a specific modifer+keycode. This is mostly useful when handling key events within clutter.

The id -> action-name mapping is not(?) exposed. For builtin actions `Meta.prefs_get_keybinding_action(actionName)` will give the id of actionName.

It’s not possible to look up the handler of a action…(?)

A slightly annoying detail about how all this works is that you normally give the handler before you know the action-id. So if the handler need to know the action-id (eg. if it use clutter to implement a mini-mode and want to respond to the same key that triggered the mode) you either have to store a name->id map, or re-assign the handler afterward.

The Keybinding object which is supplied to keyhandler doesn’t seem to expose the key used to trigger the action either?

Modifier-only bindings

Simply use the keysym name as if the modifier was a regular key. Don’t use angle brackets - those are used for **modifiers**.

settings.set_strv("my-action", ["Super_L"])

Bind keys without using actions from a schema

From: https://stackoverflow.com/a/42466781/1517969

Meta = imports.gi.Meta;
Main = imports.ui.main;
Shell = imports.gi.Shell;

let action = global.display.grab_accelerator("<super>u");
let name = Meta.external_binding_name_for_action(action);
Main.wm.allowKeybinding(name, Shell.ActionMode.ALL);
global.display.connect(
    'accelerator-activated',
    function(display, action, deviceId, timestamp){
        print('Accelerator Activated: [display={}, action={}, deviceId={}, timestamp={}]',
            display, action, deviceId, timestamp)
    })

Lookup an keybinding action by a accelerator string

global.display.get_keybinding_action(keycode, mask) is simple to use in clutter event handlers since the keycode and mask is readily available. Outside of clutter is harder:

function devirtualizeMask(gdkVirtualMask) {
    const keymap = Gdk.Keymap.get_default();
    let [success, rawMask] = keymap.map_virtual_modifiers(gdkVirtualMask);
    if (!success)
        throw new Error("Couldn't devirtualize mask " + gdkVirtualMask);
    return rawMask;
}

function getBoundActionId(keystr) {
    let [dontcare, keycodes, mask] =
        Gtk.accelerator_parse_with_keycode(keystr);
    if(keycodes.length > 1) {
        throw new Error("Multiple keycodes " + keycodes + " " + keystr);
    }
    const rawMask = devirtualizeMask(mask);
    return global.display.get_keybinding_action(keycodes[0], rawMask);
}

GJS

import system / module system

`imports.NAME` reflects the directories and javascript files present in `imports.searchPath`. To add a path, simply do `imports.searchPath.push(PATH)`

Environment variable `GJS_PATH` initializes `imports.searchPath`.

The special property `imports.gi` expose gobject-introspectable libraries. Another search path controls which libraries are available: `imports.gi.GIRepository.Repository.get_search_path()` initialized by environment variable `GI_TYPELIB_PATH` (`Repository` is the global instance of GIRepository)

Reloading modules

Modules **can’t** be reloaded, but writing to `imports.myModule.myVariable` works. Eg.

// myModule
var foo = 1;
function printFoo() {
  print(foo);
}

After `imports.myModule.foo = 2`, `printFoo` will print 2. All users of the module share the same module object so they will also see the updated variable.

Refering to the current module

Refering to the module being loaded works:

// myModule.js
var currentModule = imports.myModule;
var foo = 1;
currentModule.foo = 2;
print(foo); // prints 2

I don’t know if it’s possible without knowing the module name.

Creating a standalone importer

This trick is due to gnome-shell

function createImporter (directoryPath) {
    const Gio = imports.gi.Gio;
    let oldSearchPath = imports.searchPath.slice();  // make a copy
    let directory = Gio.file_new_for_path(directoryPath);
    try {
        imports.searchPath = [ directory.get_parent().get_path() ];
        // importing a "subdir" creates a new importer object that doesn't
        // affect the global one
        return imports[directory.get_basename()];
    } finally {
        imports.searchPath = oldSearchPath;
    }
}

Debugging

Get a stacktrace

`(new Error()).stack`

GObject

The `notify` signal is emited on changes to all GObject properties. Listen to `notify::propery-name` to only receive for changes to ` property-name`. (Reference)

Gnome-shell scene graph and GUI system

NB: some details might differ with the wayland backend.

Gnome shell use Clutter to mange all visible components including the window textures. Basic GUI components are provided by the St (built on top of clutter).

Low level window management and input handling happens through mutter/meta. Gnome-shell is technically a mutter plugin.

Input handling

(Also see Keybinding system)

Input is normally fully handled by X11. This means that even though gnome-shell use clutter (which have input mechanisms) inputs does not normally go through clutter.

Ie. making an actor `reactive` is not enough to capture input reliable.

Input handling can be directed through clutter by using:

Main.layoutManager._trackActor(actor)

This informs mutter[1] that mouse input in the actor’s region should be sent through clutter.

Some higher-level interfaces:

Main.pushModal(actor)

The clutter actor will receives all input until `Main.popModal` is called.

Main.layoutManager.trackChrome(actor)

NB: It does not seem to be possible to propagate input captured by a tracked actor to a window actor below.

NB! When a “tracked” actor is stacked below a window actor it will still prevent the window actor from receiving input!

[1] By using `meta_set_stage_input_region` through `global.set_stage_input_region`

`MetaWindow` and `MetaWindowActor`

WIP: display_rect vs frame_rect vs actor.width. Gotchas when placing MetaWindowActors in containers, etc.

Warning: This is a somewhat confusing part of gnome-shell/mutter.

A window is represented by two objects: a `MetaWindow` representing the underlying windowing system object (eg. a X11 window) and a `MetaWindowActor` which basically is the window texture/visible part.

Both of these objects have a geometry (size and position). The meta window geometry determines the input region, while the actor geometry determines the texture. Normally these geometries are kept in sync so the visible and input regions corresponds. It is however possible for these to drift: The thumb of rule is that changes to the meta window geometry is propagated to the actor, but not the other way.

The coordinate system used is thankfully shared :)

The size of the window actor is slightly bigger than the meta window since the actor includes border decorations and window-resize region. The size difference varies with the toolkit used to create the window.

Basic operations

To get the window actor of a meta window: `metaWindow.get_compositor_private()`

To get the meta window of a window actor: `windowActor.meta_window`

The window actor geometry: `windowActor.size, windowActor.position` or `metaWindow.get_buffer_rect`

The meta window geometry: `metaWindow.get_frame_rect()`

Changing the geometry of a window: `metaWindow.move_frame` or `metaWindow.move_resize_frame`

Stacking/”z-index”

The “z-index” in clutter is controlled by the actors position in the scene graph. Ie. the actors are drawn in a depth first manner. So the last child of a parent will be drawn on top of all the other children, and so on.

To my knowledge there is no way to make a actor “break out” of its parent. If sibling A is drawn below another actor X, sibling B will also be drawn below X.

NB: `ClutterActor.z-position` **don’t** control the z-index. It is used to control the perspective of the actors (most relevant for rotated actors).

A complication when using non-window actors inside `global.window_group` is that mutter keep restacking the window actors in a way that destroys the non-window actors z-index. Listening on the `restacked` signal of `global.screen` (`MetaScreen`) and restack the non-window actors in the handler is a workaround that seems to work.

Gotchas

Building `StWidget` detached from the stage are prone to result in the following warning:

st_widget_get_theme_node called on the widget [0x... St...] which is not in the stage.

This is because a lot of actor properties depend on the style of the actor and that can depend on the ancestors of the actor. (`.parent .child { border: 2px; }`)

So any code that try to access eg. height/width (unless these have been explicitly set beforehand) requires that the full style info is present.

Extension system

All extension objects are available using `imports.misc.extensionUtils.extensions[extensionUiid];` where the key is the uuid from the metadata.json file.

The current extension object is usually found like this:

const ExtensionUtils = imports.misc.extensionUtils;
const Me = ExtensionUtils.getCurrentExtension();

The absolute path of the an extension: `Extension.path`

Misc HowTo

Defer an execution of a function

~Meta.later_add~ (assoc: imports.mainloop.timeout_add)

Increase mutter log verbosity

Meta.add_verbose_topic(Meta.DebugTopic.FOCUS) Meta.remove_verbose_topic(Meta.DebugTopic.FOCUS)

Profiling

Show clutter FPS

Clutter prints the FPS at regular intervals if CLUTTER_SHOW_FPS is set when gnome-shell starts. Where the output ends up depends on how gnome-shell was started. On my system it ends up in the system journal (journalctl)

To turn on off without disrupting flow too much use GLib.setenv("CLUTTER_SHOW_FPS", "1", true) and restart gnome-shell.

Invariants

Focus and active workspace

It’s not possible the have a focused window which doesn’t belong to the active workspace global.display.focus_window.workspace === workspaceManger.get_active_workspace()

Clutter animation

time: 0 does not result in an instant animation. A default duration seems to be selected instead.