Ubuntu – Block Unity keyboard shortcuts when a certain application is active

intellijshortcut-keysunity

The great JetBrains IDEs (IDEA et al.) assign pretty much every conceivable keyboard shortcut to some function. While mildly overwhelming at times, it also makes for efficient use.

My problem is that Unity assigns some of these shortcuts as well, and they take precedence. One particularly annoying example is CTRL + ALT + L. The issue has been explored before here.

However, neither of the approaches is satisfactory.

  1. Turning off system shortcuts globally impedes my overall productivity with the system.
  2. Switching to a different keymap in IDEA will confuse the hell out of me when I develop on different platforms (and have to choose different mappings).

Is there a way to turn off system shortcuts only when a certain application is active, i.e. running and in focus?

I'd be willing to run a script every time I launch the application.

Best Answer

How to automatically disable multiple (specific) shortcuts if (and as long as) a specific application's window is active

The script below will disable specific key shortcuts when an arbitrary application's window is active.

Although you mentioned""I'd be willing to run a script every time I launch the application.", There is no reason to kill the script afterwards, it is extremely low on juice.

The script

#!/usr/bin/env python3
import subprocess
import time
import os

app = "gedit"

f = os.path.join(os.environ["HOME"], "keylist")

def run(cmd):
    subprocess.Popen(cmd)

def get(cmd):
    try:
        return subprocess.check_output(cmd).decode("utf-8").strip()
    except:
        pass

def getactive():
    return get(["xdotool", "getactivewindow"])

def setkeys(val):
    # --- add the keys to be disabled below  
    keys = [
         ["org.gnome.settings-daemon.plugins.media-keys", "logout"],
         ["org.gnome.settings-daemon.plugins.media-keys", "screensaver"],
        ]
    # ---
    writelist = []
    if not val:
        try:
            values = open(f).read().splitlines()
        except FileNotFoundError:
            values = []
        for i, key in enumerate(keys):
            try:
                cmd = ["gsettings", "set"]+key+[values[i]]
            except IndexError:
                cmd = ["gsettings", "reset"]+key
            run(cmd)
    else:
        for key in keys:
            cmd = ["gsettings", "set"]+key+["['']"]
            read =  get(["gsettings", "get"]+key)
            writelist.append(read)
            run(cmd)

    if writelist:
        open(f, "wt").write("\n".join(writelist))

front1 = None

while True:
    time.sleep(1)
    pid = get(["pgrep", app])
    if pid:
        try:
            active = get(["xdotool", "getactivewindow"])
            relevant = get(["xdotool", "search", "--all", "--pid", pid]).splitlines()
            front2 = active in relevant
        except AttributeError:
            front2 = front1           
    else:
        front2 = False
    if front2 != front1:
        if front2:
            setkeys(True)
        else:
            setkeys(False)

    front1 = front2

How to use

  1. The script needs xdotool:

    sudo apt-get install xdotool
    
  2. Copy the script into an empty file, save it as disable_shortcuts.py

  3. In the head of the script, replace in the line:

    app = "gedit"
    

    "gedit" by your application, meaning: the process name that owns the window.

  4. Test-run the script by the command:

    python3 /path/to/disable_shortcuts.py
    
  5. If all works fine, add it to Startup Applications: Dash > Startup Applications > Add. Add the command:

    /bin/bash -c "sleep 15 && python3 /path/to/disable_shortcuts.py"
    

Adding more shortcuts to be disabled

As an example, I added the shortcut you mentioned: CTRL + ALT + L. Shortcuts are set in the dconf database, and can be set or disabled using gsettings.

In the script, these gsettings entries are set in the function: setkeys()

def setkeys(val):
    # --- add the keys to be disabled below
    keys = [
        ["org.gnome.settings-daemon.plugins.media-keys", "screensaver"]
        ]
    # ---

An example to add (disabling) the log out shortcut:

  1. Open a terminal window, run the command dconf watch /
  2. Open System Settings > "Keyboard" > "Shortcuts" > "System"
  3. Re-set the shortcut to itself. In the terminal, you can see the gsettings key that belongs to the shortcut:

    enter image description here

  4. Now we have to add the found key (in a slightly different appearance):

    ["org.gnome.settings-daemon.plugins.media-keys", "logout"]
    

    ...to the "keys" list in our function:

    def setkeys(val):
        # --- add the keys to be disabled below
        keys = [
            ["org.gnome.settings-daemon.plugins.media-keys", "screensaver"],
             ["org.gnome.settings-daemon.plugins.media-keys", "logout"],
            ]
    

Now both CTRL + ALT + L and CTRL + ALT + Delete are disabled if your application is in front.

Explanation

As mentioned, shortcuts, like the ones you mention, are set in the dconf database. In the example CTRL + ALT + L, the key to set or edit the schortcut is:

