13

What is the simplest way to (globally) bind a key combination (e.g. <Super>+A) to a function in a gnome shell extension?

Inspecting a couple of extensions, I ran into the following code:

global.display.add_keybinding('random-name',
                              new Gio.Settings({schema: 'org.gnome.shell.keybindings'}),
                              Meta.KeyBindingFlags.NONE,
                              function() { /* ... some code */ });

I understand that the key combination is specified by the schema parameter, and that it's possible to create an XML file describing the combination. Is there a simpler way to do this?

user1655742
  • 131
  • 1
  • 3
  • 8
    I am trying to find a solution to this exact problem, but documentation is lacking, or at least difficult to find. I don't understand why would this question be off topic. It's a direct question addressing a well defined problem concerning a programming API. Please re-open. Stackoverflow is the place to ask this question. The snippet in the question is clearly to be used in an applet. It's _not_ meant to be used to tweak gnome manually, there are other ways to do that. This question has no place in superuser. – Pico Jul 10 '13 at 17:19
  • 1
    agreed. what does coding something has to do with super user?! Sadly this is the top result for anyone looking on how to consume keystrokes on a gnome shell extension. – gcb Feb 15 '15 at 00:48

3 Answers3

5

The question is old, but I just implemented that for Gnome Shell 40. So here is how I did it.

The key is defined in your normal schema file that you use for the settings of the extension. So it looks like this:

<?xml version="1.0" encoding="UTF-8"?>
<schemalist>
    <schema id="org.gnome.shell.extensions.mycoolstuff" path="/org/gnome/shell/extensions/mycoolstuff/">
        <key name="cool-hotkey" type="as">
            <default><![CDATA[['<Ctrl><Super>T']]]></default>
            <summary>Hotkey to open the cool stuff.</summary>
        </key>
        
        ... other config options

    </schema>
</schemalist>

The key type is a "Array of String", so you can configure multiple key-combinations for the action.

In your code you use it like this:

const Main = imports.ui.main;
const Meta = imports.gi.Meta
const Shell = imports.gi.Shell
const ExtensionUtils = imports.misc.extensionUtils;

...

let my_settings = ExtensionUtils.getSettings("org.gnome.shell.extensions.mycoolstuff");

Main.wm.addKeybinding("cool-hotkey", my_settings,
    Meta.KeyBindingFlags.IGNORE_AUTOREPEAT,
    Shell.ActionMode.NORMAL | Shell.ActionMode.OVERVIEW
    this._hotkeyActionMethod.bind(this));

I would recommend to remove the key binding when the extension gets disabled. Don't know what happens if you don't do this.

Main.wm.removeKeybinding("cool-hotkey");

BTW: Changes to the settings (via dconf editor, gsettings or your extensions preferences) are active immediately.

Gareve
  • 3,372
  • 2
  • 21
  • 23
Ralf
  • 1,773
  • 8
  • 17
  • `> Don't know what happens if you don't do this.` If you don't remove the keybind on disable, the callback `_hotkeyActionMethod` will be called & possibly try to access some value that's not on memory or on an invalid state, leading to weird errors & null pointer exceptions. Thus why is good practice to remove the keybinding. – Gareve Jan 17 '22 at 00:04
  • 1
    Thanks! This code saved me so much time and it looks clean! :) – Gareve Jan 17 '22 at 00:04
3

Following is a copy of my answer here I've only tested this in Gnome 3.22

TL;DR

Here is a class:

