Skip to content

Commit 47461f2

Browse files
committed
Merge pull request #116 from balshetzer/master
A variety of updates
2 parents 7762ff6 + 329c096 commit 47461f2

11 files changed

+340623
-338971
lines changed

plover/assets/american_english_words.txt

+340,422-338,882
Large diffs are not rendered by default.

plover/config.py

+17-8
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
"""Configuration management."""
55

6+
import ConfigParser
67
from ConfigParser import RawConfigParser
78
import os
89
import shutil
@@ -127,20 +128,28 @@ def get_machine_type(self):
127128
DEFAULT_MACHINE_TYPE)
128129

129130
def set_machine_specific_options(self, machine_name, options):
130-
if not self._config.has_section(machine_name):
131-
self._config.add_section(machine_name)
131+
if self._config.has_section(machine_name):
132+
self._config.remove_section(machine_name)
133+
self._config.add_section(machine_name)
132134
for k, v in options.items():
133135
self._config.set(machine_name, k, str(v))
134136

135137
def get_machine_specific_options(self, machine_name):
138+
def convert(p, v):
139+
try:
140+
return p[1](v)
141+
except ValueError:
142+
return p[0]
136143
machine = machine_registry.get(machine_name)
137-
option_info = machine.get_option_info()
144+
info = machine.get_option_info()
145+
defaults = {k: v[0] for k, v in info.items()}
138146
if self._config.has_section(machine_name):
139-
options = dict((o, self._config.get(machine_name, o))
140-
for o in self._config.options(machine_name))
141-
return dict((k, option_info[k][1](v)) for k, v in options.items()
142-
if k in option_info)
143-
return dict((k, v[0]) for k, v in option_info.items())
147+
options = {o: self._config.get(machine_name, o)
148+
for o in self._config.options(machine_name)
149+
if o in info}
150+
options = {k: convert(info[k], v) for k, v in options.items()}
151+
defaults.update(options)
152+
return defaults
144153

145154
def set_dictionary_file_names(self, filenames):
146155
if self._config.has_section(DICTIONARY_CONFIG_SECTION):

plover/machine/stentura.py

+6-5
Original file line numberDiff line numberDiff line change
@@ -559,7 +559,7 @@ def __call__(self):
559559
return cur
560560

561561

562-
def _read(port, stop, seq, request_buf, response_buf, stroke_buf, timeout=1):
562+
def _read(port, stop, seq, request_buf, response_buf, stroke_buf, block, byte, timeout=1):
563563
"""Read the full contents of the current file from beginning to end.
564564
565565
The file should be opened first.
@@ -580,7 +580,7 @@ def _read(port, stop, seq, request_buf, response_buf, stroke_buf, timeout=1):
580580
_ConnectionLostException: If we can't seem to talk to the machine.
581581
582582
"""
583-
bytes_read, block, byte = 0, 0, 0
583+
bytes_read = 0
584584
while True:
585585
packet = _make_read(request_buf, seq(), block, byte, length=512)
586586
response = _send_receive(port, stop, packet, response_buf,
@@ -590,7 +590,7 @@ def _read(port, stop, seq, request_buf, response_buf, stroke_buf, timeout=1):
590590
(p1 == len(response) - 16)): # Data.
591591
raise _ProtocolViolationException()
592592
if p1 == 0:
593-
return buffer(stroke_buf, 0, bytes_read)
593+
return block, byte, buffer(stroke_buf, 0, bytes_read)
594594
data = buffer(response, 14, p1)
595595
_write_to_buffer(stroke_buf, bytes_read, data)
596596
bytes_read += len(data)
@@ -631,10 +631,11 @@ def _loop(port, stop, callback, ready_callback, timeout=1):
631631
# Any checking needed on the response packet?
632632
_send_receive(port, stop, request, response_buf)
633633
# Do a full read to get to the current position in the realtime file.
634-
_read(port, stop, seq, request_buf, response_buf, stroke_buf)
634+
block, byte = 0, 0
635+
block, byte, _ = _read(port, stop, seq, request_buf, response_buf, stroke_buf, block, byte)
635636
ready_callback()
636637
while True:
637-
data = _read(port, stop, seq, request_buf, response_buf, stroke_buf)
638+
block, byte, data = _read(port, stop, seq, request_buf, response_buf, stroke_buf, block, byte)
638639
strokes = _parse_strokes(data)
639640
for stroke in strokes:
640641
callback(stroke)