org.gnome.settings-daemon.plugins.media-keys screensaver

To disable the key, the command is:

gsettings set org.gnome.settings-daemon.plugins.media-keys screensaver ""

To reset the key to its default value:

gsettings reset org.gnome.settings-daemon.plugins.media-keys screensaver

The script looks once per second if:

  • your application runs at all
  • if so, it looks if any of its windows is active
  • again (only) if so, it disables the shortcuts, listed in

    # --- add the keys to be disabled below
    keys = [
        ["org.gnome.settings-daemon.plugins.media-keys", "screensaver"],
         ["org.gnome.settings-daemon.plugins.media-keys", "logout"],
       ]
    

    ...waiting for the next change in state.

If the active window is not one of your application any more, the keys, mentioned in the list, are reset to default.

Note

As mentioned earlier, the additional burden to the processor of the script is nihil. You could very well run it on startup, as explained in "How to use".


Affecting multiple applications

As discussed in comments, in OP's specific case, it is useful to apply disabling shortcuts on a group of applications, all residing in one directory.

Below a version to apply this on all applications of which the output of

pgrep -f 

will include a specific directory. In my example, I set the /opt directory, so if the active window is one of any of the applications in /opt, the set shortcuts will be disabled.


bringing a window of one of the applications in /opt to front will disable the logout shortcut

enter image description here

re- enabling the shortcut if another window gets focus

enter image description here


The script

#!/usr/bin/env python3
import subprocess
import time
import os 

appdir = "/opt"

f = os.path.join(os.environ["HOME"], "keylist")

def run(cmd):
    subprocess.call(cmd)

def get(cmd):
    try:
        return subprocess.check_output(cmd).decode("utf-8").strip()
    except:
        pass

def getactive():
    return get(["xdotool", "getactivewindow"])

def setkeys(val):
    # --- add the keys to be disabled below  
    keys = [
         ["org.gnome.settings-daemon.plugins.media-keys", "logout"],
         ["org.gnome.settings-daemon.plugins.media-keys", "screensaver"],
         ["org.gnome.desktop.wm.keybindings", "begin-move"],
        ]
    # ---
    writelist = []
    if not val:
        try:
            values = open(f).read().splitlines()
        except FileNotFoundError:
            values = []
        # for key in keys:
        for i, key in enumerate(keys):
            try:
                cmd = ["gsettings", "set"]+key+[values[i]]
            except IndexError:
                cmd = ["gsettings", "reset"]+key
            run(cmd)
    else:
        for key in keys:
            cmd = ["gsettings", "set"]+key+["['']"]
            read =  get(["gsettings", "get"]+key)
            writelist.append(read)
            run(cmd)
    if writelist:
        open(f, "wt").write("\n".join(writelist))

front1 = None

while True:
    time.sleep(1)
    # check if any of the apps runs at all
    checkpids = get(["pgrep", "-f", appdir])
    # if so:
    if checkpids:
        checkpids = checkpids.splitlines()
        active = getactive()
        # get pid frontmost (doesn't work on pid 0)
        match = [l for l in get(["xprop", "-id", active]).splitlines()\
                 if "_NET_WM_PID(CARDINAL)" in l]
        if match:
            # check if pid is of any of the relevant apps
            pid = match[0].split("=")[1].strip()
            front2 = True if pid in checkpids else False
        else:
            front2 = False
    else:
        front2 = False
    if front2 != front1:
        if front2:
            setkeys(True)
        else:
            setkeys(False)
    front1 = front2

How to use

  1. Like the first script, xdotool needs to be installed:

    sudo apt-get install xdotool
    
  2. Copy the script into an empty file, save it as disable_shortcuts.py

  3. In the head of the script, replace in the line:

    appdir = "/opt"
    

    "/opt" by the directory your applications are.

  4. Test-run the script by the command:

    python3 /path/to/disable_shortcuts.py
    
  5. If all works fine, add it to Startup Applications: Dash > Startup Applications > Add. Add the command:

    /bin/bash -c "sleep 15 && python3 /path/to/disable_shortcuts.py"
    

Adding other shortcuts to the list works exactly similar to version 1 of the script.

Does it work on all applications?

In your answer, you mention:

xprop does not reveal PIDs for all windows. Failing example: stopwatch.

Windows with pid 0 (like tkinter windows, including Idle), have no window- id in the output of xprop -id. Idle does not have any clashing shortcuts though in my experience. If you run into any application with pid 0 that would require disabling specific shortcuts, please mention.

In that case, a possible escape would be to convert the output of

xdotool getactivewindow

to hex, the format wmctrl uses, and subsequently look up the corresponding pid in the output of

wmctrl -lp

Although that seemed the most obvious thing to do to start with, I didn't use it in the script to keep the script as light-weight as possible.

Related Question