Multiple Key Event Bindings in Tkinter - "Control + E" "Command (apple) + E" etc - events

Mac OS X 10.6.6 - Tkinter
I want to bind multiple-key events, and while I have found an effbot article and the Tk man pages, I've been unable to make this work correctly. I'm new here.
I've had mixed success. I've been able to get Shift + letter key, but not Control or Command (Apple key). What I really want to do is Command + letter and Control + letter key so it would theoretically work in Windows and OS X.
I want it to work at window-level, so I'm using root. Perhaps there is a better way. Below is what I've tried:
root.bind('<Shift-E>', self.pressedCmdE) # Works
root.bind('e', self.pressedCmdE) # Works
root.bind('<Command-E>', self.pressedCmdE) # Does Not Work
#root.bind('<Mod1-E>', self.pressedCmdE) # # Do Mod1, M1, and
#root.bind('<M1-E>', self.pressedCmdE) # # Command mean the same thing?
Strangely, when I press alt/option + (E, N, or others) it creates an error. Is it interacting with PythonLauncher?
2011-06-16 16:19:22.618 Python[1546:d07] An uncaught exception was raised
2011-06-16 16:19:22.621 Python[1546:d07] *** -[NSCFString characterAtIndex:]: Range or index out of bounds
2011-06-16 16:19:22.622 Python[1546:d07] *** Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[NSCFString characterAtIndex:]: Range or index out of bounds'
*** Call stack at first throw:
(
0 CoreFoundation 0x00007fff85b397b4 __exceptionPreprocess + 180
1 libobjc.A.dylib 0x00007fff848b90f3 objc_exception_throw + 45
2 CoreFoundation 0x00007fff85b395d7 +[NSException raise:format:arguments:] + 103
3 CoreFoundation 0x00007fff85b39564 +[NSException raise:format:] + 148
4 Foundation 0x00007fff866eb5e1 -[NSCFString characterAtIndex:] + 97
5 Tk 0x0000000100759bcf Tk_SetCaretPos + 663
6 Tk 0x000000010075fd94 Tk_MacOSXSetupTkNotifier + 699
7 Tcl 0x000000010061d2ae Tcl_DoOneEvent + 297
8 _tkinter.so 0x00000001001d9be9 init_tkinter + 1132
9 Python 0x0000000100089187 PyEval_EvalFrameEx + 15317
10 Python 0x000000010008acce PyEval_EvalCodeEx + 1803
11 Python 0x000000010008935e PyEval_EvalFrameEx + 15788
12 Python 0x000000010008acce PyEval_EvalCodeEx + 1803
13 Python 0x000000010008ad61 PyEval_EvalCode + 54
14 Python 0x00000001000a265a Py_CompileString + 78
15 Python 0x00000001000a2723 PyRun_FileExFlags + 150
16 Python 0x00000001000a423d PyRun_SimpleFileExFlags + 704
17 Python 0x00000001000b0286 Py_Main + 2718
18 Python 0x0000000100000e6c start + 52
)
terminate called after throwing an instance of 'NSException'
Abort trap

With Tkinter, "Control-R" means Ctrl-Shift-R whereas "Control-r" means Ctrl-R. So make sure you're not mixing up uppercase and lowercase.

This appears to be a bug in Tk. I get the same error with tcl/tk on the mac as well as with python/tkinter. You can bind <Command-e> to a widget (I tried with a text widget) but binding it to the root window or to "all" seems to cause the error you get.