plover/machine/test_stentura.py

+10-7
Original file line numberDiff line numberDiff line change
@@ -505,23 +505,28 @@ def test_read(self):
505505
requests, responses = make_readc_packets(data)
506506
port = MockPacketPort(responses, requests)
507507
seq = stentura._SequenceCounter()
508-
response = stentura._read(port, event, seq, request_buf,
509-
response_buf, stroke_buf)
508+
block, byte = 0, 0
509+
block, byte, response = stentura._read(port, event, seq, request_buf,
510+
response_buf, stroke_buf, block, byte)
510511
self.assertEqual(data, str(response))
512+
self.assertEqual(block, len(data) / 512)
513+
self.assertEqual(byte, len(data) % 512)
511514

512515
def test_loop(self):
513516
class Event(object):
514517
def __init__(self, count, data, stop=False):
515518
self.count = count
516519
self.data = data
517520
self.stop = stop
521+
522+
def __repr__(self):
523+
return '<{}, {}, {}>'.format(self.count, self.data, self.stop)
518524

519525
class MockPort(object):
520526
def __init__(self, events=[]):
521527
self._file = ''
522528
self._out = ''
523529
self._is_open = False
524-
self._read_pos = 0
525530
self.event = threading.Event()
526531
self.count = 0
527532
self.events = [Event(*x) for x in
@@ -536,17 +541,15 @@ def write(self, request):
536541
if p['action'] == stentura._OPEN:
537542
self._out = make_response(p['seq'], p['action'])
538543
self._is_open = True
539-
self._read_pos = 0
540544
elif p['action'] == stentura._READC:
541545
if not self._is_open:
542546
raise Exception("no open")
543547
length, block, byte = p['p3'], p['p4'], p['p5']
544548
seq = p['seq']
545549
action = stentura._READC
546-
start = self._read_pos + block * 512 + byte
550+
start = block * 512 + byte
547551
end = start + length
548552
data = self._file[start:end]
549-
self._read_pos += len(data)
550553
self._out = make_response(seq, action, p1=len(data),
551554
data=data)
552555
while self.events and self.events[0].count <= self.count:
@@ -584,7 +587,7 @@ def flushOutput(self):
584587
# Ignore data that's there before we started.
585588
(MockPort([(46, '', True)]).append(data2), []),
586589
# Ignore data that was there and also parse some strokes.
587-
(MockPort([(5, data1), (36, '', True)]).append(data2), data1_trans)
590+
(MockPort([(25, data1), (36, '', True)]).append(data2), data1_trans)
588591
]
589592

590593
for test in tests:

plover/orthography.py

+25-20
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@
88
from plover.config import ASSETS_DIR
99

1010
word_list_file_name = os.path.join(ASSETS_DIR, 'american_english_words.txt')
11-
WORDS = set()
11+
WORDS = dict()
1212
try:
1313
with open(word_list_file_name) as f:
14-
WORDS = set(word.strip().lower() for word in f)
14+
pairs = [word.strip().rsplit(' ', 1) for word in f]
15+
pairs.sort(reverse=True, key=lambda x: int(x[1]))
16+
WORDS = {p[0].lower(): int(p[1]) for p in pairs}
1517
except IOError as e:
1618
print e
1719

@@ -59,43 +61,46 @@
5961
]
6062

6163

62-
def try_rules(word, suffix, check=lambda x: True):
63-
"""Add suffix to word using RULES."""
64+
def make_candidates_from_rules(word, suffix, check=lambda x: True):
65+
candidates = []
6466
for r in RULES:
6567
m = r[0].match(word + " ^ " + suffix)
6668
if m:
6769
expanded = m.expand(r[1])
6870
if check(expanded):
69-
return expanded
70-
return None
71-
71+
candidates.append(expanded)
72+
return candidates
7273

7374
def _add_suffix(word, suffix):
7475
in_dict_f = lambda x: x in WORDS
76+
77+
candidates = []
7578

