| 1 |
# WinDictator: |
|---|
| 2 |
# Dictate in Windows, have the text typed in Linux via X faked keystroke events |
|---|
| 3 |
# |
|---|
| 4 |
# Copyright (C) 2005-2006 by Edwin A. Suominen, http://www.eepatents.com |
|---|
| 5 |
# |
|---|
| 6 |
# This program is free software; you can redistribute it and/or modify it under |
|---|
| 7 |
# the terms of the GNU General Public License as published by the Free Software |
|---|
| 8 |
# Foundation; either version 2 of the License, or (at your option) any later |
|---|
| 9 |
# version. |
|---|
| 10 |
# |
|---|
| 11 |
# This program is distributed in the hope that it will be useful, but WITHOUT |
|---|
| 12 |
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
|---|
| 13 |
# FOR A PARTICULAR PURPOSE. See the file COPYING for more details. |
|---|
| 14 |
# |
|---|
| 15 |
# You should have received a copy of the GNU General Public License along with |
|---|
| 16 |
# this program; if not, write to the Free Software Foundation, Inc., 51 |
|---|
| 17 |
# Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA |
|---|
| 18 |
|
|---|
| 19 |
|
|---|
| 20 |
""" |
|---|
| 21 |
A virtual typist |
|---|
| 22 |
""" |
|---|
| 23 |
|
|---|
| 24 |
from twisted.internet import defer |
|---|
| 25 |
|
|---|
| 26 |
import observer, keyer, keycodes |
|---|
| 27 |
|
|---|
| 28 |
|
|---|
| 29 |
class Typist(object): |
|---|
| 30 |
""" |
|---|
| 31 |
I type what you send. Give me some text and I'll see that the right fake |
|---|
| 32 |
keystrokes are generated |
|---|
| 33 |
|
|---|
| 34 |
""" |
|---|
| 35 |
def __init__(self): |
|---|
| 36 |
self.history = observer.KeyHistory() |
|---|
| 37 |
self.observer = observer.Observer(self.history) |
|---|
| 38 |
self.keyer = keyer.Keyer() |
|---|
| 39 |
self.keyCoder = keycodes.KeyCoder() |
|---|
| 40 |
|
|---|
| 41 |
def startup(self, *null): |
|---|
| 42 |
""" |
|---|
| 43 |
Starts up all my helpers, returning a C{Deferred} that fires when all |
|---|
| 44 |
are ready. |
|---|
| 45 |
""" |
|---|
| 46 |
d1 = self.keyer.startup() |
|---|
| 47 |
d2 = self.observer.startup() |
|---|
| 48 |
return defer.DeferredList([d1,d2]) |
|---|
| 49 |
|
|---|
| 50 |
def shutdown(self, *null): |
|---|
| 51 |
""" |
|---|
| 52 |
Shuts down up all my helpers, returning a C{Deferred} that fires when |
|---|
| 53 |
all are stopped. |
|---|
| 54 |
""" |
|---|
| 55 |
d1 = self.keyer.shutdown() |
|---|
| 56 |
d2 = self.observer.shutdown() |
|---|
| 57 |
return defer.DeferredList([d1,d2]) |
|---|
| 58 |
|
|---|
| 59 |
@defer.deferredGenerator |
|---|
| 60 |
def pressAndReleaseKeys(self, keyList): |
|---|
| 61 |
""" |
|---|
| 62 |
This method is decorated with L{defer.deferredGenerator} to immediately |
|---|
| 63 |
return a C{Deferred} that fires upon completion of an iteration over |
|---|
| 64 |
key combinations, which are supplied as sub-sequences of the I{keyList} |
|---|
| 65 |
sequence. The deferred fires with C{True} if all keys were observed to |
|---|
| 66 |
have been pressed and reseased, C{False} otherwise. |
|---|
| 67 |
|
|---|
| 68 |
The generator sends fake X key events for each key of each |
|---|
| 69 |
sub-sequence, a keypress followed by a release, and waits for |
|---|
| 70 |
confirmation of each faked key event before proceeding with the next |
|---|
| 71 |
one. |
|---|
| 72 |
""" |
|---|
| 73 |
running = True |
|---|
| 74 |
|
|---|
| 75 |
def gotConfirmation(confirmed): |
|---|
| 76 |
running = confirmed |
|---|
| 77 |
return confirmed |
|---|
| 78 |
|
|---|
| 79 |
def keyAndConfirm(func, key): |
|---|
| 80 |
d = func(key) |
|---|
| 81 |
d.addCallback(self.observer.confirm) |
|---|
| 82 |
d.addCallback(gotConfirmation) |
|---|
| 83 |
return defer.waitForDeferred(d) |
|---|
| 84 |
|
|---|
| 85 |
stack = [] |
|---|
| 86 |
while running and keyList: |
|---|
| 87 |
keySequence = keyList.pop(0) |
|---|
| 88 |
# Depress each key in order |
|---|
| 89 |
for key in keySequence: |
|---|
| 90 |
stack.append(key) |
|---|
| 91 |
wfd = keyAndConfirm(self.keyer.pressKey, key) |
|---|
| 92 |
yield wfd |
|---|
| 93 |
if not wfd.getResult(): |
|---|
| 94 |
# Key depression not noted, try one more time |
|---|
| 95 |
yield defer.waitForDeferred(self.keyer.releaseKey(key)) |
|---|
| 96 |
wfd = keyAndConfirm(self.keyer.pressKey, key) |
|---|
| 97 |
yield wfd |
|---|
| 98 |
if not wfd.getResult(): |
|---|
| 99 |
# Key depression still not noted, bail out |
|---|
| 100 |
yield defer.waitForDeferred(self.keyer.releaseKey(key)) |
|---|
| 101 |
running = False |
|---|
| 102 |
break |
|---|
| 103 |
# Release each key in reverse order |
|---|
| 104 |
while running and stack: |
|---|
| 105 |
key = stack.pop() |
|---|
| 106 |
wfd = keyAndConfirm(self.keyer.releaseKey, key) |
|---|
| 107 |
yield wfd |
|---|
| 108 |
if not wfd.getResult(): |
|---|
| 109 |
# Key release not noted, bail out |
|---|
| 110 |
running = False |
|---|
| 111 |
# Final yield of a Boolean, rather than a wfd, is supplied to the |
|---|
| 112 |
# deferredGenerator's callback |
|---|
| 113 |
yield running |
|---|
| 114 |
|
|---|
| 115 |
def backspace(self, N): |
|---|
| 116 |
""" |
|---|
| 117 |
Deletes backwards I{N} characters using the C{BackSpace} keysym. |
|---|
| 118 |
""" |
|---|
| 119 |
if self.typingEnabled(): |
|---|
| 120 |
self.history.backspace(N) |
|---|
| 121 |
keyList = [("BackSpace",)] * N |
|---|
| 122 |
d = self.pressAndReleaseKeys(keyList) |
|---|
| 123 |
else: |
|---|
| 124 |
# Typing isn't enabled, just return a Deferred that immediately |
|---|
| 125 |
# fires with False typing-failed status |
|---|
| 126 |
d = defer.succeed(False) |
|---|
| 127 |
return d |
|---|
| 128 |
|
|---|
| 129 |
def insert(self, text): |
|---|
| 130 |
""" |
|---|
| 131 |
Parses the supplied I{text} and sends chunks to the appropriate methods |
|---|
| 132 |
of the keyer, returning C{True} if entry of all text was noted and |
|---|
| 133 |
C{False} if not. |
|---|
| 134 |
""" |
|---|
| 135 |
if self.typingEnabled: |
|---|
| 136 |
text = self.contextAdjust(text) |
|---|
| 137 |
keyList = self.keyCoder.textToKeys(text) |
|---|
| 138 |
d = self.pressAndReleaseKeys(keyList) |
|---|
| 139 |
else: |
|---|
| 140 |
# Typing isn't enabled, just return a Deferred that immediately |
|---|
| 141 |
# fires with False typing-failed status |
|---|
| 142 |
d = defer.succeed(False) |
|---|
| 143 |
return d |
|---|
| 144 |
|
|---|
| 145 |
def typingEnabled(self, text=None): |
|---|
| 146 |
""" |
|---|
| 147 |
Indicates whether typing is currently enabled based on the last text |
|---|
| 148 |
supplied and a recent history of observed keystrokes. Typing is B{not} |
|---|
| 149 |
enabled if any of the following conditions are met: |
|---|
| 150 |
|
|---|
| 151 |
- the last observed keystroke was recently typed and implemented |
|---|
| 152 |
cursor movement; |
|---|
| 153 |
|
|---|
| 154 |
- the last observed keystroke implemented cursor movement and the |
|---|
| 155 |
last-supplied text consists of single word; |
|---|
| 156 |
|
|---|
| 157 |
- the dictation mode is 'sleep' instead of 'active' |
|---|
| 158 |
|
|---|
| 159 |
""" |
|---|
| 160 |
# TODO |
|---|
| 161 |
return True |
|---|
| 162 |
|
|---|
| 163 |
def contextAdjust(self, text): |
|---|
| 164 |
""" |
|---|
| 165 |
Adjusts the supplied I{text} based on a recent history of observed |
|---|
| 166 |
keystrokes, excluding I{charsBackwards} characters that are being |
|---|
| 167 |
skipped over or trimmed off the tail end of the history to reach the |
|---|
| 168 |
insertion point. Here are the rules: |
|---|
| 169 |
|
|---|
| 170 |
- If a word, comma, or semicolon followed by zero or one space was |
|---|
| 171 |
previously entered, and the beginning of this text is another |
|---|
| 172 |
word, this text will be adjusted as needed to have a single space |
|---|
| 173 |
present. |
|---|
| 174 |
|
|---|
| 175 |
- If a period, exclamation point, or question mark followed by zero |
|---|
| 176 |
through two spaces was previously entered, and the beginning of |
|---|
| 177 |
this text is another word, this text will be adjusted as needed |
|---|
| 178 |
to have two spaces present and have the new text start with a |
|---|
| 179 |
capital letter. |
|---|
| 180 |
|
|---|
| 181 |
- If a hyphen was previously entered with no spaces thereafter, any |
|---|
| 182 |
spaces at the beginning of this text will be deleted and the |
|---|
| 183 |
first letter will be forced to lowercase. |
|---|
| 184 |
|
|---|
| 185 |
""" |
|---|
| 186 |
# TODO |
|---|
| 187 |
return text |
|---|
| 188 |
|
|---|