Enhanced to cover the Alt and Meta keys, aka Option and Command on macOS.
# Original <https://StackOverflow.com/questions/6378556/
# multiple-key-event-bindings-in-tkinter-control-e-command-apple-e-etc>
# Status of alt (ak option), control, meta (aka command)
# and shift keys in Python tkinter
# Note, tested only on macOS 10.13.6 with Python 3.7.4 and Tk 8.6.9
import tkinter as tk
import sys
_macOS = sys.platform == 'darwin'
_Alt = 'Option' if _macOS else 'Alt'
_Ctrl = 'Control'
_Meta = 'Command' if _macOS else 'Meta'
_Shift = 'Shift'
alt = ctrl = meta = shift = ''
def up_down(mod, down):
print('<%s> %s' % (mod, 'down' if down else 'up'))
return down
def key(event):
'''Other key pressed or released'''
# print(event.keycode, event.keysym, event.down)
global alt, ctrl, meta, shift
t = [m for m in (alt, ctrl, shift, meta, str(event.keysym)) if m]
print('+'.join(t))
def alt_key(down, *unused):
'''Alt (aka Option on macOS) key is pressed or released'''
global alt
alt = up_down(_Alt, down)
def control_key(down, *unused):
'''Control key is pressed or released'''
global ctrl
ctrl = up_down(_Ctrl, down)
def meta_key(down, *unused):
'''Meta (aka Command on macOS) key is pressed or released'''
global meta
meta = up_down(_Meta, down)
def shift_key(down, *unused):
'''Shift button is pressed or released'''
global shift
shift = up_down(_Shift, down)
def modifier(root, mod, handler, down):
'''Add events and handlers for key press and release'''
root.event_add('<<%sOn>>' % (mod,), ' <KeyPress-%s_L>' % (mod,), '<KeyPress-%s_R>' % (mod,))
root.bind( '<<%sOn>>' % (mod,), lambda _: handler('<%s>' % (down,)))
root.event_add('<<%sOff>>' % (mod,), '<KeyRelease-%s_L>' % (mod,), '<KeyRelease-%s_R>' % (mod,))
root.bind( '<<%sOff>>' % (mod,), lambda _: handler(''))
root = tk.Tk()
root.geometry('256x64+0+0')
modifier(root, 'Alt', alt_key, _Alt)
modifier(root, 'Control', control_key, _Ctrl)
modifier(root, 'Meta', meta_key, _Meta)
modifier(root, 'Shift', shift_key, _Shift)
root.bind('<Key>', key)
root.mainloop()

Option 1
Something like this:
# Status of control, shift and control+shift keys in Python
import tkinter as tk
ctrl = False
shift = False
ctrl_shift = False
def key(event):
global ctrl, shift, ctrl_shift
#print(event.keycode, event.keysym, event.state)
if ctrl_shift:
print('<Ctrl>+<Shift>+{}'.format(event.keysym))
elif ctrl:
print('<Ctrl>+{}'.format(event.keysym))
elif shift:
print('<Shift>+{}'.format(event.keysym))
ctrl = False
shift = False
ctrl_shift = False
def control_key(state, event=None):
''' Controll button is pressed or released '''
global ctrl
ctrl = state
def shift_key(state, event=None):
''' Controll button is pressed or released '''
global shift
shift = state
control_shift(state)
def control_shift(state):
''' <Ctrl>+<Shift> buttons are pressed or released '''
global ctrl, ctrl_shift
if ctrl == True and state == True:
ctrl_shift = True
else:
ctrl_shift = False
root = tk.Tk()
root.geometry('256x256+0+0')
root.event_add('<<ControlOn>>', '<KeyPress-Control_L>', '<KeyPress-Control_R>')
root.event_add('<<ControlOff>>', '<KeyRelease-Control_L>', '<KeyRelease-Control_R>')
root.event_add('<<ShiftOn>>', '<KeyPress-Shift_L>', '<KeyPress-Shift_R>')
root.event_add('<<ShiftOff>>', '<KeyRelease-Shift_L>', '<KeyRelease-Shift_R>')
root.bind('<<ControlOn>>', lambda e: control_key(True))
root.bind('<<ControlOff>>', lambda e: control_key(False))
root.bind('<<ShiftOn>>', lambda e: shift_key(True))
root.bind('<<ShiftOff>>', lambda e: shift_key(False))
root.bind('<Key>', key)
root.mainloop()
Option 2
However, in the end, I decided to process keystrokes manually. You can se the example in this file. First, I set keycodes and shortcuts in two dictionaries self.keycode and self.__shortcuts:
# List of shortcuts in the following format: [name, keycode, function]
self.keycode = {} # init key codes
if os.name == 'nt': # Windows OS
self.keycode = {
'o': 79,
'w': 87,
'r': 82,
'q': 81,
'h': 72,
's': 83,
'a': 65,
}
else: # Linux OS
self.keycode = {
'o': 32,
'w': 25,
'r': 27,
'q': 24,
'h': 43,
's': 39,
'a': 38,
}
self.__shortcuts = [['Ctrl+O', self.keycode['o'], self.__open_image], # 0 open image
['Ctrl+W', self.keycode['w'], self.__close_image], # 1 close image
['Ctrl+R', self.keycode['r'], self.__roll], # 2 rolling window
['Ctrl+Q', self.keycode['q'], self.__toggle_poly], # 3 toggle between roi/hole drawing
['Ctrl+H', self.keycode['h'], self.__open_poly], # 4 open polygons for the image
['Ctrl+S', self.keycode['s'], self.__save_poly], # 5 save polygons of the image
['Ctrl+A', self.keycode['a'], self.__show_rect]] # 6 show rolling window rectangle
Then added self.__keystroke function to monitor <Ctrl> keystroke events. This function checks if <Ctrl> key is pressed or not:
def __keystroke(self, event):
""" Language independent handle events from the keyboard """
#print(event.keycode, event.keysym, event.state) # uncomment it for debug purposes
if event.state - self.__previous_state == 4: # check if <Control> key is pressed
for shortcut in self.__shortcuts:
if event.keycode == shortcut[1]:
shortcut[2]()
else: # remember previous state of the event
self.__previous_state = event.state
Finally, bind the self.__keystroke function to the master GUI window. Note that this function is bonded in the idle mode, because multiple keystrokes slow down the program on weak computers:
# Handle keystrokes in the idle mode, because program slows down on a weak computers,
# when too many key stroke events in the same time.
self.master.bind('<Key>', lambda event: self.master.after_idle(self.__keystroke, event))

