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()