7679
# Try 'ible' and see if it's in the dictionary.
7780
if suffix == 'able':
78-
expanded = try_rules(word, 'ible', in_dict_f)
79-
if expanded:
80-
return expanded
81+
candidates.extend(make_candidates_from_rules(word, 'ible', in_dict_f))
8182

8283
# Try a simple join if it is in the dictionary.
83-
expanded = word + suffix
84-
if expanded in WORDS:
85-
return expanded
84+
simple = word + suffix
85+
if in_dict_f(simple):
86+
candidates.append(simple)
8687

8788
# Try rules with dict lookup.
88-
expanded = try_rules(word, suffix, in_dict_f)
89-
if expanded:
90-
return expanded
89+
candidates.extend(make_candidates_from_rules(word, suffix, in_dict_f))
90+
91+
# For all candidates sort by prominence in dictionary and, since sort is
92+
# stable, also by the order added to candidates list.
93+
if candidates:
94+
candidates.sort(key=lambda x: WORDS[x])
95+
return candidates[0]
9196

9297
# Try rules without dict lookup.
93-
expanded = try_rules(word, suffix)
94-
if expanded:
95-
return expanded
98+
candidates = make_candidates_from_rules(word, suffix)
99+
if candidates:
100+
return candidates[0]
96101

97102
# If all else fails then just do a simple join.
98-
return word + suffix
103+
return simple
99104

100105
def add_suffix(word, suffix):
101106
"""Add a suffix to a word by applying the rules above

plover/oslayer/osxkeyboardcontrol.py

+62-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,40 @@
1-
from Quartz import *
1+
from Quartz import (
2+
CFMachPortCreateRunLoopSource,
3+
CFRunLoopAddSource,
4+
CFRunLoopGetCurrent,
5+
CFRunLoopRun,
6+
CFRunLoopStop,
7+
CGEventCreateKeyboardEvent,
8+
CGEventGetFlags,
9+
CGEventGetIntegerValueField,
10+
CGEventMaskBit,
11+
CGEventPost,
12+
CGEventSetFlags,
13+
CGEventSourceCreate,
14+
CGEventSourceGetSourceStateID,
15+
CGEventTapCreate,
16+
CGEventTapEnable,
17+
kCFRunLoopCommonModes,
18+
kCGEventFlagMaskAlternate,
19+
kCGEventFlagMaskCommand,
20+
kCGEventFlagMaskControl,
21+
kCGEventFlagMaskNonCoalesced,
22+
kCGEventFlagMaskShift,
23+
kCGEventKeyDown,
24+
kCGEventKeyUp,
25+
kCGEventSourceStateID,
26+
kCGEventTapOptionDefault,
27+
kCGHeadInsertEventTap,
28+
kCGKeyboardEventKeycode,
29+
kCGSessionEventTap,
30+
)
231
import threading
332
import collections
33+
import ctypes
34+
import ctypes.util
35+
import objc
36+
import sys
37+
438

539
# This mapping only works on keyboards using the ANSI standard layout. Each
640
# entry represents a sequence of keystrokes that are needed to achieve the
@@ -250,6 +284,26 @@ def is_keyboard_suppressed(self):
250284
return self._suppress_keyboard
251285

252286

287+
# "Narrow python" unicode objects store chracters in UTF-16 so we
288+
# can't iterate over characters in the standard way. This workaround
289+
# let's us iterate over full characters in the string.
290+
def characters(s):
291+
encoded = s.encode('utf-32-be')
292+
characters = []
293+
for i in xrange(len(encoded)/4):
294+
start = i * 4
295+
end = start + 4
296+
character = encoded[start:end].decode('utf-32-be')
297+
yield character
298+
299+
CGEventKeyboardSetUnicodeString = ctypes.cdll.LoadLibrary(ctypes.util.find_library('ApplicationServices')).CGEventKeyboardSetUnicodeString
300+
CGEventKeyboardSetUnicodeString.restype = None
301+
native_utf16 = 'utf-16-le' if sys.byteorder == 'little' else 'utf-16-be'
302+
303+
def set_string(event, s):
304+
buf = s.encode(native_utf16)
305+
CGEventKeyboardSetUnicodeString(objc.pyobjc_id(event), len(buf) / 2, buf)
306+
253307
class KeyboardEmulation(object):
254308

255309
def __init__(self):
@@ -263,8 +317,13 @@ def send_backspaces(self, number_of_backspaces):
263317
CGEventCreateKeyboardEvent(MY_EVENT_SOURCE, 51, False))
264318

265319
def send_string(self, s):
266-
for c in s:
267-
self._send_sequence(down_up(KEYNAME_TO_KEYCODE[LITERALS[c]]))
320+
for c in characters(s):
321+
event = CGEventCreateKeyboardEvent(MY_EVENT_SOURCE, 0, True)
322+
set_string(event, c)
323+
CGEventPost(kCGSessionEventTap, event)
324+
event = CGEventCreateKeyboardEvent(MY_EVENT_SOURCE, 0, False)
325+
set_string(event, c)
326+
CGEventPost(kCGSessionEventTap, event)
268327

269328
def send_key_combination(self, combo_string):
270329
"""Emulate a sequence of key combinations.