Related

The wav file wont play after using pyinstaller --onefile.I just hear the windows 'beep'

This program displays a home circuit breaker panel. the user can view what is on each breaker on the panel (data taken from an imported dictionary of entered breaker panel info) or the user can check what breakers control any list zone (kitchen basement, etc) The breakerville program closes when the user decides and is supposed to play a wave file at the close. It doesn't play after the program is made into an exe with pyinstaller just the windows 'beep'.
I am suspecting that I may need to edit the spec file to get the wave file to work after compiled. Is this correct and if so how? Do I need to modify the spec file?
from playsound import playsound # CURRENTLY USING
from chart import chart
from BreakerZones import BreakerZones
import time
import sys
import colorama
import yaml # to print the nested_lookup results(n) on separate lines
from nested_lookup import nested_lookup, get_all_keys # importing 2 items from nested_lookup
from colorama import Fore, Back, Style
colorama.init(autoreset=True) # If you don't want to print Style.RESET_ALL all the time,
# reset automatically after each print statement with True
print(colorama.ansi.clear_screen())
print('\n'*4) # prints a newline 4 times
print(Fore.MAGENTA + ' Arriving-' + Fore.GREEN + ' *** BREAKERVILLE USA ***')
def main():
print('\n' * 2)
print(Fore.BLUE + ' Breaker Numbers and Zones')
k = get_all_keys(BreakerZones)
# raw amount of keys even repeats , has quotes
new_l = [] # eliminate extra repeating nested keys
for e in k: # has quotes
if e not in new_l and sorted(e) not in new_l: #
new_l.append(e) #
print()
new_l.sort() # make alphabetical
newer_l = ('%s' % ', '.join(map(str, new_l)).strip("' ,")) # remove ['%s'] brackets so they don't show up when run
print(' ', yaml.dump(newer_l, default_flow_style=False)) # strip("' ,") or will see leading "' ," in output
print(Fore.BLUE + ' ENTER A BREAKER # OR ZONE', Fore.GREEN + ': ', end='')
i = input().strip().lower() # these lines is workaround for the colorama
print() # user input() issue of 'code' appearing in screen output
if i in k:
n = (nested_lookup(i, BreakerZones, wild=False, with_keys=False)) # wild=True means key not case sensitive,
print(yaml.dump(n, default_flow_style=False)) # 'with_keys' returns values + keys also
# for key, value in n.items(): eliminated by using yaml
# print(key, '--', value) eliminated by using yaml
else:
print(Fore.YELLOW + ' Typo,' + Fore.GREEN + ' try again')
main()
print()
print(Fore.GREEN + ' Continue? Y or N: C for breaker chart : ', end='') # see comments ENTER A BREAKER
ans = input().strip().lower() # strip() removes any spaces before or after user input
if ans == 'c':
chart()
print()
print(Fore.GREEN + ' Continue? Y or N : ', end='')
ans = input().strip().lower() # strip() removes any spaces before or after user input
if ans == 'y': # shorter version 'continue Y or N' after printing breaker chart
main()
else:
print()
print(Fore.MAGENTA + ' Departing -' + Fore.GREEN + ' *** BREAKERVILLE ***')
playsound('train whistle.wav')
time.sleep(2) # delay to exit program
sys.exit()
elif ans != 'y':
print()
print(Fore.MAGENTA + ' Good Day -' + Fore.GREEN + ' *** BREAKERVILLE ***')
playsound('train whistle.wav') #CURRENTLY USING
time.sleep(2) # delay to exit program
sys.exit()
else:
main()
main()
For the records: The issue is fixed by providing full path to the sound file.
This is probably linked to the implementation of playsound and how it determines what is the current working directory. Please refer to https://pyinstaller.readthedocs.io/en/stable/runtime-information.html#run-time-information for a better understanding of that topic with pyinstaller