KeyManager: new Lang.Class({
    Name: 'MyKeyManager',

    _init: function() {
        this.grabbers = new Map()

        global.display.connect(
            'accelerator-activated',
            Lang.bind(this, function(display, action, deviceId, timestamp){
                log('Accelerator Activated: [display={}, action={}, deviceId={}, timestamp={}]',
                    display, action, deviceId, timestamp)
                this._onAccelerator(action)
            }))
    },

    listenFor: function(accelerator, callback){
        log('Trying to listen for hot key [accelerator={}]', accelerator)
        let action = global.display.grab_accelerator(accelerator)

        if(action == Meta.KeyBindingAction.NONE) {
            log('Unable to grab accelerator [binding={}]', accelerator)
        } else {
            log('Grabbed accelerator [action={}]', action)
            let name = Meta.external_binding_name_for_action(action)
            log('Received binding name for action [name={}, action={}]',
                name, action)

            log('Requesting WM to allow binding [name={}]', name)
            Main.wm.allowKeybinding(name, Shell.ActionMode.ALL)

            this.grabbers.set(action, {
                name: name,
                accelerator: accelerator,
                callback: callback
            })
        }

    },

    _onAccelerator: function(action) {
        let grabber = this.grabbers.get(action)

        if(grabber) {
            this.grabbers.get(action).callback()
        } else {
            log('No listeners [action={}]', action)
        }
    }
})

And that's how you you use it:

let keyManager = new KeyManager()
keyManager.listenFor("<ctrl><shift>a", function(){
    log("Hot keys are working!!!")
})

You're going to need imports:

const Lang = imports.lang
const Meta = imports.gi.Meta
const Shell = imports.gi.Shell
const Main = imports.ui.main

Explanation

I might be terribly wrong, but that what I've figured out in last couple days.

First of all it is Mutter who is responsible for listening for hotkeys. Mutter is a framework for creating Window Managers, it is not an window manager itself. Gnome Shell has a class written in JS and called "Window Manager" - this is the real Window Manager which uses Mutter internally to do all low-level stuff. Mutter has an object MetaDisplay. This is object you use to request listening for a hotkey. But! But Mutter will require Window Manager to approve usage of this hotkey. So what happens when hotkey is pressed? - MetaDisplay generates event 'filter-keybinding'. - Window Manager in Gnome Shell checks if this hotkey allowed to be processed. - Window Manager returns appropriate value to MetaDisplay - If it is allowed to process this hotkey, MetaDisplay generates event 'accelerator-actived' - Your extension must listen for that event and figure out by action id which hotkey is activated.

Community
  • 1
  • 1
p2t2p
  • 182
  • 2
  • 6
0

Same as that of @p2t2p but recast using ES5 class. This is also using my logger class but you can replace that with log().

const Lang = imports.lang
const Meta = imports.gi.Meta
const Shell = imports.gi.Shell
const Main = imports.ui.main

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

const Logger = Me.imports.logger.Logger;

var KeyboardShortcuts = class KeyboardShortcuts {
  constructor(settings) {
    this._grabbers = {};

    this.logger = new Logger('kt kbshortcuts', settings);

    global.display.connect('accelerator-activated', (display, action, deviceId, timestamp) => {
      this.logger.debug("Accelerator Activated: [display=%s, action=%s, deviceId=%s, timestamp=%s]",
        display, action, deviceId, timestamp)
      this._onAccelerator(action)
    });
  }

  listenFor(accelerator, callback) {
    this.logger.debug('Trying to listen for hot key [accelerator=%s]', accelerator);
    let action = global.display.grab_accelerator(accelerator, 0);

    if (action == Meta.KeyBindingAction.NONE) {
      this.logger.error('Unable to grab accelerator [%s]', accelerator);
      return;
    }

    this.logger.debug('Grabbed accelerator [action={}]', action);
    let name = Meta.external_binding_name_for_action(action);
    this.logger.debug('Received binding name for action [name=%s, action=%s]',
        name, action)

    this.logger.debug('Requesting WM to allow binding [name=%s]', name)
    Main.wm.allowKeybinding(name, Shell.ActionMode.ALL)

    this._grabbers[action]={
      name: name,
      accelerator: accelerator,
      callback: callback
    };
  }

  _onAccelerator(action) {
    let grabber = this._grabbers[action];

    if (grabber) {
      grabber.callback();
    } else {
      this.logger.debug('No listeners [action=%s]', action);
    }
  }
}

and use it like,

      this.accel = new KeyboardShortcuts(this.settings);
      this.accel.listenFor("<ctrl><super>T", () => {
        this.logger.debug("Toggling show endtime");
        this._timers.settings.show_endtime = !this._timers.settings.show_endtime;
      });
Steeve McCauley
  • 683
  • 5
  • 11