plover/oslayer/winkeyboardcontrol.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ class KeyboardEmulation:
162162
"F16": "{F16}",
163163
}
164164

165-
SPECIAL_CHARS_PATTERN = re.compile(r'([{}[\]()+^%~])')
165+
SPECIAL_CHARS_PATTERN = re.compile(r'([]{}()+^%~[])')
166166

167167
def send_backspaces(self, number_of_backspaces):
168168
for _ in xrange(number_of_backspaces):

plover/test_config.py

+21-12
Original file line numberDiff line numberDiff line change
@@ -126,23 +126,17 @@ def get_option_info():
126126
'booloption1': (True, bool_converter),
127127
'booloption2': (False, bool_converter)
128128
}
129+
defaults = {k: v[0] for k, v in FakeMachine.get_option_info().items()}
130+
129131
machine_name = 'machine foo'
130132
registry = Registry()
131133
registry.register(machine_name, FakeMachine)
132134
with patch('plover.config.machine_registry', registry):
133135
c = config.Config()
134136

135137
# Check default value.
136-
expected = {
137-
'stroption1': None,
138-
'intoption1': 3,
139-
'stroption2': 'abc',
140-
'floatoption1': 1,
141-
'booloption1': True,
142-
'booloption2': False,
143-
}
144138
actual = c.get_machine_specific_options(machine_name)
145-
self.assertEqual(actual, expected)
139+
self.assertEqual(actual, defaults)
146140

147141
# Make sure setting a value is reflecting in the getter.
148142
options = {
@@ -153,12 +147,13 @@ def get_option_info():
153147
}
154148
c.set_machine_specific_options(machine_name, options)
155149
actual = c.get_machine_specific_options(machine_name)
156-
self.assertEqual(actual, options)
150+
expected = dict(defaults.items() + options.items())
151+
self.assertEqual(actual, expected)
157152

158-
# Test loading a file.
153+
# Test loading a file. Unknown option is ignored.
159154
s = '\n'.join(('[machine foo]', 'stroption1 = foo',
160155
'intoption1 = 3', 'booloption1 = True',
161-
'booloption2 = False'))
156+
'booloption2 = False', 'unknown = True'))
162157
f = StringIO(s)
163158
c.load(f)
164159
expected = {
@@ -167,13 +162,27 @@ def get_option_info():
167162
'booloption1': True,
168163
'booloption2': False,
169164
}
165+
expected = dict(defaults.items() + expected.items())
170166
actual = c.get_machine_specific_options(machine_name)
171167
self.assertEqual(actual, expected)
172168

173169
# Test saving a file.
174170
f = StringIO()
175171
c.save(f)
176172
self.assertEqual(f.getvalue(), s + '\n\n')
173+
174+
# Test reading invalid values.
175+
s = '\n'.join(['[machine foo]', 'floatoption1 = None',
176+
'booloption2 = True'])
177+
f = StringIO(s)
178+
c.load(f)
179+
expected = {
180+
'floatoption1': 1,
181+
'booloption2': True,
182+
}
183+
expected = dict(defaults.items() + expected.items())
184+
actual = c.get_machine_specific_options(machine_name)
185+
self.assertEqual(actual, expected)
177186

178187
def test_dictionary_option(self):
179188
c = config.Config()

0 commit comments

Comments
 (0)