Logitech Script, 1st and 2nd click events with time reset

What I want to do is if I press the button on my mouse it uses a key like "E" and if I press the button again it uses the key "W" and after 2 seconds it resets, I mean if I don’t press the same button after 2 seconds it uses letter "e " again. Is that possible?
I've tried some codes but no results yet:
function OnEvent(event, arg, family)
if event == "MOUSE_BUTTON_PRESSED" and arg == 5 then
toggle = not toggle
if toggle then
PressKey("e")
ReleaseKey("e")
else
PressKey("w")
ReleaseKey("w")
end
end
end
local prev_tm_btn5 = -math.huge
function OnEvent(event, arg, family)
if event == "MOUSE_BUTTON_PRESSED" and arg == 5 then
local tm = GetRunningTime()
local key = tm - prev_tm_btn5 > 2000 and "e" or "w"
prev_tm_btn5 = tm
PressKey(key)
Sleep(15)
ReleaseKey(key)
end
end

Tool/Algorithm for text comparision after every key hit

I am struggling to find a text comparison tool or algorithm that can compare an expected text against the current state of the text being typed.
I will have an experimentee typewrite a text that he has in front of his eyes. My idea is to compare the current state of the text against the expected text whenever something is typed. That way I want to find out when and what the subject does wrong (I also want to find errors that are not in the resulting text but were in the intermediate text for some time).
Can someone point me in a direction?
Update #1
I have access to the typing data in a csv format:
This is example output data of me typing "foOBar". Every line has the form (timestamp, Key, Press/Release)
17293398.576653,F,P
17293398.6885,F,R
17293399.135282,LeftShift,P
17293399.626881,LeftShift,R
17293401.313254,O,P
17293401.391732,O,R
17293401.827314,LeftShift,P
17293402.073046,O,P
17293402.184859,O,R
17293403.178612,B,P
17293403.301748,B,R
17293403.458137,LeftShift,R
17293404.966193,A,P
17293405.077869,A,R
17293405.725405,R,P
17293405.815159,R,R
In Python
Given your input csv file (I called it keyboard_records.csv)
17293398.576653,F,P
17293398.6885,F,R
17293399.135282,LeftShift,P
17293399.626881,LeftShift,R
17293401.313254,O,P
17293401.391732,O,R
17293401.827314,LeftShift,P
17293402.073046,O,P
17293402.184859,O,R
17293403.178612,B,P
17293403.301748,B,R
17293403.458137,LeftShift,R
17293404.966193,A,P
17293405.077869,A,R
17293405.725405,R,P
17293405.815159,R,R
The following code does the following:
Read its content and store it in a list named steps
For each step in steps recognizes what happened and
If it was a shift press or release sets a flag (shift_on) accordingly
If it was an arrow pressed moves the cursor (index of current where we insert characters) – if it the cursor is at the start or at the end of the string it shouldn't move, that's why those min() and max()
If it was a letter/number/symbol it adds it in curret at cursor position and increments cursor
Here you have it
import csv
steps = [] # list of all actions performed by user
expected = "Hello"
with open("keyboard.csv") as csvfile:
for row in csv.reader(csvfile, delimiter=','):
steps.append((float(row[0]), row[1], row[2]))
# Now we parse the information
current = [] # text written by the user
shift_on = False # is shift pressed
cursor = 0 # where is the cursor in the current text
for step in steps:
time, key, action = step
if key == 'LeftShift':
if action == 'P':
shift_on = True
else:
shift_on = False
continue
if key == 'LeftArrow' and action == 'P':
cursor = max(0, cursor-1)
continue
if key == 'RightArrow' and action == 'P':
cursor = min(len(current), cursor+1)
continue
if action == 'P':
if shift_on is True:
current.insert(cursor, key.upper())
else:
current.insert(cursor, key.lower())
cursor += 1
# Now you can join current into a string
# and compare current with expected
print(''.join(current)) # printing current (just to see what's happening)
else:
# What to do when a key is released?
# Depends on your needs...
continue
To compare current and expected have a look here.
Note: by playing around with the code above and a few more flags you can make it recognize also symbols. This will depend on your keyboard. In mine Shift + 6 = &, AltGr + E = € and Ctrl + Shift + AltGr + è = {. I think this is a good point to start.
Update
Comparing 2 texts isn't a difficult task and you can find tons of pages on the web about it.
Anyway I wanted to present you an object oriented approach to the problem, so I added the compare part that I previously omitted in the first solution.
This is still a rough code, without primary controls over the input. But, as you asked, this is pointing you in a direction.
class UserText:
# Initialize UserText:
# - empty text
# - cursor at beginning
# - shift off
def __init__(self, expected):
self.expected = expected
self.letters = []
self.cursor = 0
self.shift = False
# compares a and b and returns a
# list containing the indices of
# mismatches between a and b
def compare(a, b):
err = []
for i in range(min(len(a), len(b))):
if a[i] != b[i]:
err.append(i)
return err
# Parse a command given in the
# form (time, key, action)
def parse(self, command):
time, key, action = command
output = ""
if action == 'P':
if key == 'LeftShift':
self.shift = True
elif key == 'LeftArrow':
self.cursor = max(0, self.cursor - 1)
elif key == 'RightArrow':
self.cursor = min(len(self.letters), self.cursor + 1)
else:
# Else, a letter/number was pressed. Let's
# add it to self.letters in cursor position
if self.shift is True:
self.letters.insert(self.cursor, key.upper())
else:
self.letters.insert(self.cursor, key.lower())
self.cursor += 1
########## COMPARE WITH EXPECTED ##########
output += "Expected: \t" + self.expected + "\n"
output += "Current: \t" + str(self) + "\n"
errors = UserText.compare(str(self), self.expected[:len(str(self))])
output += "\t\t"
i = 0
for e in errors:
while i != e:
output += " "
i += 1
output += "^"
i += 1
output += "\n[{} errors at time {}]".format(len(errors), time)
return output
else:
if key == 'LeftShift':
self.shift = False
return output
def __str__(self):
return "".join(self.letters)
import csv
steps = [] # list of all actions performed by user
expected = "foobar"
with open("keyboard.csv") as csvfile:
for row in csv.reader(csvfile, delimiter=','):
steps.append((float(row[0]), row[1], row[2]))
# Now we parse the information
ut = UserText(expected)
for step in steps:
print(ut.parse(step))
The output for the csv file above was:
Expected: foobar
Current: f
[0 errors at time 17293398.576653]
Expected: foobar
Current: fo
[0 errors at time 17293401.313254]
Expected: foobar
Current: foO
^
[1 errors at time 17293402.073046]
Expected: foobar
Current: foOB
^^
[2 errors at time 17293403.178612]
Expected: foobar
Current: foOBa
^^
[2 errors at time 17293404.966193]
Expected: foobar
Current: foOBar
^^
[2 errors at time 17293405.725405]
I found the solution to my own question around a year ago. Now i have time to share it with you:
In their 2003 paper 'Metrics for text entry research: An evaluation of MSD and KSPC, and a new unified error metric', R. William Soukoreff and I. Scott MacKenzie propose three major new metrics: 'total error rate', 'corrected error rate' and 'not corrected error rate'. These metrics have become well established since the publication of this paper. These are exaclty the metrics i was looking for.
If you are trying to do something similiar to what i did, e.g. compare the writing performance on different input devices this is the way to go.

How can I read the arrow keys in a Ruby curses application?

I have a Ruby curses application in which I'd like to trap for the arrow keys and function keys. The problem is that some keystrokes generate multiple values when using STDIN.getch. When I type a 'regular' key like a-z I get a single value back. When I type a [F]key or arrow key I get three values back.
Is there a gem designed for handling keyboard input or a better way to accomplish reading keystrokes?
#!/usr/bin/ruby
require 'curses'
require 'io/console'
Curses.noecho
Curses.init_screen
main_window = Curses::Window.new(24, 40, 1, 0)
num_keys = 0
loop do
ch = STDIN.getch
num_keys = num_keys + 1
main_window.addstr(' key:' + ch.inspect + ' count:' + num_keys.to_s)
main_window.refresh
break if ch == 'q'
end
Curses.close_screen
Trying enabling the keypad on the window right after you instantiate it.
main_window = Curses::Window.new(24, 40, 1, 0)
main_window.keypad = true
and then instead of using STDIN.getch there's a getch method on the window as well you can use, so try changing
ch = STDIN.getch
to
ch = main_window.getch
now when I run your program, I get
key: 259 count: 1
when I hit the up arrow instead of
key:"\e" count 1 key:"[" count:2 key:"A" count:3

KEY_DOWN not working in CURSES

I'm building a curses module and using KEY_DOWN to check if a arrow down key is pressed.
But, I get a Name error saying KEY_DOWN is not defined.
if value == KEY_DOWN:
NameError: global name 'KEY_DOWN' is not defined
Good day!
You have to do:
if value == curses.KEY_DOWN:
for it to work.
Hope this works!!!
But if this doesn't work show us your code (so we can analyze it)
To follow up to mvndaai's answer, if you want to detect arrow keys in Python, you have to AND together the three different ASCII values. For example:
key = getch()
if key == (27 and 91 and 65): #27 is ESC, 91 is [, and 65 is A
print("Up key pressed!")
if key == (27 and 91 and 66):
print("Down key pressed!")
if key == (27 and 91 and 67):
print("Right key pressed!")
if key == (27 and 91 and 68):
print("Left key pressed!")
I am not sure why the yave a gloabl named KEY_DOWN, but if you want a key down, you need to do 3 getchs. Warning, the first getch is the same as an ESC. That means you either make sure it doesn't close on ESC or do a work around like I did below. I also included a chart of what you will get as a getch for each key.
KeyESCUPDOWNRIGHTLEFT
getch2727272727
getch [[[[
getch ABCD
Which means that when you hit any Arrow key and you are triggering a ESC.
In ruby I tried a work around of checking for 27, then doing a quick timeout on another getch. If that gets a [ it is an arrow or something else, otherwise it is the escape key. Here is my ruby code:
def read_key
ch = getch
return check_escape_chars if ch == 27
ch
end
def check_escape_chars
require 'timeout'
begin
Timeout.timeout(0.0001) {getch}
case getch
when "A"; return "UP"
when "B"; return "DOWN"
when "C"; return "RIGHT"
when "D"; return "LEFT"
end
rescue Timeout::Error
return "ESC"
end
end

Resources