Python script to toggle mapping of Wacom tablet to a single monitor

About writing shell scripts and making the most of your shell
Forum rules
Topics in this forum are automatically closed 6 months after creation.
Locked
Pheeble
Level 3
Level 3
Posts: 116
Joined: Sun Jun 21, 2015 11:27 pm
Location: Brisbane, Australia

Python script to toggle mapping of Wacom tablet to a single monitor

Post by Pheeble »

I have a 4-monitor display, and I also use a prehistoric Wacom graphics tablet in Blender, Gimp, etc. (Anybody remember the Graphire 2 tablet? Yep, that's the one. :lol: )

It's difficult to work with the Wacom when its input area is spread across 4 monitors, and it's better to map the Wacom's input to just one monitor. The problem for me is that I want to map the Wacom to different monitors depending on what I'm doing.

To deal with this, I wrote a Python3 script to map all attached Wacom devices to the monitor that currently contains the mouse cursor. The script acts as a toggle, so running the script again disables the mapping and reverts the Wacom input back to the default. That way I can lock the Wacom to one monitor, then disable the mapping and move the cursor to a different monitor, and lock the Wacom to that monitor.

I doubt if anyone else will ever have a use for this script, but I thought I'd post it here just in case.

Code: Select all

#!/usr/bin/env python3
""""
    wacom_toggle_map_to_monitor.py
    
    A Python3 script to toggle the mapping of all connected Wacom
    devices to the monitor containing the mouse cursor.

    This script requires the utilities 'xinput' and 'notify-send'.

    OS: Linux (Tested on Linux Mint 20.1 XFCE)

    Released: 2021-04-06_09-27-58
"""

import sys
import subprocess
import numpy as np
from Xlib import display
import gi
gi.require_version("Gdk", "3.0")
from gi.repository import Gdk

# Default 'Coordinate Transformation Matrix' values (ie. no mapping)
F_CTM_DEFAULT = np.array([1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0])
S_CTM_DEFAULT = ['1', '0', '0', '0', '1', '0', '0', '0', '1']

def get_wacoms():
    """ Get the names of all connected Wacom devices from 'xinput'

        Returns: list of strings
    """

    # Get a list of device names from xinput (NO PYTHON LIBRARY TO DO THIS IN XINPUT)
    xinput = subprocess.run(['xinput',
                             '--list',
                             '--name-only'],
                            check=False,
                            encoding='UTF-8',
                            capture_output=True)

    if xinput.returncode:
        print('get_wacoms() : xinput error: {0}'.format(xinput.stderr))
        sys.exit(xinput.returncode)

    xlines = xinput.stdout

    # Find any line containing 'wacom' and store it in a list
    wacoms = []
    for line in xlines.split('\n'):
        if 'wacom' in line.lower():
            wacoms.append(line)

    return wacoms

def get_cursor_monitor_name():
    """ Get the name of the monitor containing the mouse cursor.

        Returns: string
    """

    # Get the current mouse cursor position
    data = display.Display().screen().root.query_pointer()
    cursor_x = data.root_x
    cursor_y = data.root_y

    # Try to find which monitor contains the cursor
    cmon = ''
    gdkdsp = Gdk.Display.get_default()

    for i in range(gdkdsp.get_n_monitors()):
        monitor = gdkdsp.get_monitor(i)
        scale = monitor.get_scale_factor()
        geo = monitor.get_geometry()

        # Calculate the coordinates of this monitor
        xmin = geo.x * scale
        xmax = xmin + (geo.width * scale)
        ymin = geo.y * scale
        ymax = ymin + (geo.height * scale)

        # Check if the cursor is within this monitor's coordinates
        if xmin <= cursor_x < xmax and ymin <= cursor_y < ymax:
            # Get the name of the monitor containing the cursor
            cmon = monitor.get_model()
            break

    return cmon

def is_mapping_on(wacoms):
    """ Check if any of the connected Wacom devices have a non-default
        Coordinate Transformation Matrix, indicating that they have
        already been mapped to a monitor

        Parameters: string list

        Returns: boolean
    """

    for wacd in wacoms:
        xinput = subprocess.run(['xinput',
                                 'list-props',
                                 wacd],
                                check=False,
                                encoding='UTF-8',
                                capture_output=True)

        if xinput.returncode:
            print('is_mapping_on(wacoms) : xinput error: {0}'.format(xinput.stderr))
            sys.exit(xinput.returncode)

        xlines = xinput.stdout

        for line in xlines.split('\n'):
            if 'Coordinate Transformation Matrix' in line:
                # Get the xinput result after ';', then remove whitespace,
                # and then convert to list using ',' as separator
                line = (line.split(':', 1)[-1]).strip().split(',')

                # Convert list to array of floats
                ctm = np.array(line, dtype=float)

                # Check if ctm float array matches f_default_CTM array
                comparison = F_CTM_DEFAULT == ctm
                if not comparison.all(): # ctm has been changed
                    return True

    return False

def set_mapping_off(wacoms):
    """ Undo any mapping of connected Wacom devices by setting their
        Coordinate Transformation Matrix to the default.

        Parameters: string list

        Returns: none
    """

    wmsg = ''

    for wacd in wacoms:
        xinput = subprocess.run(['xinput',
                                 'set-prop',
                                 wacd,
                                 'Coordinate Transformation Matrix']
                                + S_CTM_DEFAULT,
                                check=False,
                                encoding='UTF-8',
                                capture_output=True)

        if xinput.returncode:
            print('set_mapping_off(wacoms) : xinput error: {0}'.format(xinput.stderr))
            sys.exit(xinput.returncode)

        wmsg += '\n<b>' + wacd + '</b>'

    notify('Wacom Mapping Disabled',
           '\nThe Wacom devices:\n{0}\n\nare not mapped to'
           ' a monitor'.format(wmsg))

def set_mapping_on(wacoms, monitor):
    """
        Map each listed Wacom device to the specified monitor

        Parameters: string list, string

        Returns: none
    """

    wmsg = ''

    for wacd in wacoms:
        xinput = subprocess.run(['xinput',
                                 'map-to-output',
                                 wacd, monitor],
                                check=False,
                                encoding='UTF-8',
                                capture_output=True)

        if xinput.returncode:
            print('set_mapping_on(wacoms, monitor) : xinput error: {0}'.format(xinput.stderr))
            sys.exit(xinput.returncode)

        wmsg += '\n<b>' + wacd + '</b>'

    notify('Wacom Mapping Enabled',
           '\nThe Wacom devices:\n{0}\n\nhave been mapped to'
           ' the monitor connected at\n\n<b>{1}</b>'.format(wmsg, monitor))

def notify(title, message):
    """
        Send a desktop notification re tablet mapping state.

        Parameters: string, string

        Returns: none

    """
    send = subprocess.run(['notify-send',
                           '--urgency=normal',
                           '--expire-time=5000',
                           '--icon=input-tablet',
                           '--category=device',
                           title,
                           message],
                          check=False,
                          encoding='UTF-8',
                          capture_output=True)
    if send.returncode:
        print('notify(title, message) : notify-send error: {0}'.format(send.stderr))
        sys.exit(send.returncode)

def main():
    """ It's the main function, innit... """

    wacoms = get_wacoms()

    if is_mapping_on(wacoms):
        set_mapping_off(wacoms)
    else:
        monitor = get_cursor_monitor_name()
        if monitor:
            set_mapping_on(wacoms, monitor)
        else:
            print('Error: could not get name of cursor monitor')
            sys.exit(1)

    sys.exit(0)

if __name__ == "__main__":
    # execute only if run as a script
    main()
Last edited by LockBot on Wed Dec 28, 2022 7:16 am, edited 1 time in total.
Reason: Topic automatically closed 6 months after creation. New replies are no longer allowed.
nukeCrayon
Level 1
Level 1
Posts: 34
Joined: Fri Apr 09, 2021 1:07 am
Location: Quezon City, NCR, Philippines

Re: Python script to toggle mapping of Wacom tablet to a single monitor

Post by nukeCrayon »

hello. I'm also looking for a program like 'display toggle' on my wacom cintiq in windows. is this the one?
User avatar
absque fenestris
Level 12
Level 12
Posts: 4124
Joined: Sat Nov 12, 2016 8:42 pm
Location: Confoederatio Helvetica

Re: Python script to toggle mapping of Wacom tablet to a single monitor

Post by absque fenestris »

This is exactly what can be set in System Preferences > Graphics Tablet.

At least on the Cinnamon desktop, 2 monitors and an old Wacom tablet.
nukeCrayon
Level 1
Level 1
Posts: 34
Joined: Fri Apr 09, 2021 1:07 am
Location: Quezon City, NCR, Philippines

Re: Python script to toggle mapping of Wacom tablet to a single monitor

Post by nukeCrayon »

absque fenestris wrote: Sat Jun 05, 2021 2:19 am This is exactly what can be set in System Preferences > Graphics Tablet.

At least on the Cinnamon desktop, 2 monitors and an old Wacom tablet.
I'm in a Cintiq 16 and it doesn't have toggle display. how can I run this python script?
User avatar
absque fenestris
Level 12
Level 12
Posts: 4124
Joined: Sat Nov 12, 2016 8:42 pm
Location: Confoederatio Helvetica

Re: Python script to toggle mapping of Wacom tablet to a single monitor

Post by absque fenestris »

Image


Mint 20.0 Cinnamon & Wacom Bamboo > Settings: Graphics Tablet
nukeCrayon
Level 1
Level 1
Posts: 34
Joined: Fri Apr 09, 2021 1:07 am
Location: Quezon City, NCR, Philippines

Re: Python script to toggle mapping of Wacom tablet to a single monitor

Post by nukeCrayon »

absque fenestris wrote: Sat Jun 05, 2021 5:28 am Image


Mint 20.0 Cinnamon & Wacom Bamboo > Settings: Graphics Tablet
Image
User avatar
absque fenestris
Level 12
Level 12
Posts: 4124
Joined: Sat Nov 12, 2016 8:42 pm
Location: Confoederatio Helvetica

Re: Python script to toggle mapping of Wacom tablet to a single monitor

Post by absque fenestris »

In the meantime, I am convinced that - for whatever reason* - I got a particularly good Mint.

* Conjecture:
  • a) compensatory all-mighty justice for horrendous bad investments in the 90s?
  • b) a secret fund by the fruit company especially for me, which supplies me with a highly functional Mint? (...just kidding)
  • c) because of my nickname I get something particularly fine ..?
nukeCrayon
Level 1
Level 1
Posts: 34
Joined: Fri Apr 09, 2021 1:07 am
Location: Quezon City, NCR, Philippines

Re: Python script to toggle mapping of Wacom tablet to a single monitor

Post by nukeCrayon »

Hey, I found something in the web. Is it the same?

https://forum.tvpaint.com/viewtopic.php?t=4923
Locked

Return to “Scripts & Bash”